1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23  * Use is subject to license terms.
24  */
25 
26 #include <time.h>
27 #include <stdio.h>
28 #include <assert.h>
29 #include <string.h>
30 #include <stdlib.h>
31 #include <unistd.h>
32 #include <sys/types.h>
33 #include <sys/wait.h>
34 #include <signal.h>
35 #include <fcntl.h>
36 #include <dhcpmsg.h>
37 
38 #include "agent.h"
39 #include "script_handler.h"
40 #include "states.h"
41 #include "interface.h"
42 
43 /*
44  * scripts are directly managed by a script helper process. dhcpagent creates
45  * the helper process and it, in turn, creates a process to run the script
46  * dhcpagent owns one end of a pipe and the helper process owns the other end
47  * the helper process calls waitpid to wait for the script to exit. an alarm
48  * is set for SCRIPT_TIMEOUT seconds. If the alarm fires, SIGTERM is sent to
49  * the script process and a second alarm is set for SCRIPT_TIMEOUT_GRACE. if
50  * the second alarm fires, SIGKILL is sent to forcefully kill the script. when
51  * script exits, the helper process notifies dhcpagent by closing its end
52  * of the pipe.
53  */
54 
55 unsigned int	script_count;
56 
57 /*
58  * the signal to send to the script process. it is a global variable
59  * to this file as sigterm_handler needs it.
60  */
61 
62 static int	script_signal = SIGTERM;
63 
64 /*
65  * script's absolute timeout value. the first timeout is set to SCRIPT_TIMEOUT
66  * seconds from the time it is started. SIGTERM is sent on the first timeout
67  * the second timeout is set to SCRIPT_TIMEOUT_GRACE from the first timeout
68  * and SIGKILL is sent on the second timeout.
69  */
70 static time_t	timeout;
71 
72 /*
73  * sigalarm_handler(): signal handler for SIGALRM
74  *
75  *   input: int: signal the handler was called with
76  *  output: void
77  */
78 
79 /* ARGSUSED */
80 static void
sigalarm_handler(int sig)81 sigalarm_handler(int sig)
82 {
83 	time_t	now;
84 
85 	/* set a another alarm if it fires too early */
86 	now = time(NULL);
87 	if (now < timeout)
88 		(void) alarm(timeout - now);
89 }
90 
91 /*
92  * sigterm_handler(): signal handler for SIGTERM, fired when dhcpagent wants
93  *		      to stop the script
94  *   input: int: signal the handler was called with
95  *  output: void
96  */
97 
98 /* ARGSUSED */
99 static void
sigterm_handler(int sig)100 sigterm_handler(int sig)
101 {
102 	if (script_signal != SIGKILL) {
103 		/* send SIGKILL SCRIPT_TIMEOUT_GRACE seconds from now */
104 		script_signal = SIGKILL;
105 		timeout = time(NULL) + SCRIPT_TIMEOUT_GRACE;
106 		(void) alarm(SCRIPT_TIMEOUT_GRACE);
107 	}
108 }
109 
110 /*
111  * run_script(): it forks a process to execute the script
112  *
113  *   input: dhcp_smach_t *: the state machine
114  *	    const char *: the event name
115  *	    int: the pipe end owned by the script helper process
116  *  output: void
117  */
118 
119 static void
run_script(dhcp_smach_t * dsmp,const char * event,int fd)120 run_script(dhcp_smach_t *dsmp, const char *event, int fd)
121 {
122 	int		n;
123 	char		c;
124 	char		*path;
125 	char		*name;
126 	pid_t		pid;
127 	time_t		now;
128 
129 	if ((pid = fork()) == -1)
130 		return;
131 
132 	if (pid == 0) {
133 		path = SCRIPT_PATH;
134 		name = strrchr(path, '/') + 1;
135 
136 		/* close all files */
137 		closefrom(0);
138 
139 		/* redirect stdin, stdout and stderr to /dev/null */
140 		if ((n = open("/dev/null", O_RDWR)) < 0)
141 			_exit(127);
142 
143 		(void) dup2(n, STDOUT_FILENO);
144 		(void) dup2(n, STDERR_FILENO);
145 		(void) execl(path, name, dsmp->dsm_name, event, NULL);
146 		_exit(127);
147 	}
148 
149 	/*
150 	 * the first timeout fires SCRIPT_TIMEOUT seconds from now.
151 	 */
152 	timeout = time(NULL) + SCRIPT_TIMEOUT;
153 	(void) sigset(SIGALRM, sigalarm_handler);
154 	(void) alarm(SCRIPT_TIMEOUT);
155 
156 	/*
157 	 * pass script's pid to dhcpagent.
158 	 */
159 	(void) write(fd, &pid, sizeof (pid));
160 
161 	for (;;) {
162 		if (waitpid(pid, NULL, 0) >= 0) {
163 			/* script has exited */
164 			c = SCRIPT_OK;
165 			break;
166 		}
167 
168 		if (errno != EINTR)
169 			return;
170 
171 		now = time(NULL);
172 		if (now >= timeout) {
173 			(void) kill(pid, script_signal);
174 			if (script_signal == SIGKILL) {
175 				c = SCRIPT_KILLED;
176 				break;
177 			}
178 
179 			script_signal = SIGKILL;
180 			timeout = now + SCRIPT_TIMEOUT_GRACE;
181 			(void) alarm(SCRIPT_TIMEOUT_GRACE);
182 		}
183 	}
184 
185 	(void) write(fd, &c, 1);
186 }
187 
188 /*
189  * script_init(): initialize script state on a given state machine
190  *
191  *   input: dhcp_smach_t *: the state machine
192  *  output: void
193  */
194 
195 void
script_init(dhcp_smach_t * dsmp)196 script_init(dhcp_smach_t *dsmp)
197 {
198 	dsmp->dsm_script_pid = -1;
199 	dsmp->dsm_script_helper_pid = -1;
200 	dsmp->dsm_script_event_id = -1;
201 	dsmp->dsm_script_fd = -1;
202 	dsmp->dsm_script_callback = NULL;
203 	dsmp->dsm_script_event = NULL;
204 	dsmp->dsm_callback_arg = NULL;
205 }
206 
207 /*
208  * script_cleanup(): cleanup helper function
209  *
210  *   input: dhcp_smach_t *: the state machine
211  *  output: void
212  */
213 
214 static void
script_cleanup(dhcp_smach_t * dsmp)215 script_cleanup(dhcp_smach_t *dsmp)
216 {
217 	/*
218 	 * We must clear dsm_script_pid prior to invoking the callback or we
219 	 * could get in an infinite loop via async_finish().
220 	 */
221 	dsmp->dsm_script_pid = -1;
222 	dsmp->dsm_script_helper_pid = -1;
223 
224 	if (dsmp->dsm_script_fd != -1) {
225 		assert(dsmp->dsm_script_event_id != -1);
226 		(void) iu_unregister_event(eh, dsmp->dsm_script_event_id, NULL);
227 		(void) close(dsmp->dsm_script_fd);
228 
229 		assert(dsmp->dsm_script_callback != NULL);
230 		dsmp->dsm_script_callback(dsmp, dsmp->dsm_callback_arg);
231 		script_init(dsmp);
232 		script_count--;
233 		release_smach(dsmp);	/* hold from script_start() */
234 	}
235 }
236 
237 /*
238  * script_exit(): does cleanup and invokes the callback when the script exits
239  *
240  *   input: eh_t *: unused
241  *	    int: the end of pipe owned by dhcpagent
242  *	    short: unused
243  *	    eh_event_id_t: unused
244  *	    void *: the state machine
245  *  output: void
246  */
247 
248 /* ARGSUSED */
249 static void
script_exit(iu_eh_t * ehp,int fd,short events,iu_event_id_t id,void * arg)250 script_exit(iu_eh_t *ehp, int fd, short events, iu_event_id_t id, void *arg)
251 {
252 	char c;
253 
254 	if (read(fd, &c, 1) <= 0)
255 		c = SCRIPT_FAILED;
256 
257 	if (c == SCRIPT_OK)
258 		dhcpmsg(MSG_DEBUG, "script ok");
259 	else if (c == SCRIPT_KILLED)
260 		dhcpmsg(MSG_DEBUG, "script killed");
261 	else
262 		dhcpmsg(MSG_DEBUG, "script failed");
263 
264 	script_cleanup(arg);
265 }
266 
267 /*
268  * script_start(): tries to start a script.
269  *		   if a script is already running, it's stopped first.
270  *
271  *
272  *   input: dhcp_smach_t *: the state machine
273  *	    const char *: the event name
274  *	    script_callback_t: callback function
275  *	    void *: data to the callback function
276  *  output: boolean_t: B_TRUE if script starts successfully
277  *	    int *: the returned value of the callback function if script
278  *		starts unsuccessfully
279  */
280 
281 boolean_t
script_start(dhcp_smach_t * dsmp,const char * event,script_callback_t * callback,void * arg,int * status)282 script_start(dhcp_smach_t *dsmp, const char *event,
283     script_callback_t *callback, void *arg, int *status)
284 {
285 	int		n;
286 	int		fds[2];
287 	pid_t		pid;
288 	iu_event_id_t	event_id;
289 
290 	assert(callback != NULL);
291 
292 	if (dsmp->dsm_script_pid != -1) {
293 		/* script is running, stop it */
294 		dhcpmsg(MSG_DEBUG, "script_start: stopping ongoing script");
295 		script_stop(dsmp);
296 	}
297 
298 	if (access(SCRIPT_PATH, X_OK) == -1) {
299 		/* script does not exist */
300 		goto out;
301 	}
302 
303 	/*
304 	 * dhcpagent owns one end of the pipe and script helper process
305 	 * owns the other end. dhcpagent reads on the pipe; and the helper
306 	 * process notifies it when the script exits.
307 	 */
308 	if (pipe(fds) < 0) {
309 		dhcpmsg(MSG_ERROR, "script_start: can't create pipe");
310 		goto out;
311 	}
312 
313 	if ((pid = fork()) < 0) {
314 		dhcpmsg(MSG_ERROR, "script_start: can't fork");
315 		(void) close(fds[0]);
316 		(void) close(fds[1]);
317 		goto out;
318 	}
319 
320 	if (pid == 0) {
321 		/*
322 		 * SIGCHLD is ignored in dhcpagent, the helper process
323 		 * needs it. it calls waitpid to wait for the script to exit.
324 		 */
325 		(void) close(fds[0]);
326 		(void) sigset(SIGCHLD, SIG_DFL);
327 		(void) sigset(SIGTERM, sigterm_handler);
328 		run_script(dsmp, event, fds[1]);
329 		exit(0);
330 	}
331 
332 	(void) close(fds[1]);
333 
334 	/* get the script's pid */
335 	if (read(fds[0], &dsmp->dsm_script_pid, sizeof (pid_t)) !=
336 	    sizeof (pid_t)) {
337 		(void) kill(pid, SIGKILL);
338 		dsmp->dsm_script_pid = -1;
339 		(void) close(fds[0]);
340 		goto out;
341 	}
342 
343 	dsmp->dsm_script_helper_pid = pid;
344 	event_id = iu_register_event(eh, fds[0], POLLIN, script_exit, dsmp);
345 	if (event_id == -1) {
346 		(void) close(fds[0]);
347 		script_stop(dsmp);
348 		goto out;
349 	}
350 
351 	script_count++;
352 	dsmp->dsm_script_event_id = event_id;
353 	dsmp->dsm_script_callback = callback;
354 	dsmp->dsm_script_event = event;
355 	dsmp->dsm_callback_arg = arg;
356 	dsmp->dsm_script_fd = fds[0];
357 	hold_smach(dsmp);
358 	return (B_TRUE);
359 
360 out:
361 	/* callback won't be called in script_exit, so call it here */
362 	n = callback(dsmp, arg);
363 	if (status != NULL)
364 		*status = n;
365 
366 	return (B_FALSE);
367 }
368 
369 /*
370  * script_stop(): stops the script if it is running
371  *
372  *   input: dhcp_smach_t *: the state machine
373  *  output: void
374  */
375 
376 void
script_stop(dhcp_smach_t * dsmp)377 script_stop(dhcp_smach_t *dsmp)
378 {
379 	if (dsmp->dsm_script_pid != -1) {
380 		assert(dsmp->dsm_script_helper_pid != -1);
381 
382 		/*
383 		 * sends SIGTERM to the script and asks the helper process
384 		 * to send SIGKILL if it does not exit after
385 		 * SCRIPT_TIMEOUT_GRACE seconds.
386 		 */
387 		(void) kill(dsmp->dsm_script_pid, SIGTERM);
388 		(void) kill(dsmp->dsm_script_helper_pid, SIGTERM);
389 	}
390 
391 	script_cleanup(dsmp);
392 }
393