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, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 * or http://www.opensolaris.org/os/licensing.
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 * If applicable, add the following below this CDDL HEADER, with the
17 * fields enclosed by brackets "[]" replaced with your own identifying
18 * information: Portions Copyright [yyyy] [name of copyright owner]
19 *
20 * CDDL HEADER END
21 */
22 /*
23 * Copyright 2005 Sun Microsystems, Inc. All rights reserved.
24 * Use is subject to license terms.
25 */
26
27 #pragma ident "%Z%%M% %I% %E% SMI"
28
29 /*
30 * This file contains public functions for managing legacy DHCP network
31 * containers. For the semantics of these functions, please see the
32 * Enterprise DHCP Architecture Document.
33 */
34
35 #include <alloca.h>
36 #include <arpa/inet.h>
37 #include <ctype.h>
38 #include <dhcp_svc_public.h>
39 #include <dirent.h>
40 #include <errno.h>
41 #include <fcntl.h>
42 #include <libgen.h>
43 #include <libinetutil.h>
44 #include <netinet/in.h>
45 #include <stdlib.h>
46 #include <string.h>
47 #include <sys/socket.h>
48 #include <sys/stat.h>
49 #include <sys/types.h>
50 #include <unistd.h>
51
52 #include "dhcp_network.h"
53 #include "util.h"
54
55 static void net2path(char *, size_t, const char *, ipaddr_t, const char *);
56 static boolean_t record_match(char *[], dn_rec_t *, const dn_rec_t *, uint_t);
57 static int write_rec(int, dn_rec_t *, off_t);
58
59 /* ARGSUSED */
60 int
open_dn(void ** handlep,const char * location,uint_t flags,const struct in_addr * netp,const struct in_addr * maskp)61 open_dn(void **handlep, const char *location, uint_t flags,
62 const struct in_addr *netp, const struct in_addr *maskp)
63 {
64 char dnpath[MAXPATHLEN];
65 dn_handle_t *dhp;
66 int retval;
67 int fd;
68
69 dhp = malloc(sizeof (dn_handle_t));
70 if (dhp == NULL)
71 return (DSVC_NO_MEMORY);
72
73 dhp->dh_net = netp->s_addr;
74 dhp->dh_oflags = flags;
75 (void) strlcpy(dhp->dh_location, location, MAXPATHLEN);
76
77 /*
78 * This is a legacy format which has no header, so we neither write
79 * nor verify a header (we just create the file or make sure it
80 * exists, depending on the value of `flags').
81 */
82 net2path(dnpath, MAXPATHLEN, location, netp->s_addr, "");
83 retval = open_file(dnpath, flags, &fd);
84 if (retval != DSVC_SUCCESS) {
85 free(dhp);
86 return (retval);
87 }
88 (void) close(fd);
89
90 *handlep = dhp;
91 return (DSVC_SUCCESS);
92 }
93
94 int
close_dn(void ** handlep)95 close_dn(void **handlep)
96 {
97 free(*handlep);
98 return (DSVC_SUCCESS);
99 }
100
101 int
remove_dn(const char * dir,const struct in_addr * netp)102 remove_dn(const char *dir, const struct in_addr *netp)
103 {
104 char dnpath[MAXPATHLEN];
105
106 net2path(dnpath, MAXPATHLEN, dir, netp->s_addr, "");
107 if (unlink(dnpath) == -1)
108 return (syserr_to_dsvcerr(errno));
109
110 return (DSVC_SUCCESS);
111 }
112
113 static int
find_dn(FILE * fp,uint_t flags,uint_t query,int count,const dn_rec_t * targetp,dn_rec_list_t ** recordsp,uint_t * nrecordsp)114 find_dn(FILE *fp, uint_t flags, uint_t query, int count,
115 const dn_rec_t *targetp, dn_rec_list_t **recordsp, uint_t *nrecordsp)
116 {
117 int retval = DSVC_SUCCESS;
118 char *commentp, *fields[DNF_MAX_FIELDS];
119 char *buf = NULL;
120 uint_t nrecords;
121 dn_rec_t dn, *recordp;
122 dn_rec_list_t *records, *new_records;
123 unsigned int nfields;
124 off_t recoff;
125
126 if (fseek(fp, 0, SEEK_SET) == -1)
127 return (DSVC_INTERNAL);
128
129 records = NULL;
130 for (nrecords = 0; count < 0 || nrecords < count; ) {
131 free(buf);
132
133 if (flags & FIND_POSITION)
134 recoff = ftello(fp);
135
136 buf = read_entry(fp);
137 if (buf == NULL) {
138 if (!feof(fp))
139 retval = DSVC_NO_MEMORY;
140 break;
141 }
142
143 /*
144 * Skip pure comment lines; for now this just skips the
145 * header information at the top of the container.
146 */
147 if (buf[0] == DNF_COMMENT_CHAR)
148 continue;
149
150 /*
151 * Tell field_split() that there's one less field than
152 * there really is. We do this so that the comment and the
153 * macro field both end up in the DNF_MACRO field, since
154 * both fields are optional and it requires some fancy
155 * footwork (below) to tell which (if any) the record
156 * contains.
157 */
158 nfields = field_split(buf, DNF_MAX_FIELDS - 1, fields, " \t");
159 if (nfields < DNF_REQ_FIELDS)
160 continue;
161
162 if (nfields == DNF_REQ_FIELDS) {
163 fields[DNF_MACRO] = "";
164 fields[DNF_COMMENT] = "";
165 } else {
166 /*
167 * Assume there is a comment; if we hit a comment
168 * delimiter char (DNF_COMMENT_CHAR), then simply
169 * change it to a NUL and advance commentp. If we
170 * hit whitespace, replace the first instance with
171 * NUL, and go searching for DNF_COMMENT_CHAR.
172 * This step is important since it efficiently
173 * handles the common case where a comment is
174 * preceded by a space.
175 */
176 commentp = fields[DNF_MACRO];
177 while (!isspace(*commentp) &&
178 *commentp != DNF_COMMENT_CHAR && *commentp != '\0')
179 commentp++;
180
181 if (isspace(*commentp)) {
182 *commentp++ = '\0';
183 commentp = strchr(commentp, DNF_COMMENT_CHAR);
184 if (commentp == NULL)
185 commentp = "";
186 }
187
188 if (*commentp == DNF_COMMENT_CHAR)
189 *commentp++ = '\0';
190
191 fields[DNF_COMMENT] = commentp;
192 }
193
194 /*
195 * See if we've got a match, filling in dnf.dnf_rec as
196 * we go. If record_match() succeeds, dnf.dnf_rec will
197 * be completely filled in.
198 */
199 if (!record_match(fields, &dn, targetp, query))
200 continue;
201
202 /*
203 * Caller just wants a count of the number of matching
204 * records, not the records themselves; continue.
205 */
206 if (recordsp == NULL) {
207 nrecords++;
208 continue;
209 }
210
211 /*
212 * Allocate record; if FIND_POSITION flag is set, then
213 * we need to allocate an extended (dn_recpos_t) record.
214 */
215 if (flags & FIND_POSITION)
216 recordp = malloc(sizeof (dn_recpos_t));
217 else
218 recordp = malloc(sizeof (dn_rec_t));
219
220 if (recordp == NULL) {
221 if ((flags & FIND_PARTIAL) == 0)
222 retval = DSVC_NO_MEMORY;
223 break;
224 }
225
226 /*
227 * Fill in record; do a structure copy from our automatic
228 * dn. If FIND_POSITION flag is on, pass back additional
229 * position information.
230 */
231 *recordp = dn;
232 if (flags & FIND_POSITION) {
233 ((dn_recpos_t *)recordp)->dnp_off = recoff;
234 ((dn_recpos_t *)recordp)->dnp_size = ftello(fp) -
235 recoff;
236 }
237
238 /*
239 * Chuck the record on the list and up the counter.
240 */
241 new_records = add_dnrec_to_list(recordp, records);
242 if (new_records == NULL) {
243 free(recordp);
244 if ((flags & FIND_PARTIAL) == 0)
245 retval = DSVC_NO_MEMORY;
246 break;
247 }
248
249 records = new_records;
250 nrecords++;
251 }
252
253 free(buf);
254
255 if (retval == DSVC_SUCCESS) {
256 *nrecordsp = nrecords;
257 if (recordsp != NULL)
258 *recordsp = records;
259 return (DSVC_SUCCESS);
260 }
261
262 if (records != NULL)
263 free_dnrec_list(records);
264
265 return (retval);
266 }
267
268 int
lookup_dn(void * handle,boolean_t partial,uint_t query,int count,const dn_rec_t * targetp,dn_rec_list_t ** recordsp,uint_t * nrecordsp)269 lookup_dn(void *handle, boolean_t partial, uint_t query, int count,
270 const dn_rec_t *targetp, dn_rec_list_t **recordsp, uint_t *nrecordsp)
271 {
272 int retval;
273 char dnpath[MAXPATHLEN];
274 FILE *fp;
275 dn_handle_t *dhp = (dn_handle_t *)handle;
276
277 if ((dhp->dh_oflags & DSVC_READ) == 0)
278 return (DSVC_ACCESS);
279
280 net2path(dnpath, MAXPATHLEN, dhp->dh_location, dhp->dh_net, "");
281 fp = fopen(dnpath, "r");
282 if (fp == NULL)
283 return (syserr_to_dsvcerr(errno));
284
285 retval = find_dn(fp, partial ? FIND_PARTIAL : 0, query, count, targetp,
286 recordsp, nrecordsp);
287
288 (void) fclose(fp);
289 return (retval);
290 }
291
292 /*
293 * Compares the fields in fields[] agains the fields in target `targetp',
294 * using `query' to decide what fields to compare. Returns B_TRUE if `dnp'
295 * matches `targetp', B_FALSE if not. On success, `dnp' is completely
296 * filled in.
297 */
298 static boolean_t
record_match(char * fields[],dn_rec_t * dnp,const dn_rec_t * targetp,uint_t query)299 record_match(char *fields[], dn_rec_t *dnp, const dn_rec_t *targetp,
300 uint_t query)
301 {
302 unsigned int qflags[] = { DN_QFDYNAMIC, DN_QFAUTOMATIC, DN_QFMANUAL,
303 DN_QFUNUSABLE, DN_QFBOOTP_ONLY };
304 unsigned int flags[] = { DN_FDYNAMIC, DN_FAUTOMATIC, DN_FMANUAL,
305 DN_FUNUSABLE, DN_FBOOTP_ONLY };
306 unsigned int i;
307 uint_t dn_cid_len;
308
309 dnp->dn_cip.s_addr = ntohl(inet_addr(fields[DNF_CIP]));
310 if (DSVC_QISEQ(query, DN_QCIP) &&
311 dnp->dn_cip.s_addr != targetp->dn_cip.s_addr)
312 return (B_FALSE);
313 if (DSVC_QISNEQ(query, DN_QCIP) &&
314 dnp->dn_cip.s_addr == targetp->dn_cip.s_addr)
315 return (B_FALSE);
316
317 dnp->dn_lease = atoi(fields[DNF_LEASE]);
318 if (DSVC_QISEQ(query, DN_QLEASE) && targetp->dn_lease != dnp->dn_lease)
319 return (B_FALSE);
320 if (DSVC_QISNEQ(query, DN_QLEASE) && targetp->dn_lease == dnp->dn_lease)
321 return (B_FALSE);
322
323 /*
324 * We use dn_cid_len since dnp->dn_cid_len is of type uchar_t but
325 * hexascii_to_octet() expects a uint_t *
326 */
327 dn_cid_len = DN_MAX_CID_LEN;
328 if (hexascii_to_octet(fields[DNF_CID], strlen(fields[DNF_CID]),
329 dnp->dn_cid, &dn_cid_len) != 0)
330 return (B_FALSE);
331
332 dnp->dn_cid_len = dn_cid_len;
333 if (DSVC_QISEQ(query, DN_QCID) &&
334 (dnp->dn_cid_len != targetp->dn_cid_len ||
335 (memcmp(dnp->dn_cid, targetp->dn_cid, dnp->dn_cid_len) != 0)))
336 return (B_FALSE);
337 if (DSVC_QISNEQ(query, DN_QCID) &&
338 (dnp->dn_cid_len == targetp->dn_cid_len &&
339 (memcmp(dnp->dn_cid, targetp->dn_cid, dnp->dn_cid_len) == 0)))
340 return (B_FALSE);
341
342 dnp->dn_sip.s_addr = ntohl(inet_addr(fields[DNF_SIP]));
343 if (DSVC_QISEQ(query, DN_QSIP) &&
344 dnp->dn_sip.s_addr != targetp->dn_sip.s_addr)
345 return (B_FALSE);
346 if (DSVC_QISNEQ(query, DN_QSIP) &&
347 dnp->dn_sip.s_addr == targetp->dn_sip.s_addr)
348 return (B_FALSE);
349
350 (void) strlcpy(dnp->dn_macro, fields[DNF_MACRO],
351 sizeof (dnp->dn_macro));
352 if (DSVC_QISEQ(query, DN_QMACRO) &&
353 strcmp(targetp->dn_macro, dnp->dn_macro) != 0)
354 return (B_FALSE);
355 if (DSVC_QISNEQ(query, DN_QMACRO) &&
356 strcmp(targetp->dn_macro, dnp->dn_macro) == 0)
357 return (B_FALSE);
358
359 dnp->dn_flags = atoi(fields[DNF_FLAGS]);
360 for (i = 0; i < sizeof (qflags) / sizeof (unsigned int); i++) {
361 if (DSVC_QISEQ(query, qflags[i]) &&
362 (dnp->dn_flags & flags[i]) !=
363 (targetp->dn_flags & flags[i]))
364 return (B_FALSE);
365 if (DSVC_QISNEQ(query, qflags[i]) &&
366 (dnp->dn_flags & flags[i]) ==
367 (targetp->dn_flags & flags[i]))
368 return (B_FALSE);
369 }
370 (void) strlcpy(dnp->dn_comment, fields[DNF_COMMENT],
371 sizeof (dnp->dn_comment));
372
373 return (B_TRUE);
374 }
375
376 /*
377 * Internal dhcp_network record update routine, used to factor out the
378 * common code between add_dn(), delete_dn(), and modify_dn(). If `origp'
379 * is NULL, then act like add_dn(); if `newp' is NULL, then act like
380 * delete_dn(); otherwise act like modify_dn().
381 */
382 static int
update_dn(const dn_handle_t * dhp,const dn_rec_t * origp,dn_rec_t * newp)383 update_dn(const dn_handle_t *dhp, const dn_rec_t *origp, dn_rec_t *newp)
384 {
385 char dnpath[MAXPATHLEN], newpath[MAXPATHLEN];
386 int retval = DSVC_SUCCESS;
387 off_t recoff, recnext;
388 dn_rec_list_t *reclist;
389 FILE *fp;
390 int newfd;
391 uint_t found;
392 int query;
393 struct stat st;
394
395 if ((dhp->dh_oflags & DSVC_WRITE) == 0)
396 return (DSVC_ACCESS);
397
398 /*
399 * Open the container to update and a new container file which we
400 * will store the updated version of the container in. When the
401 * update is done, rename the new file to be the real container.
402 */
403 net2path(dnpath, MAXPATHLEN, dhp->dh_location, dhp->dh_net, "");
404 fp = fopen(dnpath, "r");
405 if (fp == NULL)
406 return (syserr_to_dsvcerr(errno));
407
408 net2path(newpath, MAXPATHLEN, dhp->dh_location, dhp->dh_net, ".new");
409 newfd = open(newpath, O_CREAT|O_TRUNC|O_WRONLY, 0644);
410 if (newfd == -1) {
411 (void) fclose(fp);
412 return (syserr_to_dsvcerr(errno));
413 }
414
415 DSVC_QINIT(query);
416 DSVC_QEQ(query, DN_QCIP);
417
418 /*
419 * If we're adding a new record or changing a key for an existing
420 * record, bail if the record we want to add already exists.
421 */
422 if (newp != NULL) {
423 if (origp == NULL ||
424 origp->dn_cip.s_addr != newp->dn_cip.s_addr) {
425 retval = find_dn(fp, 0, query, 1, newp, NULL, &found);
426 if (retval != DSVC_SUCCESS)
427 goto out;
428 if (found != 0) {
429 retval = DSVC_EXISTS;
430 goto out;
431 }
432 }
433 }
434
435 /*
436 * If we're deleting or modifying record, make sure the record
437 * still exists. Note that we don't check signatures because this
438 * is a legacy format that has no signatures.
439 */
440 if (origp != NULL) {
441 retval = find_dn(fp, FIND_POSITION, query, 1, origp, &reclist,
442 &found);
443 if (retval != DSVC_SUCCESS)
444 goto out;
445 if (found == 0) {
446 retval = DSVC_NOENT;
447 goto out;
448 }
449
450 /*
451 * Note the offset of the record we're modifying or deleting
452 * for use down below.
453 */
454 recoff = ((dn_recpos_t *)reclist->dnl_rec)->dnp_off;
455 recnext = recoff + ((dn_recpos_t *)reclist->dnl_rec)->dnp_size;
456
457 free_dnrec_list(reclist);
458 } else {
459 /*
460 * No record to modify or delete, so set `recoff' and
461 * `recnext' appropriately.
462 */
463 recoff = 0;
464 recnext = 0;
465 }
466
467 /*
468 * Make a new copy of the container. If we're deleting or
469 * modifying a record, don't copy that record to the new container.
470 */
471 if (fstat(fileno(fp), &st) == -1) {
472 retval = DSVC_INTERNAL;
473 goto out;
474 }
475
476 retval = copy_range(fileno(fp), 0, newfd, 0, recoff);
477 if (retval != DSVC_SUCCESS)
478 goto out;
479
480 retval = copy_range(fileno(fp), recnext, newfd, recoff,
481 st.st_size - recnext);
482 if (retval != DSVC_SUCCESS)
483 goto out;
484
485 /*
486 * If there's a new/modified record, append it to the new container.
487 */
488 if (newp != NULL) {
489 retval = write_rec(newfd, newp, recoff + st.st_size - recnext);
490 if (retval != DSVC_SUCCESS)
491 goto out;
492 }
493
494 /*
495 * Note: we close these descriptors before the rename(2) (rather
496 * than just having the `out:' label clean them up) to save NFS
497 * some work (otherwise, NFS has to save `dnpath' to an alternate
498 * name since its vnode would still be active).
499 */
500 (void) fclose(fp);
501 (void) close(newfd);
502
503 if (rename(newpath, dnpath) == -1)
504 retval = syserr_to_dsvcerr(errno);
505
506 return (retval);
507 out:
508 (void) fclose(fp);
509 (void) close(newfd);
510 (void) unlink(newpath);
511 return (retval);
512 }
513
514 int
add_dn(void * handle,dn_rec_t * addp)515 add_dn(void *handle, dn_rec_t *addp)
516 {
517 return (update_dn((dn_handle_t *)handle, NULL, addp));
518 }
519
520 int
modify_dn(void * handle,const dn_rec_t * origp,dn_rec_t * newp)521 modify_dn(void *handle, const dn_rec_t *origp, dn_rec_t *newp)
522 {
523 return (update_dn((dn_handle_t *)handle, origp, newp));
524 }
525
526 int
delete_dn(void * handle,const dn_rec_t * delp)527 delete_dn(void *handle, const dn_rec_t *delp)
528 {
529 return (update_dn((dn_handle_t *)handle, delp, NULL));
530 }
531
532 int
list_dn(const char * location,char *** listppp,uint_t * countp)533 list_dn(const char *location, char ***listppp, uint_t *countp)
534 {
535 char ipaddr[INET_ADDRSTRLEN];
536 struct dirent *result;
537 DIR *dirp;
538 unsigned int i, count = 0;
539 char *re, **new_listpp, **listpp = NULL;
540 int error;
541
542 dirp = opendir(location);
543 if (dirp == NULL) {
544 switch (errno) {
545 case EACCES:
546 case EPERM:
547 return (DSVC_ACCESS);
548 case ENOENT:
549 return (DSVC_NO_LOCATION);
550 default:
551 break;
552 }
553 return (DSVC_INTERNAL);
554 }
555
556 /*
557 * Compile a regular expression matching an IP address delimited by
558 * underscores. Note that the `$0' at the end allows us to save the
559 * IP address in ipaddr when calling regex(3C).
560 */
561 re = regcmp("^(([0-9]{1,3}\\_){3}[0-9]{1,3})$0$", (char *)0);
562 if (re == NULL)
563 return (DSVC_NO_MEMORY);
564
565 while ((result = readdir(dirp)) != NULL) {
566 if (regex(re, result->d_name, ipaddr) != NULL) {
567 new_listpp = realloc(listpp,
568 (sizeof (char **)) * (count + 1));
569 if (new_listpp == NULL) {
570 error = DSVC_NO_MEMORY;
571 goto fail;
572 }
573 listpp = new_listpp;
574 listpp[count] = strdup(ipaddr);
575 if (listpp[count] == NULL) {
576 error = DSVC_NO_MEMORY;
577 goto fail;
578 }
579
580 /*
581 * Change all underscores to dots.
582 */
583 for (i = 0; listpp[count][i] != '\0'; i++) {
584 if (listpp[count][i] == '_')
585 listpp[count][i] = '.';
586 }
587
588 count++;
589 }
590 }
591 free(re);
592 (void) closedir(dirp);
593
594 *countp = count;
595 *listppp = listpp;
596 return (DSVC_SUCCESS);
597
598 fail:
599 free(re);
600 (void) closedir(dirp);
601
602 for (i = 0; i < count; i++)
603 free(listpp[i]);
604 free(listpp);
605 return (error);
606 }
607
608 /*
609 * Given a buffer `path' of `pathlen' bytes, fill it in with a path to the
610 * DHCP Network table for IP network `ip' located in directory `dir' with a
611 * suffix of `suffix'.
612 */
613 static void
net2path(char * path,size_t pathlen,const char * dir,ipaddr_t ip,const char * suffix)614 net2path(char *path, size_t pathlen, const char *dir, ipaddr_t ip,
615 const char *suffix)
616 {
617 (void) snprintf(path, pathlen, "%s/%d_%d_%d_%d%s", dir, ip >> 24,
618 (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff, suffix);
619 }
620
621 /*
622 * Write the dn_rec_t `recp' into the open container `fd' at offset
623 * `recoff'. Returns DSVC_* error code.
624 */
625 static int
write_rec(int fd,dn_rec_t * recp,off_t recoff)626 write_rec(int fd, dn_rec_t *recp, off_t recoff)
627 {
628 char entbuf[1024], *ent = entbuf;
629 size_t entsize = sizeof (entbuf);
630 int entlen;
631 char dn_cip[INET_ADDRSTRLEN], dn_sip[INET_ADDRSTRLEN];
632 char dn_cid[DN_MAX_CID_LEN * 2 + 1];
633 unsigned int dn_cid_len = sizeof (dn_cid);
634 struct in_addr nip;
635
636 if (octet_to_hexascii(recp->dn_cid, recp->dn_cid_len, dn_cid,
637 &dn_cid_len) != 0)
638 return (DSVC_INTERNAL);
639
640 nip.s_addr = htonl(recp->dn_cip.s_addr);
641 (void) inet_ntop(AF_INET, &nip, dn_cip, sizeof (dn_cip));
642 nip.s_addr = htonl(recp->dn_sip.s_addr);
643 (void) inet_ntop(AF_INET, &nip, dn_sip, sizeof (dn_sip));
644 again:
645 if (recp->dn_comment[0] != '\0') {
646 entlen = snprintf(ent, entsize, "%s %02hu %s %s %u %s %c%s\n",
647 dn_cid, recp->dn_flags, dn_cip, dn_sip, recp->dn_lease,
648 recp->dn_macro, DNF_COMMENT_CHAR, recp->dn_comment);
649 } else {
650 entlen = snprintf(ent, entsize, "%s %02hu %s %s %u %s\n",
651 dn_cid, recp->dn_flags, dn_cip, dn_sip, recp->dn_lease,
652 recp->dn_macro);
653 }
654
655 if (entlen == -1)
656 return (syserr_to_dsvcerr(errno));
657
658 if (entlen > entsize) {
659 entsize = entlen;
660 ent = alloca(entlen);
661 goto again;
662 }
663
664 if (pnwrite(fd, ent, entlen, recoff) == -1)
665 return (syserr_to_dsvcerr(errno));
666
667 return (DSVC_SUCCESS);
668 }
669