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 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); 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); 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); 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); 184 ATF_TC_BODY(mailx_sigint_noninteractive, tc) 185 { 186 mailx_signal_test(SIGINT, false); 187 } 188 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