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