xref: /titanic_50/usr/src/uts/common/io/sad.c (revision c2cb63342c63c60cee771d1af82f377e34aa1217)
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 /*	Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T	*/
29 /*	  All Rights Reserved  	*/
30 
31 
32 /*
33  * STREAMS Administrative Driver
34  *
35  * Currently only handles autopush and module name verification.
36  */
37 
38 #include <sys/types.h>
39 #include <sys/param.h>
40 #include <sys/errno.h>
41 #include <sys/stream.h>
42 #include <sys/stropts.h>
43 #include <sys/strsubr.h>
44 #include <sys/strsun.h>
45 #include <sys/conf.h>
46 #include <sys/sad.h>
47 #include <sys/cred.h>
48 #include <sys/debug.h>
49 #include <sys/ddi.h>
50 #include <sys/sunddi.h>
51 #include <sys/stat.h>
52 #include <sys/cmn_err.h>
53 #include <sys/systm.h>
54 #include <sys/modctl.h>
55 #include <sys/priv_names.h>
56 #include <sys/sysmacros.h>
57 #include <sys/zone.h>
58 
59 static int sadopen(queue_t *, dev_t *, int, int, cred_t *);
60 static int sadclose(queue_t *, int, cred_t *);
61 static int sadwput(queue_t *qp, mblk_t *mp);
62 
63 static int sad_info(dev_info_t *, ddi_info_cmd_t, void *, void **);
64 static int sad_attach(dev_info_t *, ddi_attach_cmd_t);
65 
66 static void apush_ioctl(), apush_iocdata();
67 static void vml_ioctl(), vml_iocdata();
68 static int valid_major(major_t);
69 
70 static dev_info_t *sad_dip;		/* private copy of devinfo pointer */
71 
72 static struct module_info sad_minfo = {
73 	0x7361, "sad", 0, INFPSZ, 0, 0
74 };
75 
76 static struct qinit sad_rinit = {
77 	NULL, NULL, sadopen, sadclose, NULL, &sad_minfo, NULL
78 };
79 
80 static struct qinit sad_winit = {
81 	sadwput, NULL, NULL, NULL, NULL, &sad_minfo, NULL
82 };
83 
84 struct streamtab sadinfo = {
85 	&sad_rinit, &sad_winit, NULL, NULL
86 };
87 
88 DDI_DEFINE_STREAM_OPS(sad_ops, nulldev, nulldev, sad_attach,
89     nodev, nodev, sad_info, D_MTPERQ | D_MP, &sadinfo);
90 
91 /*
92  * Module linkage information for the kernel.
93  */
94 
95 static struct modldrv modldrv = {
96 	&mod_driverops, /* Type of module.  This one is a pseudo driver */
97 	"STREAMS Administrative Driver 'sad' %I%",
98 	&sad_ops,	/* driver ops */
99 };
100 
101 static struct modlinkage modlinkage = {
102 	MODREV_1, &modldrv, NULL
103 };
104 
105 int
106 _init(void)
107 {
108 	return (mod_install(&modlinkage));
109 }
110 
111 int
112 _fini(void)
113 {
114 	return (mod_remove(&modlinkage));
115 }
116 
117 int
118 _info(struct modinfo *modinfop)
119 {
120 	return (mod_info(&modlinkage, modinfop));
121 }
122 
123 static int
124 sad_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
125 {
126 	int instance = ddi_get_instance(devi);
127 
128 	if (cmd != DDI_ATTACH)
129 		return (DDI_FAILURE);
130 
131 	ASSERT(instance == 0);
132 	if (instance != 0)
133 		return (DDI_FAILURE);
134 
135 	if (ddi_create_minor_node(devi, "user", S_IFCHR,
136 	    0, DDI_PSEUDO, NULL) == DDI_FAILURE) {
137 		return (DDI_FAILURE);
138 	}
139 	if (ddi_create_priv_minor_node(devi, "admin", S_IFCHR,
140 	    1, DDI_PSEUDO, PRIVONLY_DEV, PRIV_SYS_CONFIG,
141 	    PRIV_SYS_CONFIG, 0666) == DDI_FAILURE) {
142 		ddi_remove_minor_node(devi, NULL);
143 		return (DDI_FAILURE);
144 	}
145 	sad_dip = devi;
146 	return (DDI_SUCCESS);
147 }
148 
149 /* ARGSUSED */
150 static int
151 sad_info(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
152 {
153 	int error;
154 
155 	switch (infocmd) {
156 	case DDI_INFO_DEVT2DEVINFO:
157 		if (sad_dip == NULL) {
158 			error = DDI_FAILURE;
159 		} else {
160 			*result = sad_dip;
161 			error = DDI_SUCCESS;
162 		}
163 		break;
164 	case DDI_INFO_DEVT2INSTANCE:
165 		*result = (void *)0;
166 		error = DDI_SUCCESS;
167 		break;
168 	default:
169 		error = DDI_FAILURE;
170 	}
171 	return (error);
172 }
173 
174 
175 /*
176  * sadopen() -
177  * Allocate a sad device.  Only one
178  * open at a time allowed per device.
179  */
180 /* ARGSUSED */
181 static int
182 sadopen(
183 	queue_t *qp,	/* pointer to read queue */
184 	dev_t *devp,	/* major/minor device of stream */
185 	int flag,	/* file open flags */
186 	int sflag,	/* stream open flags */
187 	cred_t *credp)	/* user credentials */
188 {
189 	int i;
190 	netstack_t *ns;
191 	str_stack_t *ss;
192 
193 	if (sflag)		/* no longer called from clone driver */
194 		return (EINVAL);
195 
196 	ns = netstack_find_by_cred(credp);
197 	ASSERT(ns != NULL);
198 	ss = ns->netstack_str;
199 	ASSERT(ss != NULL);
200 
201 	/*
202 	 * Both USRMIN and ADMMIN are clone interfaces.
203 	 */
204 	for (i = 0; i < ss->ss_sadcnt; i++)
205 		if (ss->ss_saddev[i].sa_qp == NULL)
206 			break;
207 	if (i >= ss->ss_sadcnt) {		/* no such device */
208 		netstack_rele(ss->ss_netstack);
209 		return (ENXIO);
210 	}
211 	switch (getminor(*devp)) {
212 	case USRMIN:			/* mere mortal */
213 		ss->ss_saddev[i].sa_flags = 0;
214 		break;
215 
216 	case ADMMIN:			/* privileged user */
217 		ss->ss_saddev[i].sa_flags = SADPRIV;
218 		break;
219 
220 	default:
221 		netstack_rele(ss->ss_netstack);
222 		return (EINVAL);
223 	}
224 
225 	ss->ss_saddev[i].sa_qp = qp;
226 	ss->ss_saddev[i].sa_ss = ss;
227 	qp->q_ptr = (caddr_t)&ss->ss_saddev[i];
228 	WR(qp)->q_ptr = (caddr_t)&ss->ss_saddev[i];
229 
230 	/*
231 	 * NOTE: should the ADMMIN or USRMIN minors change
232 	 * then so should the offset of 2 below
233 	 * Both USRMIN and ADMMIN are clone interfaces and
234 	 * therefore their minor numbers (0 and 1) are reserved.
235 	 */
236 	*devp = makedevice(getemajor(*devp), i + 2);
237 	qprocson(qp);
238 	return (0);
239 }
240 
241 /*
242  * sadclose() -
243  * Clean up the data structures.
244  */
245 /* ARGSUSED */
246 static int
247 sadclose(
248 	queue_t *qp,	/* pointer to read queue */
249 	int flag,	/* file open flags */
250 	cred_t *credp)	/* user credentials */
251 {
252 	struct saddev *sadp;
253 
254 	qprocsoff(qp);
255 	sadp = (struct saddev *)qp->q_ptr;
256 	sadp->sa_qp = NULL;
257 	sadp->sa_addr = NULL;
258 	netstack_rele(sadp->sa_ss->ss_netstack);
259 	sadp->sa_ss = NULL;
260 	qp->q_ptr = NULL;
261 	WR(qp)->q_ptr = NULL;
262 	return (0);
263 }
264 
265 /*
266  * sadwput() -
267  * Write side put procedure.
268  */
269 static int
270 sadwput(
271 	queue_t *qp,	/* pointer to write queue */
272 	mblk_t *mp)	/* message pointer */
273 {
274 	struct iocblk *iocp;
275 
276 	switch (mp->b_datap->db_type) {
277 	case M_FLUSH:
278 		if (*mp->b_rptr & FLUSHR) {
279 			*mp->b_rptr &= ~FLUSHW;
280 			qreply(qp, mp);
281 		} else
282 			freemsg(mp);
283 		break;
284 
285 	case M_IOCTL:
286 		iocp = (struct iocblk *)mp->b_rptr;
287 		switch (SAD_CMD(iocp->ioc_cmd)) {
288 		case SAD_CMD(SAD_SAP):
289 		case SAD_CMD(SAD_GAP):
290 			apush_ioctl(qp, mp);
291 			break;
292 
293 		case SAD_VML:
294 			vml_ioctl(qp, mp);
295 			break;
296 
297 		default:
298 			miocnak(qp, mp, 0, EINVAL);
299 			break;
300 		}
301 		break;
302 
303 	case M_IOCDATA:
304 		iocp = (struct iocblk *)mp->b_rptr;
305 		switch (SAD_CMD(iocp->ioc_cmd)) {
306 		case SAD_CMD(SAD_SAP):
307 		case SAD_CMD(SAD_GAP):
308 			apush_iocdata(qp, mp);
309 			break;
310 
311 		case SAD_VML:
312 			vml_iocdata(qp, mp);
313 			break;
314 
315 		default:
316 			cmn_err(CE_WARN,
317 			    "sadwput: invalid ioc_cmd in case M_IOCDATA: %d",
318 			    iocp->ioc_cmd);
319 			freemsg(mp);
320 			break;
321 		}
322 		break;
323 
324 	default:
325 		freemsg(mp);
326 		break;
327 	} /* switch (db_type) */
328 	return (0);
329 }
330 
331 /*
332  * apush_ioctl() -
333  * Handle the M_IOCTL messages associated with
334  * the autopush feature.
335  */
336 static void
337 apush_ioctl(
338 	queue_t *qp,	/* pointer to write queue */
339 	mblk_t *mp)	/* message pointer */
340 {
341 	struct iocblk	*iocp;
342 	struct saddev	*sadp;
343 	uint_t		size;
344 
345 	iocp = (struct iocblk *)mp->b_rptr;
346 	if (iocp->ioc_count != TRANSPARENT) {
347 		miocnak(qp, mp, 0, EINVAL);
348 		return;
349 	}
350 	if (SAD_VER(iocp->ioc_cmd) > AP_VERSION) {
351 		miocnak(qp, mp, 0, EINVAL);
352 		return;
353 	}
354 
355 	sadp = (struct saddev *)qp->q_ptr;
356 	switch (SAD_CMD(iocp->ioc_cmd)) {
357 	case SAD_CMD(SAD_SAP):
358 		if (!(sadp->sa_flags & SADPRIV)) {
359 			miocnak(qp, mp, 0, EPERM);
360 			break;
361 		}
362 		/* FALLTHRU */
363 
364 	case SAD_CMD(SAD_GAP):
365 		sadp->sa_addr = (caddr_t)*(uintptr_t *)mp->b_cont->b_rptr;
366 		if (SAD_VER(iocp->ioc_cmd) == 1)
367 			size = STRAPUSH_V1_LEN;
368 		else
369 			size = STRAPUSH_V0_LEN;
370 		mcopyin(mp, (void *)GETSTRUCT, size, NULL);
371 		qreply(qp, mp);
372 		break;
373 
374 	default:
375 		ASSERT(0);
376 		miocnak(qp, mp, 0, EINVAL);
377 		break;
378 	} /* switch (ioc_cmd) */
379 }
380 
381 /*
382  * apush_iocdata() -
383  * Handle the M_IOCDATA messages associated with
384  * the autopush feature.
385  */
386 static void
387 apush_iocdata(
388 	queue_t *qp,	/* pointer to write queue */
389 	mblk_t *mp)	/* message pointer */
390 {
391 	int i, ret;
392 	struct copyresp *csp;
393 	struct strapush *sap = NULL;
394 	struct autopush *ap, *ap_tmp;
395 	struct saddev *sadp;
396 	uint_t size;
397 	dev_t dev;
398 	str_stack_t *ss;
399 
400 	sadp = (struct saddev *)qp->q_ptr;
401 	ss = sadp->sa_ss;
402 
403 	csp = (struct copyresp *)mp->b_rptr;
404 	if (csp->cp_rval) {	/* if there was an error */
405 		freemsg(mp);
406 		return;
407 	}
408 	if (mp->b_cont) {
409 		/*
410 		 * sap needed only if mp->b_cont is set.  figure out the
411 		 * size of the expected sap structure and make sure
412 		 * enough data was supplied.
413 		 */
414 		if (SAD_VER(csp->cp_cmd) == 1)
415 			size = STRAPUSH_V1_LEN;
416 		else
417 			size = STRAPUSH_V0_LEN;
418 		if (MBLKL(mp->b_cont) < size) {
419 			miocnak(qp, mp, 0, EINVAL);
420 			return;
421 		}
422 		sap = (struct strapush *)mp->b_cont->b_rptr;
423 		dev = makedevice(sap->sap_major, sap->sap_minor);
424 	}
425 	switch (SAD_CMD(csp->cp_cmd)) {
426 	case SAD_CMD(SAD_SAP):
427 
428 		/* currently we only support one SAD_SAP command */
429 		if (((long)csp->cp_private) != GETSTRUCT) {
430 			cmn_err(CE_WARN,
431 			    "apush_iocdata: cp_private bad in SAD_SAP: %p",
432 			    (void *)csp->cp_private);
433 			miocnak(qp, mp, 0, EINVAL);
434 			return;
435 		}
436 
437 		switch (sap->sap_cmd) {
438 		default:
439 			miocnak(qp, mp, 0, EINVAL);
440 			return;
441 		case SAP_ONE:
442 		case SAP_RANGE:
443 		case SAP_ALL:
444 			/* allocate and initialize a new config */
445 			ap = sad_ap_alloc();
446 			ap->ap_common = sap->sap_common;
447 			if (SAD_VER(csp->cp_cmd) > 0)
448 				ap->ap_anchor = sap->sap_anchor;
449 			for (i = 0; i < MIN(sap->sap_npush, MAXAPUSH); i++)
450 				(void) strncpy(ap->ap_list[i],
451 				    sap->sap_list[i], FMNAMESZ);
452 
453 			/* sanity check the request */
454 			if (((ret = sad_ap_verify(ap)) != 0) ||
455 			    ((ret = valid_major(ap->ap_major)) != 0)) {
456 				sad_ap_rele(ap, ss);
457 				miocnak(qp, mp, 0, ret);
458 				return;
459 			}
460 
461 			/* check for overlapping configs */
462 			mutex_enter(&ss->ss_sad_lock);
463 			ap_tmp = sad_ap_find(&ap->ap_common, ss);
464 			if (ap_tmp != NULL) {
465 				/* already configured */
466 				mutex_exit(&ss->ss_sad_lock);
467 				sad_ap_rele(ap_tmp, ss);
468 				sad_ap_rele(ap, ss);
469 				miocnak(qp, mp, 0, EEXIST);
470 				return;
471 			}
472 
473 			/* add the new config to our hash */
474 			sad_ap_insert(ap, ss);
475 			mutex_exit(&ss->ss_sad_lock);
476 			miocack(qp, mp, 0, 0);
477 			return;
478 
479 		case SAP_CLEAR:
480 			/* sanity check the request */
481 			if (ret = valid_major(sap->sap_major)) {
482 				miocnak(qp, mp, 0, ret);
483 				return;
484 			}
485 
486 			/* search for a matching config */
487 			if ((ap = sad_ap_find_by_dev(dev, ss)) == NULL) {
488 				/* no config found */
489 				miocnak(qp, mp, 0, ENODEV);
490 				return;
491 			}
492 
493 			/*
494 			 * If we matched a SAP_RANGE config
495 			 * the minor passed in must match the
496 			 * beginning of the range exactly.
497 			 */
498 			if ((ap->ap_type == SAP_RANGE) &&
499 			    (ap->ap_minor != sap->sap_minor)) {
500 				sad_ap_rele(ap, ss);
501 				miocnak(qp, mp, 0, ERANGE);
502 				return;
503 			}
504 
505 			/*
506 			 * If we matched a SAP_ALL config
507 			 * the minor passed in must be 0.
508 			 */
509 			if ((ap->ap_type == SAP_ALL) &&
510 			    (sap->sap_minor != 0)) {
511 				sad_ap_rele(ap, ss);
512 				miocnak(qp, mp, 0, EINVAL);
513 				return;
514 			}
515 
516 			/*
517 			 * make sure someone else hasn't already
518 			 * removed this config from the hash.
519 			 */
520 			mutex_enter(&ss->ss_sad_lock);
521 			ap_tmp = sad_ap_find(&ap->ap_common, ss);
522 			if (ap_tmp != ap) {
523 				mutex_exit(&ss->ss_sad_lock);
524 				sad_ap_rele(ap_tmp, ss);
525 				sad_ap_rele(ap, ss);
526 				miocnak(qp, mp, 0, ENODEV);
527 				return;
528 			}
529 
530 			/* remove the config from the hash and return */
531 			sad_ap_remove(ap, ss);
532 			mutex_exit(&ss->ss_sad_lock);
533 
534 			/*
535 			 * Release thrice, once for sad_ap_find_by_dev(),
536 			 * once for sad_ap_find(), and once to free.
537 			 */
538 			sad_ap_rele(ap, ss);
539 			sad_ap_rele(ap, ss);
540 			sad_ap_rele(ap, ss);
541 			miocack(qp, mp, 0, 0);
542 			return;
543 		} /* switch (sap_cmd) */
544 		/*NOTREACHED*/
545 
546 	case SAD_CMD(SAD_GAP):
547 		switch ((long)csp->cp_private) {
548 
549 		case GETSTRUCT:
550 			/* sanity check the request */
551 			if (ret = valid_major(sap->sap_major)) {
552 				miocnak(qp, mp, 0, ret);
553 				return;
554 			}
555 
556 			/* search for a matching config */
557 			if ((ap = sad_ap_find_by_dev(dev, ss)) == NULL) {
558 				/* no config found */
559 				miocnak(qp, mp, 0, ENODEV);
560 				return;
561 			}
562 
563 			/* copy out the contents of the config */
564 			sap->sap_common = ap->ap_common;
565 			if (SAD_VER(csp->cp_cmd) > 0)
566 				sap->sap_anchor = ap->ap_anchor;
567 			for (i = 0; i < ap->ap_npush; i++)
568 				(void) strcpy(sap->sap_list[i], ap->ap_list[i]);
569 			for (; i < MAXAPUSH; i++)
570 				bzero(sap->sap_list[i], FMNAMESZ + 1);
571 
572 			/* release our hold on the config */
573 			sad_ap_rele(ap, ss);
574 
575 			/* copyout the results */
576 			if (SAD_VER(csp->cp_cmd) == 1)
577 				size = STRAPUSH_V1_LEN;
578 			else
579 				size = STRAPUSH_V0_LEN;
580 
581 			mcopyout(mp, (void *)GETRESULT, size, sadp->sa_addr,
582 			    NULL);
583 			qreply(qp, mp);
584 			return;
585 		case GETRESULT:
586 			miocack(qp, mp, 0, 0);
587 			return;
588 
589 		default:
590 			cmn_err(CE_WARN,
591 			    "apush_iocdata: cp_private bad case SAD_GAP: %p",
592 			    (void *)csp->cp_private);
593 			freemsg(mp);
594 			return;
595 		} /* switch (cp_private) */
596 		/*NOTREACHED*/
597 	default:	/* can't happen */
598 		ASSERT(0);
599 		freemsg(mp);
600 		return;
601 	} /* switch (cp_cmd) */
602 }
603 
604 /*
605  * vml_ioctl() -
606  * Handle the M_IOCTL message associated with a request
607  * to validate a module list.
608  */
609 static void
610 vml_ioctl(
611 	queue_t *qp,	/* pointer to write queue */
612 	mblk_t *mp)	/* message pointer */
613 {
614 	struct iocblk *iocp;
615 
616 	iocp = (struct iocblk *)mp->b_rptr;
617 	if (iocp->ioc_count != TRANSPARENT) {
618 		miocnak(qp, mp, 0, EINVAL);
619 		return;
620 	}
621 	ASSERT(SAD_CMD(iocp->ioc_cmd) == SAD_VML);
622 	mcopyin(mp, (void *)GETSTRUCT,
623 	    SIZEOF_STRUCT(str_list, iocp->ioc_flag), NULL);
624 	qreply(qp, mp);
625 }
626 
627 /*
628  * vml_iocdata() -
629  * Handle the M_IOCDATA messages associated with
630  * a request to validate a module list.
631  */
632 static void
633 vml_iocdata(
634 	queue_t *qp,	/* pointer to write queue */
635 	mblk_t *mp)	/* message pointer */
636 {
637 	long i;
638 	int	nmods;
639 	struct copyresp *csp;
640 	struct str_mlist *lp;
641 	STRUCT_HANDLE(str_list, slp);
642 	struct saddev *sadp;
643 
644 	csp = (struct copyresp *)mp->b_rptr;
645 	if (csp->cp_rval) {	/* if there was an error */
646 		freemsg(mp);
647 		return;
648 	}
649 
650 	ASSERT(SAD_CMD(csp->cp_cmd) == SAD_VML);
651 	sadp = (struct saddev *)qp->q_ptr;
652 	switch ((long)csp->cp_private) {
653 	case GETSTRUCT:
654 		STRUCT_SET_HANDLE(slp, csp->cp_flag,
655 		    (struct str_list *)mp->b_cont->b_rptr);
656 		nmods = STRUCT_FGET(slp, sl_nmods);
657 		if (nmods <= 0) {
658 			miocnak(qp, mp, 0, EINVAL);
659 			break;
660 		}
661 		sadp->sa_addr = (caddr_t)(uintptr_t)nmods;
662 
663 		mcopyin(mp, (void *)GETLIST, nmods * sizeof (struct str_mlist),
664 		    STRUCT_FGETP(slp, sl_modlist));
665 		qreply(qp, mp);
666 		break;
667 
668 	case GETLIST:
669 		lp = (struct str_mlist *)mp->b_cont->b_rptr;
670 		for (i = 0; i < (long)sadp->sa_addr; i++, lp++) {
671 			lp->l_name[FMNAMESZ] = '\0';
672 			if (fmodsw_find(lp->l_name, FMODSW_LOAD) == NULL) {
673 				miocack(qp, mp, 0, 1);
674 				return;
675 			}
676 		}
677 		miocack(qp, mp, 0, 0);
678 		break;
679 
680 	default:
681 		cmn_err(CE_WARN, "vml_iocdata: invalid cp_private value: %p",
682 		    (void *)csp->cp_private);
683 		freemsg(mp);
684 		break;
685 	} /* switch (cp_private) */
686 }
687 
688 /*
689  * Validate a major number and also verify if
690  * it is a STREAMS device.
691  * Return values: 0 if a valid STREAMS dev
692  *		  error code otherwise
693  */
694 static int
695 valid_major(major_t major)
696 {
697 	int ret = 0;
698 
699 	if (etoimajor(major) == -1)
700 		return (EINVAL);
701 
702 	/*
703 	 * attempt to load the driver 'major' and verify that
704 	 * it is a STREAMS driver.
705 	 */
706 	if (ddi_hold_driver(major) == NULL)
707 		return (EINVAL);
708 
709 	if (!STREAMSTAB(major))
710 		ret = ENOSTR;
711 
712 	ddi_rele_driver(major);
713 
714 	return (ret);
715 }
716