1 /*-
2 * Copyright (c) 2014 Baptiste Daroussin <bapt@FreeBSD.org>
3 * Copyright (c) 2014 Vsevolod Stakhov <vsevolod@FreeBSD.org>
4 * Copyright (c) 2025 Aaron LI <aly@aaronly.me>
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer
12 * in this position and unchanged.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29 #include <sys/cdefs.h>
30 #include <sys/procctl.h>
31 #include <sys/resource.h>
32 #include <sys/time.h>
33 #include <sys/wait.h>
34
35 #include <err.h>
36 #include <errno.h>
37 #include <getopt.h>
38 #include <signal.h>
39 #include <stdarg.h>
40 #include <stdbool.h>
41 #include <stdio.h>
42 #include <stdlib.h>
43 #include <string.h>
44 #include <unistd.h>
45
46 #define EXIT_TIMEOUT 124
47 #define EXIT_INVALID 125
48 #define EXIT_CMD_ERROR 126
49 #define EXIT_CMD_NOENT 127
50
51 static volatile sig_atomic_t sig_chld = 0;
52 static volatile sig_atomic_t sig_alrm = 0;
53 static volatile sig_atomic_t sig_term = 0; /* signal to terminate children */
54 static volatile sig_atomic_t sig_other = 0; /* signal to propagate */
55 static int killsig = SIGTERM; /* signal to kill children */
56 static const char *command = NULL;
57 static bool verbose = false;
58
59 static void __dead2
usage(void)60 usage(void)
61 {
62 fprintf(stderr,
63 "Usage: %s [-f | --foreground] [-k time | --kill-after time]"
64 " [-p | --preserve-status] [-s signal | --signal signal] "
65 " [-v | --verbose] <duration> <command> [arg ...]\n",
66 getprogname());
67 exit(EXIT_FAILURE);
68 }
69
70 static void
logv(const char * fmt,...)71 logv(const char *fmt, ...)
72 {
73 va_list ap;
74
75 if (!verbose)
76 return;
77
78 va_start(ap, fmt);
79 vwarnx(fmt, ap);
80 va_end(ap);
81 }
82
83 static double
parse_duration(const char * duration)84 parse_duration(const char *duration)
85 {
86 double ret;
87 char *suffix;
88
89 ret = strtod(duration, &suffix);
90 if (suffix == duration)
91 errx(EXIT_INVALID, "duration is not a number");
92
93 if (*suffix == '\0')
94 return (ret);
95
96 if (suffix[1] != '\0')
97 errx(EXIT_INVALID, "duration unit suffix too long");
98
99 switch (*suffix) {
100 case 's':
101 break;
102 case 'm':
103 ret *= 60;
104 break;
105 case 'h':
106 ret *= 60 * 60;
107 break;
108 case 'd':
109 ret *= 60 * 60 * 24;
110 break;
111 default:
112 errx(EXIT_INVALID, "duration unit suffix invalid");
113 }
114
115 if (ret < 0 || ret >= 100000000UL)
116 errx(EXIT_INVALID, "duration out of range");
117
118 return (ret);
119 }
120
121 static int
parse_signal(const char * str)122 parse_signal(const char *str)
123 {
124 int sig, i;
125 const char *errstr;
126
127 sig = strtonum(str, 1, sys_nsig - 1, &errstr);
128 if (errstr == NULL)
129 return (sig);
130
131 if (strncasecmp(str, "SIG", 3) == 0)
132 str += 3;
133 for (i = 1; i < sys_nsig; i++) {
134 if (strcasecmp(str, sys_signame[i]) == 0)
135 return (i);
136 }
137
138 errx(EXIT_INVALID, "invalid signal");
139 }
140
141 static void
sig_handler(int signo)142 sig_handler(int signo)
143 {
144 if (signo == killsig) {
145 sig_term = signo;
146 return;
147 }
148
149 switch (signo) {
150 case SIGCHLD:
151 sig_chld = 1;
152 break;
153 case SIGALRM:
154 sig_alrm = 1;
155 break;
156 case SIGHUP:
157 case SIGINT:
158 case SIGQUIT:
159 case SIGILL:
160 case SIGTRAP:
161 case SIGABRT:
162 case SIGEMT:
163 case SIGFPE:
164 case SIGBUS:
165 case SIGSEGV:
166 case SIGSYS:
167 case SIGPIPE:
168 case SIGTERM:
169 case SIGXCPU:
170 case SIGXFSZ:
171 case SIGVTALRM:
172 case SIGPROF:
173 case SIGUSR1:
174 case SIGUSR2:
175 /*
176 * Signals with default action to terminate the process.
177 * See the sigaction(2) man page.
178 */
179 sig_term = signo;
180 break;
181 default:
182 sig_other = signo;
183 break;
184 }
185 }
186
187 static void
send_sig(pid_t pid,int signo,bool foreground)188 send_sig(pid_t pid, int signo, bool foreground)
189 {
190 struct procctl_reaper_kill rk;
191
192 logv("sending signal %s(%d) to command '%s'",
193 sys_signame[signo], signo, command);
194 if (foreground) {
195 if (kill(pid, signo) == -1)
196 warnx("kill(%d, %s)", (int)pid, sys_signame[signo]);
197 } else {
198 memset(&rk, 0, sizeof(rk));
199 rk.rk_sig = signo;
200 if (procctl(P_PID, getpid(), PROC_REAP_KILL, &rk) == -1)
201 warnx("procctl(PROC_REAP_KILL)");
202 else if (rk.rk_fpid > 0)
203 warnx("failed to signal some processes: first pid=%d",
204 (int)rk.rk_fpid);
205 logv("signaled %u processes", rk.rk_killed);
206 }
207
208 /*
209 * If the child process was stopped by a signal, POSIX.1-2024
210 * requires to send a SIGCONT signal. However, the standard also
211 * allows to send a SIGCONT regardless of the stop state, as we
212 * are doing here.
213 */
214 if (signo != SIGKILL && signo != SIGSTOP && signo != SIGCONT) {
215 logv("sending signal %s(%d) to command '%s'",
216 sys_signame[SIGCONT], SIGCONT, command);
217 if (foreground) {
218 kill(pid, SIGCONT);
219 } else {
220 memset(&rk, 0, sizeof(rk));
221 rk.rk_sig = SIGCONT;
222 procctl(P_PID, getpid(), PROC_REAP_KILL, &rk);
223 }
224 }
225 }
226
227 static void
set_interval(double iv)228 set_interval(double iv)
229 {
230 struct itimerval tim;
231
232 memset(&tim, 0, sizeof(tim));
233 if (iv > 0) {
234 tim.it_value.tv_sec = (time_t)iv;
235 iv -= (double)(time_t)iv;
236 tim.it_value.tv_usec = (suseconds_t)(iv * 1000000UL);
237 }
238
239 if (setitimer(ITIMER_REAL, &tim, NULL) == -1)
240 err(EXIT_FAILURE, "setitimer()");
241 }
242
243 /*
244 * In order to avoid any possible ambiguity that a shell may not set '$?' to
245 * '128+signal_number', POSIX.1-2024 requires that timeout mimic the wait
246 * status of the child process by terminating itself with the same signal,
247 * while disabling core generation.
248 */
249 static void __dead2
kill_self(int signo)250 kill_self(int signo)
251 {
252 sigset_t mask;
253 struct rlimit rl;
254
255 /* Reset the signal disposition and make sure it's unblocked. */
256 signal(signo, SIG_DFL);
257 sigfillset(&mask);
258 sigdelset(&mask, signo);
259 sigprocmask(SIG_SETMASK, &mask, NULL);
260
261 /* Disable core generation. */
262 memset(&rl, 0, sizeof(rl));
263 setrlimit(RLIMIT_CORE, &rl);
264
265 logv("killing self with signal %s(%d)", sys_signame[signo], signo);
266 kill(getpid(), signo);
267 err(128 + signo, "signal %s(%d) failed to kill self",
268 sys_signame[signo], signo);
269 }
270
271 int
main(int argc,char ** argv)272 main(int argc, char **argv)
273 {
274 int ch, status, sig;
275 int pstat = 0;
276 pid_t pid, cpid;
277 double first_kill;
278 double second_kill = 0;
279 bool foreground = false;
280 bool preserve = false;
281 bool timedout = false;
282 bool do_second_kill = false;
283 bool child_done = false;
284 sigset_t zeromask, allmask, oldmask;
285 struct sigaction sa;
286 struct procctl_reaper_status info;
287
288 const char optstr[] = "+fhk:ps:v";
289 const struct option longopts[] = {
290 { "foreground", no_argument, NULL, 'f' },
291 { "help", no_argument, NULL, 'h' },
292 { "kill-after", required_argument, NULL, 'k' },
293 { "preserve-status", no_argument, NULL, 'p' },
294 { "signal", required_argument, NULL, 's' },
295 { "verbose", no_argument, NULL, 'v' },
296 { NULL, 0, NULL, 0 },
297 };
298
299 while ((ch = getopt_long(argc, argv, optstr, longopts, NULL)) != -1) {
300 switch (ch) {
301 case 'f':
302 foreground = true;
303 break;
304 case 'k':
305 do_second_kill = true;
306 second_kill = parse_duration(optarg);
307 break;
308 case 'p':
309 preserve = true;
310 break;
311 case 's':
312 killsig = parse_signal(optarg);
313 break;
314 case 'v':
315 verbose = true;
316 break;
317 case 0:
318 break;
319 default:
320 usage();
321 }
322 }
323
324 argc -= optind;
325 argv += optind;
326 if (argc < 2)
327 usage();
328
329 first_kill = parse_duration(argv[0]);
330 argc--;
331 argv++;
332 command = argv[0];
333
334 if (!foreground) {
335 /* Acquire a reaper */
336 if (procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL) == -1)
337 err(EXIT_FAILURE, "procctl(PROC_REAP_ACQUIRE)");
338 }
339
340 /* Block all signals to avoid racing against the child. */
341 sigfillset(&allmask);
342 if (sigprocmask(SIG_BLOCK, &allmask, &oldmask) == -1)
343 err(EXIT_FAILURE, "sigprocmask()");
344
345 pid = fork();
346 if (pid == -1) {
347 err(EXIT_FAILURE, "fork()");
348 } else if (pid == 0) {
349 /*
350 * child process
351 *
352 * POSIX.1-2024 requires that the child process inherit the
353 * same signal dispositions as the timeout(1) utility
354 * inherited, except for the signal to be sent upon timeout.
355 */
356 signal(killsig, SIG_DFL);
357 if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1)
358 err(EXIT_FAILURE, "sigprocmask(oldmask)");
359
360 execvp(argv[0], argv);
361 warn("exec(%s)", argv[0]);
362 _exit(errno == ENOENT ? EXIT_CMD_NOENT : EXIT_CMD_ERROR);
363 }
364
365 /* parent continues here */
366
367 /* Catch all signals in order to propagate them. */
368 memset(&sa, 0, sizeof(sa));
369 sigfillset(&sa.sa_mask);
370 sa.sa_handler = sig_handler;
371 sa.sa_flags = SA_RESTART;
372 for (sig = 1; sig < sys_nsig; sig++) {
373 if (sig == SIGKILL || sig == SIGSTOP || sig == SIGCONT ||
374 sig == SIGTTIN || sig == SIGTTOU)
375 continue;
376 if (sigaction(sig, &sa, NULL) == -1)
377 err(EXIT_FAILURE, "sigaction(%d)", sig);
378 }
379
380 /* Don't stop if background child needs TTY */
381 signal(SIGTTIN, SIG_IGN);
382 signal(SIGTTOU, SIG_IGN);
383
384 set_interval(first_kill);
385 sigemptyset(&zeromask);
386
387 for (;;) {
388 sigsuspend(&zeromask);
389
390 if (sig_chld) {
391 sig_chld = 0;
392
393 while ((cpid = waitpid(-1, &status, WNOHANG)) != 0) {
394 if (cpid < 0) {
395 if (errno != EINTR)
396 break;
397 } else if (cpid == pid) {
398 pstat = status;
399 child_done = true;
400 logv("child terminated: pid=%d, "
401 "exit=%d, signal=%d",
402 (int)pid, WEXITSTATUS(status),
403 WTERMSIG(status));
404 } else {
405 /*
406 * Collect grandchildren zombies.
407 * Only effective if we're a reaper.
408 */
409 logv("collected zombie: pid=%d, "
410 "exit=%d, signal=%d",
411 (int)cpid, WEXITSTATUS(status),
412 WTERMSIG(status));
413 }
414 }
415 if (child_done) {
416 if (foreground) {
417 break;
418 } else {
419 procctl(P_PID, getpid(),
420 PROC_REAP_STATUS, &info);
421 if (info.rs_children == 0)
422 break;
423 }
424 }
425 } else if (sig_alrm || sig_term) {
426 if (sig_alrm) {
427 sig = killsig;
428 sig_alrm = 0;
429 timedout = true;
430 logv("time limit reached or received SIGALRM");
431 } else {
432 sig = sig_term;
433 sig_term = 0;
434 logv("received terminating signal %s(%d)",
435 sys_signame[sig], sig);
436 }
437
438 send_sig(pid, sig, foreground);
439
440 if (do_second_kill) {
441 set_interval(second_kill);
442 do_second_kill = false;
443 killsig = SIGKILL;
444 }
445
446 } else if (sig_other) {
447 /* Propagate any other signals. */
448 sig = sig_other;
449 sig_other = 0;
450 logv("received signal %s(%d)", sys_signame[sig], sig);
451
452 send_sig(pid, sig, foreground);
453 }
454 }
455
456 if (!foreground)
457 procctl(P_PID, getpid(), PROC_REAP_RELEASE, NULL);
458
459 if (timedout && !preserve) {
460 pstat = EXIT_TIMEOUT;
461 } else {
462 if (WIFSIGNALED(pstat))
463 kill_self(WTERMSIG(pstat));
464 /* NOTREACHED */
465
466 if (WIFEXITED(pstat))
467 pstat = WEXITSTATUS(pstat);
468 }
469
470 return (pstat);
471 }
472