xref: /freebsd/usr.bin/mail/tests/mailx_signal_test.c (revision d5e5e24179f4a98efaadea2b3c43006b322d7f15)
1 /*-
2  * Copyright (c) 2025 Klara, Inc.
3  *
4  * SPDX-License-Identifier: BSD-2-Clause
5  */
6 
7 #include <sys/poll.h>
8 #include <sys/wait.h>
9 
10 #include <fcntl.h>
11 #include <signal.h>
12 #include <stdbool.h>
13 #include <stdlib.h>
14 #include <time.h>
15 #include <unistd.h>
16 
17 #include <atf-c.h>
18 
19 #define MAILX	"mailx"
20 #define BODY	"hello\n"
21 #define BODYLEN	(sizeof(BODY) - 1)
22 
23 /*
24  * When interactive, mailx(1) should print a message on receipt of SIGINT,
25  * then exit cleanly on receipt of a second.
26  *
27  * When not interactive, mailx(1) should terminate on receipt of SIGINT.
28  *
29  * In either case, mailx(1) should terminate on receipt of SIGHUP.
30  */
31 static void
mailx_signal_test(int signo,bool interactive)32 mailx_signal_test(int signo, bool interactive)
33 {
34 	char obuf[1024] = "";
35 	char ebuf[1024] = "";
36 	struct pollfd fds[2];
37 	int ipd[2], opd[2], epd[2], spd[2];
38 	time_t start, now;
39 	size_t olen = 0, elen = 0;
40 	ssize_t rlen;
41 	pid_t pid;
42 	int kc, status;
43 
44 	/* input, output, error, sync pipes */
45 	if (pipe(ipd) != 0 || pipe(opd) != 0 || pipe(epd) != 0 ||
46 	    pipe(spd) != 0 || fcntl(spd[1], F_SETFD, FD_CLOEXEC) != 0)
47 		atf_tc_fail("failed to pipe");
48 	/* fork child */
49 	if ((pid = fork()) < 0)
50 		atf_tc_fail("failed to fork");
51 	if (pid == 0) {
52 		/* child */
53 		sigset_t set;
54 
55 		/*
56 		 * Ensure mailx(1) will handle SIGINT; i.e., that it's not
57 		 * ignored or blocked.
58 		 */
59 		(void)signal(signo, SIG_DFL);
60 		sigemptyset(&set);
61 		sigaddset(&set, signo);
62 		ATF_REQUIRE_INTEQ(0, sigprocmask(SIG_UNBLOCK, &set, NULL));
63 
64 		dup2(ipd[0], STDIN_FILENO);
65 		close(ipd[0]);
66 		close(ipd[1]);
67 		dup2(opd[1], STDOUT_FILENO);
68 		close(opd[0]);
69 		close(opd[1]);
70 		dup2(epd[1], STDERR_FILENO);
71 		close(epd[0]);
72 		close(epd[1]);
73 		close(spd[0]);
74 		/* force dead.letter to go to cwd */
75 		setenv("HOME", ".", 1);
76 		/* exec mailx */
77 		execlp(MAILX,
78 		    MAILX,
79 		    interactive ? "-Is" : "-s",
80 		    "test",
81 		    "test@example.com",
82 		    NULL);
83 		_exit(2);
84 	}
85 	/* parent */
86 	close(ipd[0]);
87 	close(opd[1]);
88 	close(epd[1]);
89 	close(spd[1]);
90 	/* block until child execs or exits */
91 	(void)read(spd[0], &spd[1], sizeof(spd[1]));
92 	/* send one line of input */
93 	ATF_REQUIRE_INTEQ(BODYLEN, write(ipd[1], BODY, BODYLEN));
94 	/* give it a chance to process */
95 	poll(NULL, 0, 2000);
96 	/* send first signal */
97 	ATF_CHECK_INTEQ(0, kill(pid, signo));
98 	kc = 1;
99 	/* receive output until child terminates */
100 	fds[0].fd = opd[0];
101 	fds[0].events = POLLIN;
102 	fds[1].fd = epd[0];
103 	fds[1].events = POLLIN;
104 	time(&start);
105 	for (;;) {
106 		ATF_REQUIRE(poll(fds, 2, 1000) >= 0);
107 		if (fds[0].revents == POLLIN && olen < sizeof(obuf)) {
108 			rlen = read(opd[0], obuf + olen, sizeof(obuf) - olen - 1);
109 			ATF_REQUIRE(rlen >= 0);
110 			olen += rlen;
111 		}
112 		if (fds[1].revents == POLLIN && elen < sizeof(ebuf)) {
113 			rlen = read(epd[0], ebuf + elen, sizeof(ebuf) - elen - 1);
114 			ATF_REQUIRE(rlen >= 0);
115 			elen += rlen;
116 		}
117 		time(&now);
118 		if (now - start > 1 && elen > 0 && kc == 1) {
119 			ATF_CHECK_INTEQ(0, kill(pid, signo));
120 			kc++;
121 		}
122 		if (now - start > 15 && kc > 0) {
123 			(void)kill(pid, SIGKILL);
124 			kc = -1;
125 		}
126 		if (waitpid(pid, &status, WNOHANG) == pid)
127 			break;
128 	}
129 	close(ipd[1]);
130 	close(opd[0]);
131 	close(epd[0]);
132 	close(spd[0]);
133 	/*
134 	 * In interactive mode, SIGINT results in a prompt, and a second
135 	 * SIGINT results in exit(1).  In all other cases, we should see
136 	 * the signal terminate the process.
137 	 */
138 	if (interactive && signo == SIGINT) {
139 		ATF_CHECK(WIFEXITED(status));
140 		if (WIFEXITED(status))
141 			ATF_CHECK_INTEQ(1, WEXITSTATUS(status));
142 		ATF_CHECK_INTEQ(2, kc);
143 		ATF_CHECK_STREQ("", obuf);
144 		ATF_CHECK_MATCH("Interrupt -- one more to kill letter", ebuf);
145 	} else {
146 		ATF_CHECK(WIFSIGNALED(status));
147 		if (WIFSIGNALED(status))
148 			ATF_CHECK_INTEQ(signo, WTERMSIG(status));
149 		ATF_CHECK_INTEQ(1, kc);
150 		ATF_CHECK_STREQ("", obuf);
151 		ATF_CHECK_STREQ("", ebuf);
152 	}
153 	/*
154 	 * In interactive mode, and only in interactive mode, mailx should
155 	 * save whatever was typed before termination in ~/dead.letter.
156 	 * This is why we set HOME to "." in the child.
157 	 */
158 	if (interactive) {
159 		atf_utils_compare_file("dead.letter", BODY);
160 	} else {
161 		ATF_CHECK_INTEQ(-1, access("dead.letter", F_OK));
162 	}
163 }
164 
165 ATF_TC_WITHOUT_HEAD(mailx_sighup_interactive);
ATF_TC_BODY(mailx_sighup_interactive,tc)166 ATF_TC_BODY(mailx_sighup_interactive, tc)
167 {
168 	mailx_signal_test(SIGHUP, true);
169 }
170 
171 ATF_TC_WITHOUT_HEAD(mailx_sighup_noninteractive);
ATF_TC_BODY(mailx_sighup_noninteractive,tc)172 ATF_TC_BODY(mailx_sighup_noninteractive, tc)
173 {
174 	mailx_signal_test(SIGHUP, false);
175 }
176 
177 ATF_TC_WITHOUT_HEAD(mailx_sigint_interactive);
ATF_TC_BODY(mailx_sigint_interactive,tc)178 ATF_TC_BODY(mailx_sigint_interactive, tc)
179 {
180 	mailx_signal_test(SIGINT, true);
181 }
182 
183 ATF_TC_WITHOUT_HEAD(mailx_sigint_noninteractive);
ATF_TC_BODY(mailx_sigint_noninteractive,tc)184 ATF_TC_BODY(mailx_sigint_noninteractive, tc)
185 {
186 	mailx_signal_test(SIGINT, false);
187 }
188 
ATF_TP_ADD_TCS(tp)189 ATF_TP_ADD_TCS(tp)
190 {
191 	ATF_TP_ADD_TC(tp, mailx_sighup_interactive);
192 	ATF_TP_ADD_TC(tp, mailx_sighup_noninteractive);
193 	ATF_TP_ADD_TC(tp, mailx_sigint_interactive);
194 	ATF_TP_ADD_TC(tp, mailx_sigint_noninteractive);
195 	return (atf_no_error());
196 }
197