xref: /openbsd/usr.bin/sendbug/sendbug.c (revision 3f120367)
1 /*	$OpenBSD: sendbug.c,v 1.80 2022/04/10 17:47:54 jca Exp $	*/
2 
3 /*
4  * Written by Ray Lai <ray@cyth.net>.
5  * Public domain.
6  */
7 
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <sys/sysctl.h>
11 #include <sys/wait.h>
12 
13 #include <ctype.h>
14 #include <err.h>
15 #include <errno.h>
16 #include <fcntl.h>
17 #include <limits.h>
18 #include <paths.h>
19 #include <pwd.h>
20 #include <signal.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <unistd.h>
25 
26 #include "atomicio.h"
27 
28 #define _PATH_DMESG "/var/run/dmesg.boot"
29 #define DMESG_START "OpenBSD "
30 #define BEGIN64 "begin-base64 "
31 #define END64 "===="
32 
33 void	checkfile(const char *);
34 void	debase(void);
35 void	dmesg(FILE *);
36 int	editit(const char *);
37 void	hwdump(FILE *);
38 void	init(void);
39 int	matchline(const char *, const char *, size_t);
40 int	prompt(void);
41 int	send_file(const char *, int);
42 int	sendmail(const char *);
43 void	template(FILE *);
44 void	usbdevs(FILE *);
45 
46 const char *categories = "system user library documentation kernel "
47     "alpha aarch64 amd64 arm hppa i386 m88k mips64 mips64el powerpc powerpc64 "
48     "riscv64 sh sparc64";
49 const char *comment[] = {
50 	"<synopsis of the problem (one line)>",
51 	"<PR category (one line)>",
52 	"<precise description of the problem (multiple lines)>",
53 	"<code/input/activities to reproduce the problem (multiple lines)>",
54 	"<how to correct or work around the problem, if known (multiple lines)>"
55 };
56 
57 struct passwd *pw;
58 char os[BUFSIZ], rel[BUFSIZ], mach[BUFSIZ], details[BUFSIZ];
59 const char *tmpdir = _PATH_TMP;
60 char *tmppath;
61 int Dflag, Pflag, wantcleanup;
62 
63 __dead void
usage(void)64 usage(void)
65 {
66 	extern char *__progname;
67 
68 	fprintf(stderr, "usage: %s [-DEP]\n", __progname);
69 	exit(1);
70 }
71 
72 void
cleanup()73 cleanup()
74 {
75 	if (wantcleanup && tmppath && unlink(tmppath) == -1)
76 		warn("unlink");
77 }
78 
79 
80 int
main(int argc,char * argv[])81 main(int argc, char *argv[])
82 {
83 	int ch, c, fd, ret = 1;
84 	struct stat sb;
85 	char *pr_form;
86 	time_t mtime;
87 	FILE *fp;
88 
89 	if (pledge("stdio rpath wpath cpath tmppath getpw proc exec", NULL) == -1)
90 		err(1, "pledge");
91 
92 	while ((ch = getopt(argc, argv, "DEP")) != -1)
93 		switch (ch) {
94 		case 'D':
95 			Dflag = 1;
96 			break;
97 		case 'E':
98 			debase();
99 			exit(0);
100 		case 'P':
101 			Pflag = 1;
102 			break;
103 		default:
104 			usage();
105 		}
106 	argc -= optind;
107 	argv += optind;
108 
109 	if (argc > 0)
110 		usage();
111 
112 	if (Pflag) {
113 		init();
114 		template(stdout);
115 		exit(0);
116 	}
117 
118 	if (asprintf(&tmppath, "%s%sp.XXXXXXXXXX", tmpdir,
119 	    tmpdir[strlen(tmpdir) - 1] == '/' ? "" : "/") == -1)
120 		err(1, "asprintf");
121 	if ((fd = mkstemp(tmppath)) == -1)
122 		err(1, "mkstemp");
123 	wantcleanup = 1;
124 	atexit(cleanup);
125 	if ((fp = fdopen(fd, "w+")) == NULL)
126 		err(1, "fdopen");
127 
128 	init();
129 
130 	pr_form = getenv("PR_FORM");
131 	if (pr_form) {
132 		char buf[BUFSIZ];
133 		size_t len;
134 		FILE *frfp;
135 
136 		frfp = fopen(pr_form, "r");
137 		if (frfp == NULL) {
138 			warn("can't seem to read your template file "
139 			    "(`%s'), ignoring PR_FORM", pr_form);
140 			template(fp);
141 		} else {
142 			while (!feof(frfp)) {
143 				len = fread(buf, 1, sizeof buf, frfp);
144 				if (len == 0)
145 					break;
146 				if (fwrite(buf, 1, len, fp) != len)
147 					break;
148 			}
149 			fclose(frfp);
150 		}
151 	} else
152 		template(fp);
153 
154 	if (fflush(fp) == EOF || fstat(fd, &sb) == -1 || fclose(fp) == EOF)
155 		err(1, "error creating template");
156 	mtime = sb.st_mtime;
157 
158  edit:
159 	if (editit(tmppath) == -1)
160 		err(1, "error running editor");
161 
162 	if (stat(tmppath, &sb) == -1)
163 		err(1, "stat");
164 	if (mtime == sb.st_mtime)
165 		errx(1, "report unchanged, nothing sent");
166 
167  prompt:
168 	checkfile(tmppath);
169 	c = prompt();
170 	switch (c) {
171 	case 'a':
172 	case EOF:
173 		wantcleanup = 0;
174 		errx(1, "unsent report in %s", tmppath);
175 	case 'e':
176 		goto edit;
177 	case 's':
178 		if (sendmail(tmppath) == -1)
179 			goto quit;
180 		break;
181 	default:
182 		goto prompt;
183 	}
184 
185 	ret = 0;
186 quit:
187 	return (ret);
188 }
189 
190 void
dmesg(FILE * fp)191 dmesg(FILE *fp)
192 {
193 	char buf[BUFSIZ];
194 	FILE *dfp;
195 	off_t offset = -1;
196 
197 	dfp = fopen(_PATH_DMESG, "r");
198 	if (dfp == NULL) {
199 		warn("can't read dmesg");
200 		return;
201 	}
202 
203 	/* Find last dmesg. */
204 	for (;;) {
205 		off_t o;
206 
207 		o = ftello(dfp);
208 		if (fgets(buf, sizeof(buf), dfp) == NULL)
209 			break;
210 		if (!strncmp(DMESG_START, buf, sizeof(DMESG_START) - 1))
211 			offset = o;
212 	}
213 	if (offset != -1) {
214 		size_t len;
215 
216 		clearerr(dfp);
217 		fseeko(dfp, offset, SEEK_SET);
218 		while (offset != -1 && !feof(dfp)) {
219 			len = fread(buf, 1, sizeof buf, dfp);
220 			if (len == 0)
221 				break;
222 			if (fwrite(buf, 1, len, fp) != len)
223 				break;
224 		}
225 	}
226 	fclose(dfp);
227 }
228 
229 void
usbdevs(FILE * ofp)230 usbdevs(FILE *ofp)
231 {
232 	char buf[BUFSIZ];
233 	FILE *ifp;
234 	size_t len;
235 
236 	if ((ifp = popen("usbdevs -v", "r")) != NULL) {
237 		while (!feof(ifp)) {
238 			len = fread(buf, 1, sizeof buf, ifp);
239 			if (len == 0)
240 				break;
241 			if (fwrite(buf, 1, len, ofp) != len)
242 				break;
243 		}
244 		pclose(ifp);
245 	}
246 }
247 
248 /*
249  * Execute an editor on the specified pathname, which is interpreted
250  * from the shell.  This means flags may be included.
251  *
252  * Returns -1 on error, or the exit value on success.
253  */
254 int
editit(const char * pathname)255 editit(const char *pathname)
256 {
257 	char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p;
258 	sig_t sighup, sigint, sigquit, sigchld;
259 	pid_t pid;
260 	int saved_errno, st, ret = -1;
261 
262 	ed = getenv("VISUAL");
263 	if (ed == NULL || ed[0] == '\0')
264 		ed = getenv("EDITOR");
265 	if (ed == NULL || ed[0] == '\0')
266 		ed = _PATH_VI;
267 	if (asprintf(&p, "%s %s", ed, pathname) == -1)
268 		return (-1);
269 	argp[2] = p;
270 
271 	sighup = signal(SIGHUP, SIG_IGN);
272 	sigint = signal(SIGINT, SIG_IGN);
273 	sigquit = signal(SIGQUIT, SIG_IGN);
274 	sigchld = signal(SIGCHLD, SIG_DFL);
275 	if ((pid = fork()) == -1)
276 		goto fail;
277 	if (pid == 0) {
278 		execv(_PATH_BSHELL, argp);
279 		_exit(127);
280 	}
281 	while (waitpid(pid, &st, 0) == -1) {
282 		if (errno != EINTR)
283 			goto fail;
284 	}
285 	if (!WIFEXITED(st))
286 		errno = EINTR;
287 	else
288 		ret = WEXITSTATUS(st);
289 
290  fail:
291 	saved_errno = errno;
292 	(void)signal(SIGHUP, sighup);
293 	(void)signal(SIGINT, sigint);
294 	(void)signal(SIGQUIT, sigquit);
295 	(void)signal(SIGCHLD, sigchld);
296 	free(p);
297 	errno = saved_errno;
298 	return (ret);
299 }
300 
301 int
prompt(void)302 prompt(void)
303 {
304 	int c, ret;
305 
306 	fpurge(stdin);
307 	fprintf(stderr, "a)bort, e)dit, or s)end: ");
308 	fflush(stderr);
309 	ret = getchar();
310 	if (ret == EOF || ret == '\n')
311 		return (ret);
312 	do {
313 		c = getchar();
314 	} while (c != EOF && c != '\n');
315 	return (ret);
316 }
317 
318 int
sendmail(const char * pathname)319 sendmail(const char *pathname)
320 {
321 	int filedes[2];
322 	pid_t pid;
323 
324 	if (pipe(filedes) == -1) {
325 		warn("pipe: unsent report in %s", pathname);
326 		return (-1);
327 	}
328 	switch ((pid = fork())) {
329 	case -1:
330 		warn("fork error: unsent report in %s",
331 		    pathname);
332 		return (-1);
333 	case 0:
334 		close(filedes[1]);
335 		if (dup2(filedes[0], STDIN_FILENO) == -1) {
336 			warn("dup2 error: unsent report in %s",
337 			    pathname);
338 			return (-1);
339 		}
340 		close(filedes[0]);
341 		execl(_PATH_SENDMAIL, "sendmail",
342 		    "-oi", "-t", (char *)NULL);
343 		warn("sendmail error: unsent report in %s",
344 		    pathname);
345 		return (-1);
346 	default:
347 		close(filedes[0]);
348 		/* Pipe into sendmail. */
349 		if (send_file(pathname, filedes[1]) == -1) {
350 			warn("send_file error: unsent report in %s",
351 			    pathname);
352 			return (-1);
353 		}
354 		close(filedes[1]);
355 		while (waitpid(pid, NULL, 0) == -1) {
356 			if (errno != EINTR)
357 				break;
358 		}
359 		break;
360 	}
361 	return (0);
362 }
363 
364 void
init(void)365 init(void)
366 {
367 	size_t len;
368 	int sysname[2];
369 	char *cp;
370 
371 	if ((pw = getpwuid(getuid())) == NULL)
372 		err(1, "getpwuid");
373 
374 	sysname[0] = CTL_KERN;
375 	sysname[1] = KERN_OSTYPE;
376 	len = sizeof(os) - 1;
377 	if (sysctl(sysname, 2, &os, &len, NULL, 0) == -1)
378 		err(1, "sysctl");
379 
380 	sysname[0] = CTL_KERN;
381 	sysname[1] = KERN_OSRELEASE;
382 	len = sizeof(rel) - 1;
383 	if (sysctl(sysname, 2, &rel, &len, NULL, 0) == -1)
384 		err(1, "sysctl");
385 
386 	sysname[0] = CTL_KERN;
387 	sysname[1] = KERN_VERSION;
388 	len = sizeof(details) - 1;
389 	if (sysctl(sysname, 2, &details, &len, NULL, 0) == -1)
390 		err(1, "sysctl");
391 
392 	cp = strchr(details, '\n');
393 	if (cp) {
394 		cp++;
395 		if (*cp)
396 			*cp++ = '\t';
397 		if (*cp)
398 			*cp++ = '\t';
399 		if (*cp)
400 			*cp++ = '\t';
401 	}
402 
403 	sysname[0] = CTL_HW;
404 	sysname[1] = HW_MACHINE;
405 	len = sizeof(mach) - 1;
406 	if (sysctl(sysname, 2, &mach, &len, NULL, 0) == -1)
407 		err(1, "sysctl");
408 }
409 
410 int
send_file(const char * file,int dst)411 send_file(const char *file, int dst)
412 {
413 	size_t len;
414 	char *buf, *lbuf;
415 	FILE *fp;
416 	int rval = -1, saved_errno;
417 
418 	if ((fp = fopen(file, "r")) == NULL)
419 		return (-1);
420 	lbuf = NULL;
421 	while ((buf = fgetln(fp, &len))) {
422 		if (buf[len - 1] == '\n') {
423 			buf[len - 1] = '\0';
424 			--len;
425 		} else {
426 			/* EOF without EOL, copy and add the NUL */
427 			if ((lbuf = malloc(len + 1)) == NULL)
428 				goto end;
429 			memcpy(lbuf, buf, len);
430 			lbuf[len] = '\0';
431 			buf = lbuf;
432 		}
433 
434 		/* Skip lines starting with "SENDBUG". */
435 		if (strncmp(buf, "SENDBUG", sizeof("SENDBUG") - 1) == 0)
436 			continue;
437 		while (len) {
438 			char *sp = NULL, *ep = NULL;
439 			size_t copylen;
440 
441 			if ((sp = strchr(buf, '<')) != NULL) {
442 				size_t i;
443 
444 				for (i = 0; i < sizeof(comment) / sizeof(*comment); ++i) {
445 					size_t commentlen = strlen(comment[i]);
446 
447 					if (strncmp(sp, comment[i], commentlen) == 0) {
448 						ep = sp + commentlen - 1;
449 						break;
450 					}
451 				}
452 			}
453 			/* Length of string before comment. */
454 			if (ep)
455 				copylen = sp - buf;
456 			else
457 				copylen = len;
458 			if (atomicio(vwrite, dst, buf, copylen) != copylen)
459 				goto end;
460 			if (!ep)
461 				break;
462 			/* Skip comment. */
463 			len -= ep - buf + 1;
464 			buf = ep + 1;
465 		}
466 		if (atomicio(vwrite, dst, "\n", 1) != 1)
467 			goto end;
468 	}
469 	rval = 0;
470  end:
471 	saved_errno = errno;
472 	free(lbuf);
473 	fclose(fp);
474 	errno = saved_errno;
475 	return (rval);
476 }
477 
478 /*
479  * Does line start with `s' and end with non-comment and non-whitespace?
480  * Note: Does not treat `line' as a C string.
481  */
482 int
matchline(const char * s,const char * line,size_t linelen)483 matchline(const char *s, const char *line, size_t linelen)
484 {
485 	size_t slen;
486 	int iscomment;
487 
488 	slen = strlen(s);
489 	/* Is line shorter than string? */
490 	if (linelen <= slen)
491 		return (0);
492 	/* Does line start with string? */
493 	if (memcmp(line, s, slen) != 0)
494 		return (0);
495 	/* Does line contain anything but comments and whitespace? */
496 	line += slen;
497 	linelen -= slen;
498 	iscomment = 0;
499 	while (linelen) {
500 		if (iscomment) {
501 			if (*line == '>')
502 				iscomment = 0;
503 		} else if (*line == '<')
504 			iscomment = 1;
505 		else if (!isspace((unsigned char)*line))
506 			return (1);
507 		++line;
508 		--linelen;
509 	}
510 	return (0);
511 }
512 
513 /*
514  * Are all required fields filled out?
515  */
516 void
checkfile(const char * pathname)517 checkfile(const char *pathname)
518 {
519 	FILE *fp;
520 	size_t len;
521 	int category = 0, synopsis = 0, subject = 0;
522 	char *buf;
523 
524 	if ((fp = fopen(pathname, "r")) == NULL) {
525 		warn("%s", pathname);
526 		return;
527 	}
528 	while ((buf = fgetln(fp, &len))) {
529 		if (matchline(">Category:", buf, len))
530 			category = 1;
531 		else if (matchline(">Synopsis:", buf, len))
532 			synopsis = 1;
533 		else if (matchline("Subject:", buf, len))
534 			subject = 1;
535 	}
536 	fclose(fp);
537 	if (!category || !synopsis || !subject) {
538 		fprintf(stderr, "Some fields are blank, please fill them in: ");
539 		if (!subject)
540 			fprintf(stderr, "Subject ");
541 		if (!synopsis)
542 			fprintf(stderr, "Synopsis ");
543 		if (!category)
544 			fprintf(stderr, "Category ");
545 		fputc('\n', stderr);
546 	}
547 }
548 
549 void
template(FILE * fp)550 template(FILE *fp)
551 {
552 	fprintf(fp, "SENDBUG: -*- sendbug -*-\n");
553 	fprintf(fp, "SENDBUG: Lines starting with `SENDBUG' will"
554 	    " be removed automatically.\n");
555 	fprintf(fp, "SENDBUG:\n");
556 	fprintf(fp, "SENDBUG: Choose from the following categories:\n");
557 	fprintf(fp, "SENDBUG:\n");
558 	fprintf(fp, "SENDBUG: %s\n", categories);
559 	fprintf(fp, "SENDBUG:\n");
560 	fprintf(fp, "SENDBUG:\n");
561 	fprintf(fp, "To: %s\n", "bugs@openbsd.org");
562 	fprintf(fp, "Subject: \n");
563 	fprintf(fp, "From: %s\n", pw->pw_name);
564 	fprintf(fp, "Cc: %s\n", pw->pw_name);
565 	fprintf(fp, "Reply-To: %s\n", pw->pw_name);
566 	fprintf(fp, "\n");
567 	fprintf(fp, ">Synopsis:\t%s\n", comment[0]);
568 	fprintf(fp, ">Category:\t%s\n", comment[1]);
569 	fprintf(fp, ">Environment:\n");
570 	fprintf(fp, "\tSystem      : %s %s\n", os, rel);
571 	fprintf(fp, "\tDetails     : %s\n", details);
572 	fprintf(fp, "\tArchitecture: %s.%s\n", os, mach);
573 	fprintf(fp, "\tMachine     : %s\n", mach);
574 	fprintf(fp, ">Description:\n");
575 	fprintf(fp, "\t%s\n", comment[2]);
576 	fprintf(fp, ">How-To-Repeat:\n");
577 	fprintf(fp, "\t%s\n", comment[3]);
578 	fprintf(fp, ">Fix:\n");
579 	fprintf(fp, "\t%s\n", comment[4]);
580 
581 	if (!Dflag) {
582 		int root;
583 
584 		fprintf(fp, "\n");
585 		root = !geteuid();
586 		if (!root)
587 			fprintf(fp, "SENDBUG: Run sendbug as root "
588 			    "if this is an ACPI report!\n");
589 		fprintf(fp, "SENDBUG: dmesg%s and usbdevs are attached.\n"
590 		    "SENDBUG: Feel free to delete or use the -D flag if they "
591 		    "contain sensitive information.\n",
592 		    root ? ", pcidump, acpidump" : "");
593 		fputs("\ndmesg:\n", fp);
594 		dmesg(fp);
595 		fputs("\nusbdevs:\n", fp);
596 		usbdevs(fp);
597 		if (root)
598 			hwdump(fp);
599 	}
600 }
601 
602 void
hwdump(FILE * ofp)603 hwdump(FILE *ofp)
604 {
605 	char buf[BUFSIZ];
606 	FILE *ifp;
607 	char *cmd, *acpidir;
608 	size_t len;
609 
610 	if (asprintf(&acpidir, "%s%sp.XXXXXXXXXX", tmpdir,
611 	    tmpdir[strlen(tmpdir) - 1] == '/' ? "" : "/") == -1)
612 		err(1, "asprintf");
613 	if (mkdtemp(acpidir) == NULL)
614 		err(1, "mkdtemp");
615 
616 	if (asprintf(&cmd, "echo \"\\npcidump:\"; pcidump -xxv; "
617 	    "echo \"\\nacpidump:\"; cd %s && cp /var/db/acpi/* .; "
618 	    "for i in *; do b64encode $i $i; done; rm -rf %s",
619 	    acpidir, acpidir) == -1)
620 		err(1, "asprintf");
621 
622 	if ((ifp = popen(cmd, "r")) != NULL) {
623 		while (!feof(ifp)) {
624 			len = fread(buf, 1, sizeof buf, ifp);
625 			if (len == 0)
626 				break;
627 			if (fwrite(buf, 1, len, ofp) != len)
628 				break;
629 		}
630 		pclose(ifp);
631 	}
632 	free(cmd);
633 	free(acpidir);
634 }
635 
636 void
debase(void)637 debase(void)
638 {
639 	char buf[BUFSIZ];
640 	FILE *fp = NULL;
641 	size_t len;
642 
643 	while (fgets(buf, sizeof(buf), stdin) != NULL) {
644 		len = strlen(buf);
645 		if (!strncmp(buf, BEGIN64, sizeof(BEGIN64) - 1)) {
646 			if (fp)
647 				errx(1, "double begin");
648 			fp = popen("b64decode", "w");
649 			if (!fp)
650 				errx(1, "popen b64decode");
651 		}
652 		if (fp && fwrite(buf, 1, len, fp) != len)
653 			errx(1, "pipe error");
654 		if (!strncmp(buf, END64, sizeof(END64) - 1)) {
655 			if (pclose(fp) == -1)
656 				errx(1, "pclose b64decode");
657 			fp = NULL;
658 		}
659 	}
660 }
661