Home | History | Annotate | Download | only in dhcpagent
      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 2007 Sun Microsystems, Inc.  All rights reserved.
     23  * Use is subject to license terms.
     24  */
     25 
     26 #pragma ident	"%Z%%M%	%I%	%E% SMI"
     27 
     28 #include <unistd.h>
     29 #include <sys/types.h>
     30 #include <sys/stat.h>
     31 #include <stdlib.h>
     32 #include <netinet/in.h>		/* struct in_addr */
     33 #include <netinet/dhcp.h>
     34 #include <signal.h>
     35 #include <sys/socket.h>
     36 #include <net/route.h>
     37 #include <net/if_arp.h>
     38 #include <string.h>
     39 #include <dhcpmsg.h>
     40 #include <ctype.h>
     41 #include <netdb.h>
     42 #include <fcntl.h>
     43 #include <stdio.h>
     44 
     45 #include "states.h"
     46 #include "agent.h"
     47 #include "interface.h"
     48 #include "util.h"
     49 #include "packet.h"
     50 
     51 /*
     52  * this file contains utility functions that have no real better home
     53  * of their own.  they can largely be broken into six categories:
     54  *
     55  *  o  conversion functions -- functions to turn integers into strings,
     56  *     or to convert between units of a similar measure.
     57  *
     58  *  o  time and timer functions -- functions to handle time measurement
     59  *     and events.
     60  *
     61  *  o  ipc-related functions -- functions to simplify the generation of
     62  *     ipc messages to the agent's clients.
     63  *
     64  *  o  signal-related functions -- functions to clean up the agent when
     65  *     it receives a signal.
     66  *
     67  *  o  routing table manipulation functions
     68  *
     69  *  o  true miscellany -- anything else
     70  */
     71 
     72 /*
     73  * pkt_type_to_string(): stringifies a packet type
     74  *
     75  *   input: uchar_t: a DHCP packet type value, RFC 2131 or 3315
     76  *	    boolean_t: B_TRUE if IPv6
     77  *  output: const char *: the stringified packet type
     78  */
     79 
     80 const char *
     81 pkt_type_to_string(uchar_t type, boolean_t isv6)
     82 {
     83 	/*
     84 	 * note: the ordering in these arrays allows direct indexing of the
     85 	 *	 table based on the RFC packet type value passed in.
     86 	 */
     87 
     88 	static const char *v4types[] = {
     89 		"BOOTP",  "DISCOVER", "OFFER",   "REQUEST", "DECLINE",
     90 		"ACK",    "NAK",      "RELEASE", "INFORM"
     91 	};
     92 	static const char *v6types[] = {
     93 		NULL, "SOLICIT", "ADVERTISE", "REQUEST",
     94 		"CONFIRM", "RENEW", "REBIND", "REPLY",
     95 		"RELEASE", "DECLINE", "RECONFIGURE", "INFORMATION-REQUEST",
     96 		"RELAY-FORW", "RELAY-REPL"
     97 	};
     98 
     99 	if (isv6) {
    100 		if (type >= sizeof (v6types) / sizeof (*v6types) ||
    101 		    v6types[type] == NULL)
    102 			return ("<unknown>");
    103 		else
    104 			return (v6types[type]);
    105 	} else {
    106 		if (type >= sizeof (v4types) / sizeof (*v4types) ||
    107 		    v4types[type] == NULL)
    108 			return ("<unknown>");
    109 		else
    110 			return (v4types[type]);
    111 	}
    112 }
    113 
    114 /*
    115  * monosec_to_string(): converts a monosec_t into a date string
    116  *
    117  *   input: monosec_t: the monosec_t to convert
    118  *  output: const char *: the corresponding date string
    119  */
    120 
    121 const char *
    122 monosec_to_string(monosec_t monosec)
    123 {
    124 	time_t	time = monosec_to_time(monosec);
    125 	char	*time_string = ctime(&time);
    126 
    127 	/* strip off the newline -- ugh, why, why, why.. */
    128 	time_string[strlen(time_string) - 1] = '\0';
    129 	return (time_string);
    130 }
    131 
    132 /*
    133  * monosec(): returns a monotonically increasing time in seconds that
    134  *            is not affected by stime(2) or adjtime(2).
    135  *
    136  *   input: void
    137  *  output: monosec_t: the number of seconds since some time in the past
    138  */
    139 
    140 monosec_t
    141 monosec(void)
    142 {
    143 	return (gethrtime() / NANOSEC);
    144 }
    145 
    146 /*
    147  * monosec_to_time(): converts a monosec_t into real wall time
    148  *
    149  *    input: monosec_t: the absolute monosec_t to convert
    150  *   output: time_t: the absolute time that monosec_t represents in wall time
    151  */
    152 
    153 time_t
    154 monosec_to_time(monosec_t abs_monosec)
    155 {
    156 	return (abs_monosec - monosec()) + time(NULL);
    157 }
    158 
    159 /*
    160  * hrtime_to_monosec(): converts a hrtime_t to monosec_t
    161  *
    162  *    input: hrtime_t: the time to convert
    163  *   output: monosec_t: the time in monosec_t
    164  */
    165 
    166 monosec_t
    167 hrtime_to_monosec(hrtime_t hrtime)
    168 {
    169 	return (hrtime / NANOSEC);
    170 }
    171 
    172 /*
    173  * print_server_msg(): prints a message from a DHCP server
    174  *
    175  *   input: dhcp_smach_t *: the state machine the message is associated with
    176  *	    const char *: the string to display
    177  *	    uint_t: length of string
    178  *  output: void
    179  */
    180 
    181 void
    182 print_server_msg(dhcp_smach_t *dsmp, const char *msg, uint_t msglen)
    183 {
    184 	if (msglen > 0) {
    185 		dhcpmsg(MSG_INFO, "%s: message from server: %.*s",
    186 		    dsmp->dsm_name, msglen, msg);
    187 	}
    188 }
    189 
    190 /*
    191  * alrm_exit(): Signal handler for SIGARLM. terminates grandparent.
    192  *
    193  *    input: int: signal the handler was called with.
    194  *
    195  *   output: void
    196  */
    197 
    198 static void
    199 alrm_exit(int sig)
    200 {
    201 	int exitval;
    202 
    203 	if (sig == SIGALRM && grandparent != 0)
    204 		exitval = EXIT_SUCCESS;
    205 	else
    206 		exitval = EXIT_FAILURE;
    207 
    208 	_exit(exitval);
    209 }
    210 
    211 /*
    212  * daemonize(): daemonizes the process
    213  *
    214  *   input: void
    215  *  output: int: 1 on success, 0 on failure
    216  */
    217 
    218 int
    219 daemonize(void)
    220 {
    221 	/*
    222 	 * We've found that adoption takes sufficiently long that
    223 	 * a dhcpinfo run after dhcpagent -a is started may occur
    224 	 * before the agent is ready to process the request.
    225 	 * The result is an error message and an unhappy user.
    226 	 *
    227 	 * The initial process now sleeps for DHCP_ADOPT_SLEEP,
    228 	 * unless interrupted by a SIGALRM, in which case it
    229 	 * exits immediately. This has the effect that the
    230 	 * grandparent doesn't exit until the dhcpagent is ready
    231 	 * to process requests. This defers the the balance of
    232 	 * the system start-up script processing until the
    233 	 * dhcpagent is ready to field requests.
    234 	 *
    235 	 * grandparent is only set for the adopt case; other
    236 	 * cases do not require the wait.
    237 	 */
    238 
    239 	if (grandparent != 0)
    240 		(void) signal(SIGALRM, alrm_exit);
    241 
    242 	switch (fork()) {
    243 
    244 	case -1:
    245 		return (0);
    246 
    247 	case  0:
    248 		if (grandparent != 0)
    249 			(void) signal(SIGALRM, SIG_DFL);
    250 
    251 		/*
    252 		 * setsid() makes us lose our controlling terminal,
    253 		 * and become both a session leader and a process
    254 		 * group leader.
    255 		 */
    256 
    257 		(void) setsid();
    258 
    259 		/*
    260 		 * under POSIX, a session leader can accidentally
    261 		 * (through open(2)) acquire a controlling terminal if
    262 		 * it does not have one.  just to be safe, fork again
    263 		 * so we are not a session leader.
    264 		 */
    265 
    266 		switch (fork()) {
    267 
    268 		case -1:
    269 			return (0);
    270 
    271 		case 0:
    272 			(void) signal(SIGHUP, SIG_IGN);
    273 			(void) chdir("/");
    274 			(void) umask(022);
    275 			closefrom(0);
    276 			break;
    277 
    278 		default:
    279 			_exit(EXIT_SUCCESS);
    280 		}
    281 		break;
    282 
    283 	default:
    284 		if (grandparent != 0) {
    285 			(void) signal(SIGCHLD, SIG_IGN);
    286 			/*
    287 			 * Note that we're not the agent here, so the DHCP
    288 			 * logging subsystem hasn't been configured yet.
    289 			 */
    290 			syslog(LOG_DEBUG | LOG_DAEMON, "dhcpagent: daemonize: "
    291 			    "waiting for adoption to complete.");
    292 			if (sleep(DHCP_ADOPT_SLEEP) == 0) {
    293 				syslog(LOG_WARNING | LOG_DAEMON,
    294 				    "dhcpagent: daemonize: timed out awaiting "
    295 				    "adoption.");
    296 			}
    297 			syslog(LOG_DEBUG | LOG_DAEMON, "dhcpagent: daemonize: "
    298 			    "wait finished");
    299 		}
    300 		_exit(EXIT_SUCCESS);
    301 	}
    302 
    303 	return (1);
    304 }
    305 
    306 /*
    307  * update_default_route(): update the interface's default route
    308  *
    309  *   input: int: the type of message; either RTM_ADD or RTM_DELETE
    310  *	    struct in_addr: the default gateway to use
    311  *	    const char *: the interface associated with the route
    312  *	    int: any additional flags (besides RTF_STATIC and RTF_GATEWAY)
    313  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
    314  */
    315 
    316 static boolean_t
    317 update_default_route(uint32_t ifindex, int type, struct in_addr *gateway_nbo,
    318     int flags)
    319 {
    320 	struct {
    321 		struct rt_msghdr	rm_mh;
    322 		struct sockaddr_in	rm_dst;
    323 		struct sockaddr_in	rm_gw;
    324 		struct sockaddr_in	rm_mask;
    325 		struct sockaddr_dl	rm_ifp;
    326 	} rtmsg;
    327 
    328 	(void) memset(&rtmsg, 0, sizeof (rtmsg));
    329 	rtmsg.rm_mh.rtm_version = RTM_VERSION;
    330 	rtmsg.rm_mh.rtm_msglen	= sizeof (rtmsg);
    331 	rtmsg.rm_mh.rtm_type	= type;
    332 	rtmsg.rm_mh.rtm_pid	= getpid();
    333 	rtmsg.rm_mh.rtm_flags	= RTF_GATEWAY | RTF_STATIC | flags;
    334 	rtmsg.rm_mh.rtm_addrs	= RTA_GATEWAY | RTA_DST | RTA_NETMASK | RTA_IFP;
    335 
    336 	rtmsg.rm_gw.sin_family	= AF_INET;
    337 	rtmsg.rm_gw.sin_addr	= *gateway_nbo;
    338 
    339 	rtmsg.rm_dst.sin_family = AF_INET;
    340 	rtmsg.rm_dst.sin_addr.s_addr = htonl(INADDR_ANY);
    341 
    342 	rtmsg.rm_mask.sin_family = AF_INET;
    343 	rtmsg.rm_mask.sin_addr.s_addr = htonl(0);
    344 
    345 	rtmsg.rm_ifp.sdl_family	= AF_LINK;
    346 	rtmsg.rm_ifp.sdl_index	= ifindex;
    347 
    348 	return (write(rtsock_fd, &rtmsg, sizeof (rtmsg)) == sizeof (rtmsg));
    349 }
    350 
    351 /*
    352  * add_default_route(): add the default route to the given gateway
    353  *
    354  *   input: const char *: the name of the interface associated with the route
    355  *	    struct in_addr: the default gateway to add
    356  *  output: boolean_t: B_TRUE on success, B_FALSE otherwise
    357  */
    358 
    359 boolean_t
    360 add_default_route(uint32_t ifindex, struct in_addr *gateway_nbo)
    361 {
    362 	return (update_default_route(ifindex, RTM_ADD, gateway_nbo, RTF_UP));
    363 }
    364 
    365 /*
    366  * del_default_route(): deletes the default route to the given gateway
    367  *
    368  *   input: const char *: the name of the interface associated with the route
    369  *	    struct in_addr: if not INADDR_ANY, the default gateway to remove
    370  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
    371  */
    372 
    373 boolean_t
    374 del_default_route(uint32_t ifindex, struct in_addr *gateway_nbo)
    375 {
    376 	if (gateway_nbo->s_addr == htonl(INADDR_ANY)) /* no router */
    377 		return (B_TRUE);
    378 
    379 	return (update_default_route(ifindex, RTM_DELETE, gateway_nbo, 0));
    380 }
    381 
    382 /*
    383  * inactivity_shutdown(): shuts down agent if there are no state machines left
    384  *			  to manage
    385  *
    386  *   input: iu_tq_t *: unused
    387  *	    void *: unused
    388  *  output: void
    389  */
    390 
    391 /* ARGSUSED */
    392 void
    393 inactivity_shutdown(iu_tq_t *tqp, void *arg)
    394 {
    395 	if (smach_count() > 0)	/* shouldn't happen, but... */
    396 		return;
    397 
    398 	dhcpmsg(MSG_VERBOSE, "inactivity_shutdown: timed out");
    399 
    400 	iu_stop_handling_events(eh, DHCP_REASON_INACTIVITY, NULL, NULL);
    401 }
    402 
    403 /*
    404  * graceful_shutdown(): shuts down the agent gracefully
    405  *
    406  *   input: int: the signal that caused graceful_shutdown to be called
    407  *  output: void
    408  */
    409 
    410 void
    411 graceful_shutdown(int sig)
    412 {
    413 	iu_stop_handling_events(eh, (sig == SIGTERM ? DHCP_REASON_TERMINATE :
    414 	    DHCP_REASON_SIGNAL), drain_script, NULL);
    415 }
    416 
    417 /*
    418  * bind_sock(): binds a socket to a given IP address and port number
    419  *
    420  *   input: int: the socket to bind
    421  *	    in_port_t: the port number to bind to, host byte order
    422  *	    in_addr_t: the address to bind to, host byte order
    423  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
    424  */
    425 
    426 boolean_t
    427 bind_sock(int fd, in_port_t port_hbo, in_addr_t addr_hbo)
    428 {
    429 	struct sockaddr_in	sin;
    430 	int			on = 1;
    431 
    432 	(void) memset(&sin, 0, sizeof (struct sockaddr_in));
    433 	sin.sin_family = AF_INET;
    434 	sin.sin_port   = htons(port_hbo);
    435 	sin.sin_addr.s_addr = htonl(addr_hbo);
    436 
    437 	(void) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (int));
    438 
    439 	return (bind(fd, (struct sockaddr *)&sin, sizeof (sin)) == 0);
    440 }
    441 
    442 /*
    443  * bind_sock_v6(): binds a socket to a given IP address and port number
    444  *
    445  *   input: int: the socket to bind
    446  *	    in_port_t: the port number to bind to, host byte order
    447  *	    in6_addr_t: the address to bind to, network byte order
    448  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
    449  */
    450 
    451 boolean_t
    452 bind_sock_v6(int fd, in_port_t port_hbo, const in6_addr_t *addr_nbo)
    453 {
    454 	struct sockaddr_in6	sin6;
    455 	int			on = 1;
    456 
    457 	(void) memset(&sin6, 0, sizeof (struct sockaddr_in6));
    458 	sin6.sin6_family = AF_INET6;
    459 	sin6.sin6_port   = htons(port_hbo);
    460 	if (addr_nbo != NULL) {
    461 		(void) memcpy(&sin6.sin6_addr, addr_nbo,
    462 		    sizeof (sin6.sin6_addr));
    463 	}
    464 
    465 	(void) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (int));
    466 
    467 	return (bind(fd, (struct sockaddr *)&sin6, sizeof (sin6)) == 0);
    468 }
    469 
    470 /*
    471  * valid_hostname(): check whether a string is a valid hostname
    472  *
    473  *   input: const char *: the string to verify as a hostname
    474  *  output: boolean_t: B_TRUE if the string is a valid hostname
    475  *
    476  * Note that we accept both host names beginning with a digit and
    477  * those containing hyphens.  Neither is strictly legal according
    478  * to the RFCs, but both are in common practice, so we endeavour
    479  * to not break what customers are using.
    480  */
    481 
    482 static boolean_t
    483 valid_hostname(const char *hostname)
    484 {
    485 	unsigned int i;
    486 
    487 	for (i = 0; hostname[i] != '\0'; i++) {
    488 
    489 		if (isalpha(hostname[i]) || isdigit(hostname[i]) ||
    490 		    (((hostname[i] == '-') || (hostname[i] == '.')) && (i > 0)))
    491 			continue;
    492 
    493 		return (B_FALSE);
    494 	}
    495 
    496 	return (i > 0);
    497 }
    498 
    499 /*
    500  * iffile_to_hostname(): return the hostname contained on a line of the form
    501  *
    502  * [ ^I]*inet[ ^I]+hostname[\n]*\0
    503  *
    504  * in the file located at the specified path
    505  *
    506  *   input: const char *: the path of the file to look in for the hostname
    507  *  output: const char *: the hostname at that path, or NULL on failure
    508  */
    509 
    510 #define	IFLINE_MAX	1024	/* maximum length of a hostname.<if> line */
    511 
    512 const char *
    513 iffile_to_hostname(const char *path)
    514 {
    515 	FILE		*fp;
    516 	static char	ifline[IFLINE_MAX];
    517 
    518 	fp = fopen(path, "r");
    519 	if (fp == NULL)
    520 		return (NULL);
    521 
    522 	/*
    523 	 * /etc/hostname.<if> may contain multiple ifconfig commands, but each
    524 	 * such command is on a separate line (see the "while read ifcmds" code
    525 	 * in /etc/init.d/inetinit).  Thus we will read the file a line at a
    526 	 * time, searching for a line of the form
    527 	 *
    528 	 * [ ^I]*inet[ ^I]+hostname[\n]*\0
    529 	 *
    530 	 * extract the host name from it, and check it for validity.
    531 	 */
    532 	while (fgets(ifline, sizeof (ifline), fp) != NULL) {
    533 		char *p;
    534 
    535 		if ((p = strstr(ifline, "inet")) != NULL) {
    536 			if ((p != ifline) && !isspace(p[-1])) {
    537 				(void) fclose(fp);
    538 				return (NULL);
    539 			}
    540 			p += 4;	/* skip over "inet" and expect spaces or tabs */
    541 			if ((*p == '\n') || (*p == '\0')) {
    542 				(void) fclose(fp);
    543 				return (NULL);
    544 			}
    545 			if (isspace(*p)) {
    546 				char *nlptr;
    547 
    548 				/* no need to read more of the file */
    549 				(void) fclose(fp);
    550 
    551 				while (isspace(*p))
    552 					p++;
    553 				if ((nlptr = strrchr(p, '\n')) != NULL)
    554 					*nlptr = '\0';
    555 				if (strlen(p) > MAXHOSTNAMELEN) {
    556 					dhcpmsg(MSG_WARNING,
    557 					    "iffile_to_hostname:"
    558 					    " host name too long");
    559 					return (NULL);
    560 				}
    561 				if (valid_hostname(p)) {
    562 					return (p);
    563 				} else {
    564 					dhcpmsg(MSG_WARNING,
    565 					    "iffile_to_hostname:"
    566 					    " host name not valid");
    567 					return (NULL);
    568 				}
    569 			} else {
    570 				(void) fclose(fp);
    571 				return (NULL);
    572 			}
    573 		}
    574 	}
    575 
    576 	(void) fclose(fp);
    577 	return (NULL);
    578 }
    579 
    580 /*
    581  * init_timer(): set up a DHCP timer
    582  *
    583  *   input: dhcp_timer_t *: the timer to set up
    584  *  output: void
    585  */
    586 
    587 void
    588 init_timer(dhcp_timer_t *dt, lease_t startval)
    589 {
    590 	dt->dt_id = -1;
    591 	dt->dt_start = startval;
    592 }
    593 
    594 /*
    595  * cancel_timer(): cancel a DHCP timer
    596  *
    597  *   input: dhcp_timer_t *: the timer to cancel
    598  *  output: boolean_t: B_TRUE on success, B_FALSE otherwise
    599  */
    600 
    601 boolean_t
    602 cancel_timer(dhcp_timer_t *dt)
    603 {
    604 	if (dt->dt_id == -1)
    605 		return (B_TRUE);
    606 
    607 	if (iu_cancel_timer(tq, dt->dt_id, NULL) == 1) {
    608 		dt->dt_id = -1;
    609 		return (B_TRUE);
    610 	}
    611 
    612 	return (B_FALSE);
    613 }
    614 
    615 /*
    616  * schedule_timer(): schedule a DHCP timer.  Note that it must not be already
    617  *		     running, and that we can't cancel here.  If it were, and
    618  *		     we did, we'd leak a reference to the callback argument.
    619  *
    620  *   input: dhcp_timer_t *: the timer to schedule
    621  *  output: boolean_t: B_TRUE on success, B_FALSE otherwise
    622  */
    623 
    624 boolean_t
    625 schedule_timer(dhcp_timer_t *dt, iu_tq_callback_t *cbfunc, void *arg)
    626 {
    627 	if (dt->dt_id != -1)
    628 		return (B_FALSE);
    629 	dt->dt_id = iu_schedule_timer(tq, dt->dt_start, cbfunc, arg);
    630 	return (dt->dt_id != -1);
    631 }
    632 
    633 /*
    634  * dhcpv6_status_code(): report on a DHCPv6 status code found in an option
    635  *			 buffer.
    636  *
    637  *   input: const dhcpv6_option_t *: pointer to option
    638  *	    uint_t: option length
    639  *	    const char **: error string (nul-terminated)
    640  *	    const char **: message from server (unterminated)
    641  *	    uint_t *: length of server message
    642  *  output: int: -1 on error, or >= 0 for a DHCPv6 status code
    643  */
    644 
    645 int
    646 dhcpv6_status_code(const dhcpv6_option_t *d6o, uint_t olen, const char **estr,
    647     const char **msg, uint_t *msglenp)
    648 {
    649 	uint16_t status;
    650 	static const char *v6_status[] = {
    651 		NULL,
    652 		"Unknown reason",
    653 		"Server has no addresses available",
    654 		"Client record unavailable",
    655 		"Prefix inappropriate for link",
    656 		"Client must use multicast",
    657 		"No prefix available"
    658 	};
    659 	static char sbuf[32];
    660 
    661 	*estr = "";
    662 	*msg = "";
    663 	*msglenp = 0;
    664 	if (d6o == NULL)
    665 		return (0);
    666 	olen -= sizeof (*d6o);
    667 	if (olen < 2) {
    668 		*estr = "garbled status code";
    669 		return (-1);
    670 	}
    671 
    672 	*msg = (const char *)(d6o + 1) + 2;
    673 	*msglenp = olen - 2;
    674 
    675 	(void) memcpy(&status, d6o + 1, sizeof (status));
    676 	status = ntohs(status);
    677 	if (status > 0) {
    678 		if (status > DHCPV6_STAT_NOPREFIX) {
    679 			(void) snprintf(sbuf, sizeof (sbuf), "status %u",
    680 			    status);
    681 			*estr = sbuf;
    682 		} else {
    683 			*estr = v6_status[status];
    684 		}
    685 	}
    686 	return (status);
    687 }
    688