]>
Commit | Line | Data |
---|---|---|
7fcc4f29 DG |
1 | //! Preopen functionality for WASI libc, for emulating support for absolute |
2 | //! path names on top of WASI's OCap-style API. | |
3 | //! | |
4 | //! This file is derived from code in libpreopen, the upstream for which is: | |
5 | //! | |
6 | //! https://github.com/musec/libpreopen | |
7 | //! | |
8 | //! and which bears the following copyrights and license: | |
9 | ||
10 | /*- | |
11 | * Copyright (c) 2016-2017 Stanley Uche Godfrey | |
12 | * Copyright (c) 2016-2018 Jonathan Anderson | |
13 | * All rights reserved. | |
14 | * | |
15 | * This software was developed at Memorial University under the | |
16 | * NSERC Discovery program (RGPIN-2015-06048). | |
17 | * | |
18 | * Redistribution and use in source and binary forms, with or without | |
19 | * modification, are permitted provided that the following conditions | |
20 | * are met: | |
21 | * | |
22 | * 1. Redistributions of source code must retain the above copyright | |
23 | * notice, this list of conditions and the following disclaimer. | |
24 | * 2. Redistributions in binary form must reproduce the above copyright | |
25 | * notice, this list of conditions and the following disclaimer in the | |
26 | * documentation and/or other materials provided with the distribution. | |
27 | * | |
28 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |
29 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
30 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
31 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | |
32 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
33 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
34 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
35 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |
36 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |
37 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |
38 | * SUCH DAMAGE. | |
39 | */ | |
40 | ||
41 | #ifdef _REENTRANT | |
42 | #error "__wasilibc_register_preopened_fd doesn't yet support multiple threads" | |
43 | #endif | |
44 | ||
45 | #define _ALL_SOURCE | |
46 | #include <sys/stat.h> | |
7b92f334 | 47 | #include <utime.h> |
7fcc4f29 DG |
48 | #include <fcntl.h> |
49 | #include <stdarg.h> | |
50 | #include <stdlib.h> | |
51 | #include <stdio.h> | |
52 | #include <stdbool.h> | |
53 | #include <string.h> | |
54 | #include <unistd.h> | |
55 | #include <errno.h> | |
56 | #include <dirent.h> | |
57 | #include <assert.h> | |
410c6607 | 58 | #include <sysexits.h> |
7fcc4f29 DG |
59 | #include <wasi/libc.h> |
60 | #include <wasi/libc-find-relpath.h> | |
61 | ||
62 | //////////////////////////////////////////////////////////////////////////////// | |
63 | // | |
64 | // POSIX API compatibility wrappers | |
65 | // | |
66 | //////////////////////////////////////////////////////////////////////////////// | |
67 | ||
68 | int | |
69 | open(const char *path, int flags, ...) | |
a94d2d04 DG |
70 | { |
71 | // WASI libc's openat ignores the mode argument, so call a special | |
72 | // entrypoint which avoids the varargs calling convention. | |
73 | return __wasilibc_open_nomode(path, flags); | |
74 | } | |
75 | ||
76 | int | |
77 | __wasilibc_open_nomode(const char *path, int flags) | |
7fcc4f29 DG |
78 | { |
79 | const char *relative_path; | |
54102f06 | 80 | int dirfd = __wasilibc_find_relpath(path, &relative_path); |
7fcc4f29 DG |
81 | |
82 | // If we can't find a preopened directory handle to open this file with, | |
83 | // indicate that the program lacks the capabilities. | |
84 | if (dirfd == -1) { | |
85 | errno = ENOTCAPABLE; | |
86 | return -1; | |
87 | } | |
88 | ||
a94d2d04 | 89 | return __wasilibc_openat_nomode(dirfd, relative_path, flags); |
7fcc4f29 DG |
90 | } |
91 | ||
92 | int | |
93 | access(const char *path, int mode) | |
94 | { | |
95 | const char *relative_path; | |
54102f06 | 96 | int dirfd = __wasilibc_find_relpath(path, &relative_path); |
7fcc4f29 DG |
97 | |
98 | // If we can't find a preopened directory handle to open this file with, | |
99 | // indicate that the program lacks the capabilities. | |
100 | if (dirfd == -1) { | |
101 | errno = ENOTCAPABLE; | |
102 | return -1; | |
103 | } | |
104 | ||
105 | return faccessat(dirfd, relative_path, mode, 0); | |
106 | } | |
107 | ||
108 | int | |
109 | lstat(const char *path, struct stat *st) | |
110 | { | |
111 | const char *relative_path; | |
54102f06 | 112 | int dirfd = __wasilibc_find_relpath(path, &relative_path); |
7fcc4f29 DG |
113 | |
114 | // If we can't find a preopened directory handle to open this file with, | |
115 | // indicate that the program lacks the capabilities. | |
116 | if (dirfd == -1) { | |
117 | errno = ENOTCAPABLE; | |
118 | return -1; | |
119 | } | |
120 | ||
121 | return fstatat(dirfd, relative_path, st, AT_SYMLINK_NOFOLLOW); | |
122 | } | |
123 | ||
124 | int | |
125 | rename(const char *from, const char *to) | |
126 | { | |
127 | const char *from_relative_path; | |
54102f06 | 128 | int from_dirfd = __wasilibc_find_relpath(from, &from_relative_path); |
7fcc4f29 DG |
129 | |
130 | const char *to_relative_path; | |
54102f06 | 131 | int to_dirfd = __wasilibc_find_relpath(to, &to_relative_path); |
7fcc4f29 DG |
132 | |
133 | // If we can't find a preopened directory handle to open this file with, | |
134 | // indicate that the program lacks the capabilities. | |
135 | if (from_dirfd == -1 || to_dirfd == -1) { | |
136 | errno = ENOTCAPABLE; | |
137 | return -1; | |
138 | } | |
139 | ||
140 | return renameat(from_dirfd, from_relative_path, to_dirfd, to_relative_path); | |
141 | } | |
142 | ||
143 | int | |
144 | stat(const char *path, struct stat *st) | |
145 | { | |
146 | const char *relative_path; | |
54102f06 | 147 | int dirfd = __wasilibc_find_relpath(path, &relative_path); |
7fcc4f29 DG |
148 | |
149 | // If we can't find a preopened directory handle to open this file with, | |
150 | // indicate that the program lacks the capabilities. | |
151 | if (dirfd == -1) { | |
152 | errno = ENOTCAPABLE; | |
153 | return -1; | |
154 | } | |
155 | ||
7b92f334 OT |
156 | return fstatat(dirfd, relative_path, st, 0); |
157 | } | |
158 | ||
159 | int | |
160 | utime(const char *path, const struct utimbuf *times) | |
161 | { | |
162 | const char *relative_path; | |
163 | int fd = __wasilibc_find_relpath(path, &relative_path); | |
164 | ||
165 | // If we can't find a preopened directory handle to open this file with, | |
166 | // indicate that the program lacks the capabilities. | |
167 | if (fd == -1) { | |
168 | errno = ENOTCAPABLE; | |
169 | return -1; | |
170 | } | |
171 | return utimensat(fd, relative_path, times ? ((struct timespec [2]){ | |
172 | { .tv_sec = times->actime }, { .tv_sec = times->modtime }}) | |
173 | : 0, 0); | |
7fcc4f29 DG |
174 | } |
175 | ||
176 | int | |
177 | unlink(const char *path) | |
178 | { | |
179 | const char *relative_path; | |
54102f06 | 180 | int dirfd = __wasilibc_find_relpath(path, &relative_path); |
7fcc4f29 DG |
181 | |
182 | // If we can't find a preopened directory handle to open this file with, | |
183 | // indicate that the program lacks the capabilities. | |
184 | if (dirfd == -1) { | |
185 | errno = ENOTCAPABLE; | |
186 | return -1; | |
187 | } | |
188 | ||
189 | // `unlinkat` ends up importing `__wasi_path_remove_directory` even | |
190 | // though we're not passing `AT_REMOVEDIR` here. So instead, use a | |
191 | // specialized function which just imports `__wasi_path_unlink_file`. | |
192 | return __wasilibc_unlinkat(dirfd, relative_path); | |
193 | } | |
194 | ||
195 | int | |
196 | rmdir(const char *pathname) | |
197 | { | |
198 | const char *relative_path; | |
54102f06 | 199 | int dirfd = __wasilibc_find_relpath(pathname, &relative_path); |
7fcc4f29 DG |
200 | |
201 | // If we can't find a preopened directory handle to open this file with, | |
202 | // indicate that the program lacks the capabilities. | |
203 | if (dirfd == -1) { | |
204 | errno = ENOTCAPABLE; | |
205 | return -1; | |
206 | } | |
207 | ||
208 | return __wasilibc_rmdirat(dirfd, relative_path); | |
209 | } | |
210 | ||
211 | int | |
212 | remove(const char *pathname) | |
213 | { | |
214 | const char *relative_path; | |
54102f06 | 215 | int dirfd = __wasilibc_find_relpath(pathname, &relative_path); |
7fcc4f29 DG |
216 | |
217 | // If searching for both file and directory rights failed, try searching | |
218 | // for either individually. | |
219 | if (dirfd == -1) { | |
54102f06 | 220 | dirfd = __wasilibc_find_relpath(pathname, &relative_path); |
7fcc4f29 | 221 | if (dirfd == -1) { |
54102f06 | 222 | dirfd = __wasilibc_find_relpath(pathname, &relative_path); |
7fcc4f29 DG |
223 | } |
224 | } | |
225 | ||
226 | // If we can't find a preopened directory handle to open this file with, | |
227 | // indicate that the program lacks the capabilities. | |
228 | if (dirfd == -1) { | |
229 | errno = ENOTCAPABLE; | |
230 | return -1; | |
231 | } | |
232 | ||
233 | int r = __wasilibc_unlinkat(dirfd, relative_path); | |
234 | if (r != 0 && (errno == EISDIR || errno == ENOTCAPABLE)) | |
235 | r = __wasilibc_rmdirat(dirfd, relative_path); | |
236 | return r; | |
237 | } | |
238 | ||
239 | int | |
240 | link(const char *oldpath, const char *newpath) | |
241 | { | |
242 | const char *old_relative_path; | |
54102f06 | 243 | int old_dirfd = __wasilibc_find_relpath(oldpath, &old_relative_path); |
7fcc4f29 DG |
244 | |
245 | const char *new_relative_path; | |
54102f06 | 246 | int new_dirfd = __wasilibc_find_relpath(newpath, &new_relative_path); |
7fcc4f29 DG |
247 | |
248 | // If we can't find a preopened directory handle to open this file with, | |
249 | // indicate that the program lacks the capabilities. | |
250 | if (old_dirfd == -1 || new_dirfd == -1) { | |
251 | errno = ENOTCAPABLE; | |
252 | return -1; | |
253 | } | |
254 | ||
255 | return linkat(old_dirfd, old_relative_path, new_dirfd, new_relative_path, 0); | |
256 | } | |
257 | ||
258 | int | |
259 | mkdir(const char *pathname, mode_t mode) | |
260 | { | |
261 | const char *relative_path; | |
54102f06 | 262 | int dirfd = __wasilibc_find_relpath(pathname, &relative_path); |
7fcc4f29 DG |
263 | |
264 | // If we can't find a preopened directory handle to open this file with, | |
265 | // indicate that the program lacks the capabilities. | |
266 | if (dirfd == -1) { | |
267 | errno = ENOTCAPABLE; | |
268 | return -1; | |
269 | } | |
270 | ||
271 | return mkdirat(dirfd, relative_path, mode); | |
272 | } | |
273 | ||
274 | DIR * | |
275 | opendir(const char *name) | |
276 | { | |
277 | const char *relative_path; | |
54102f06 | 278 | int dirfd = __wasilibc_find_relpath(name, &relative_path); |
7fcc4f29 DG |
279 | |
280 | // If we can't find a preopened directory handle to open this file with, | |
281 | // indicate that the program lacks the capabilities. | |
282 | if (dirfd == -1) { | |
283 | errno = ENOTCAPABLE; | |
284 | return NULL; | |
285 | } | |
286 | ||
287 | return opendirat(dirfd, relative_path); | |
288 | } | |
289 | ||
290 | ssize_t | |
291 | readlink(const char *pathname, char *buf, size_t bufsiz) | |
292 | { | |
293 | const char *relative_path; | |
54102f06 | 294 | int dirfd = __wasilibc_find_relpath(pathname, &relative_path); |
7fcc4f29 DG |
295 | |
296 | // If we can't find a preopened directory handle to open this file with, | |
297 | // indicate that the program lacks the capabilities. | |
298 | if (dirfd == -1) { | |
299 | errno = ENOTCAPABLE; | |
300 | return -1; | |
301 | } | |
302 | ||
303 | return readlinkat(dirfd, relative_path, buf, bufsiz); | |
304 | } | |
305 | ||
306 | int | |
307 | scandir( | |
308 | const char *dirp, | |
309 | struct dirent ***namelist, | |
310 | int (*filter)(const struct dirent *), | |
311 | int (*compar)(const struct dirent **, const struct dirent **)) | |
312 | { | |
313 | const char *relative_path; | |
54102f06 | 314 | int dirfd = __wasilibc_find_relpath(dirp, &relative_path); |
7fcc4f29 DG |
315 | |
316 | // If we can't find a preopened directory handle to open this file with, | |
317 | // indicate that the program lacks the capabilities. | |
318 | if (dirfd == -1) { | |
319 | errno = ENOTCAPABLE; | |
320 | return -1; | |
321 | } | |
322 | ||
323 | return scandirat(dirfd, relative_path, namelist, filter, compar); | |
324 | } | |
325 | ||
326 | int | |
327 | symlink(const char *target, const char *linkpath) | |
328 | { | |
329 | const char *relative_path; | |
54102f06 | 330 | int dirfd = __wasilibc_find_relpath(linkpath, &relative_path); |
7fcc4f29 DG |
331 | |
332 | // If we can't find a preopened directory handle to open this file with, | |
333 | // indicate that the program lacks the capabilities. | |
334 | if (dirfd == -1) { | |
335 | errno = ENOTCAPABLE; | |
336 | return -1; | |
337 | } | |
338 | ||
339 | return symlinkat(target, dirfd, relative_path); | |
340 | } | |
341 | ||
342 | //////////////////////////////////////////////////////////////////////////////// | |
343 | // | |
344 | // Support library | |
345 | // | |
346 | //////////////////////////////////////////////////////////////////////////////// | |
347 | ||
348 | /// An entry in a po_map. | |
349 | struct po_map_entry { | |
350 | /// The name this file or directory is mapped to. | |
351 | /// | |
352 | /// This name should look like a path, but it does not necessarily need | |
353 | /// match to match the path it was originally obtained from. | |
354 | const char *name; | |
355 | ||
356 | /// File descriptor (which may be a directory) | |
357 | int fd; | |
7fcc4f29 DG |
358 | }; |
359 | ||
360 | /// A vector of po_map_entry. | |
361 | struct po_map { | |
362 | struct po_map_entry *entries; | |
363 | size_t capacity; | |
364 | size_t length; | |
365 | }; | |
366 | ||
367 | static struct po_map global_map; | |
368 | ||
369 | /// Is a directory a prefix of a given path? | |
370 | /// | |
371 | /// @param dir a directory path, e.g., `/foo/bar` | |
372 | /// @param dirlen the length of @b dir | |
373 | /// @param path a path that may have @b dir as a prefix, | |
374 | /// e.g., `/foo/bar/baz` | |
375 | static bool | |
376 | po_isprefix(const char *dir, size_t dirlen, const char *path) | |
377 | { | |
378 | assert(dir != NULL); | |
379 | assert(path != NULL); | |
380 | ||
381 | // Allow an empty string as a prefix of any relative path. | |
382 | if (path[0] != '/' && dirlen == 0) | |
383 | return true; | |
384 | ||
385 | size_t i; | |
386 | for (i = 0; i < dirlen; i++) | |
387 | { | |
388 | if (path[i] != dir[i]) | |
389 | return false; | |
390 | } | |
391 | ||
392 | // Ignore trailing slashes in directory names. | |
393 | while (i > 0 && dir[i - 1] == '/') { | |
394 | --i; | |
395 | } | |
396 | ||
397 | return path[i] == '/' || path[i] == '\0'; | |
398 | } | |
399 | ||
400 | /// Enlarge a @ref po_map's capacity. | |
401 | /// | |
402 | /// This results in new memory being allocated and existing entries being copied. | |
403 | /// If the allocation fails, the function will return -1. | |
404 | static int | |
405 | po_map_enlarge(void) | |
406 | { | |
407 | size_t start_capacity = 4; | |
408 | size_t new_capacity = global_map.capacity == 0 ? | |
409 | start_capacity : | |
410 | global_map.capacity * 2; | |
411 | ||
412 | struct po_map_entry *enlarged = | |
413 | calloc(sizeof(struct po_map_entry), new_capacity); | |
414 | if (enlarged == NULL) { | |
415 | return -1; | |
416 | } | |
417 | memcpy(enlarged, global_map.entries, global_map.length * sizeof(*enlarged)); | |
418 | free(global_map.entries); | |
419 | global_map.entries = enlarged; | |
420 | global_map.capacity = new_capacity; | |
421 | return 0; | |
422 | } | |
423 | ||
424 | /// Assert that the global_map is valid. | |
425 | #ifdef NDEBUG | |
426 | #define po_map_assertvalid() | |
427 | #else | |
428 | static void | |
429 | po_map_assertvalid(void) | |
430 | { | |
431 | const struct po_map_entry *entry; | |
432 | size_t i; | |
433 | ||
434 | assert(global_map.length <= global_map.capacity); | |
435 | assert(global_map.entries != NULL || global_map.capacity == 0); | |
436 | ||
437 | for (i = 0; i < global_map.length; i++) { | |
438 | entry = &global_map.entries[i]; | |
439 | ||
440 | assert(entry->name != NULL); | |
441 | assert(entry->fd >= 0); | |
442 | } | |
443 | } | |
444 | #endif | |
445 | ||
446 | /// Register the given pre-opened file descriptor under the given path. | |
5216983a DG |
447 | /// |
448 | /// This function takes ownership of `name`. | |
449 | static int | |
450 | internal_register_preopened_fd(int fd, const char *name) | |
7fcc4f29 DG |
451 | { |
452 | po_map_assertvalid(); | |
453 | ||
454 | assert(fd >= 0); | |
5216983a | 455 | assert(name != NULL); |
7fcc4f29 DG |
456 | |
457 | if (global_map.length == global_map.capacity) { | |
458 | int n = po_map_enlarge(); | |
459 | ||
460 | po_map_assertvalid(); | |
461 | ||
462 | if (n != 0) { | |
463 | return n; | |
464 | } | |
465 | } | |
466 | ||
7fcc4f29 DG |
467 | struct po_map_entry *entry = &global_map.entries[global_map.length++]; |
468 | ||
469 | entry->name = name; | |
470 | entry->fd = fd; | |
7fcc4f29 DG |
471 | |
472 | po_map_assertvalid(); | |
473 | ||
474 | return 0; | |
475 | } | |
476 | ||
5216983a DG |
477 | /// Register the given pre-opened file descriptor under the given path. |
478 | /// | |
479 | /// This function does not take ownership of `path`. | |
480 | int | |
481 | __wasilibc_register_preopened_fd(int fd, const char *path) | |
482 | { | |
483 | const char *name = strdup(path); | |
951cc3ec | 484 | return name == NULL ? -1 : internal_register_preopened_fd(fd, name); |
5216983a DG |
485 | } |
486 | ||
7fcc4f29 DG |
487 | int |
488 | __wasilibc_find_relpath( | |
489 | const char *path, | |
7fcc4f29 DG |
490 | const char **relative_path) |
491 | { | |
492 | size_t bestlen = 0; | |
493 | int best = -1; | |
494 | ||
495 | po_map_assertvalid(); | |
496 | ||
497 | assert(path != NULL); | |
498 | assert(relative_path != NULL); | |
499 | ||
500 | bool any_matches = false; | |
501 | for (size_t i = 0; i < global_map.length; i++) { | |
502 | const struct po_map_entry *entry = &global_map.entries[i]; | |
503 | const char *name = entry->name; | |
504 | size_t len = strlen(name); | |
505 | ||
506 | if (path[0] != '/' && (path[0] != '.' || (path[1] != '/' && path[1] != '\0'))) { | |
507 | // We're matching a relative path that doesn't start with "./" and isn't ".". | |
508 | if (len >= 2 && name[0] == '.' && name[1] == '/') { | |
509 | // The entry starts with "./", so skip that prefix. | |
510 | name += 2; | |
511 | len -= 2; | |
512 | } else if (len == 1 && name[0] == '.') { | |
513 | // The entry is ".", so match it as an empty string. | |
514 | name += 1; | |
515 | len -= 1; | |
516 | } | |
517 | } | |
518 | ||
519 | if ((any_matches && len <= bestlen) || !po_isprefix(name, len, path)) { | |
520 | continue; | |
521 | } | |
522 | ||
7fcc4f29 DG |
523 | best = entry->fd; |
524 | bestlen = len; | |
525 | any_matches = true; | |
526 | } | |
527 | ||
528 | const char *relpath = path + bestlen; | |
529 | ||
530 | while (*relpath == '/') { | |
531 | relpath++; | |
532 | } | |
533 | ||
534 | if (*relpath == '\0') { | |
535 | relpath = "."; | |
536 | } | |
537 | ||
538 | *relative_path = relpath; | |
539 | return best; | |
540 | } | |
70099d4d DG |
541 | |
542 | /// This is referenced by weak reference from crt1.c and lives in the same source | |
543 | /// file as `__wasilibc_find_relpath` so that it's linked in when it's needed. | |
410c6607 | 544 | // Concerning the 51 -- see the comment by the constructor priority in |
9efc2f42 | 545 | // libc-bottom-half/sources/__wasilibc_environ.c. |
410c6607 DG |
546 | __attribute__((constructor(51))) |
547 | static void | |
70099d4d DG |
548 | __wasilibc_populate_libpreopen(void) |
549 | { | |
550 | // Skip stdin, stdout, and stderr, and count up until we reach an invalid | |
551 | // file descriptor. | |
552 | for (__wasi_fd_t fd = 3; fd != 0; ++fd) { | |
553 | __wasi_prestat_t prestat; | |
554 | __wasi_errno_t ret = __wasi_fd_prestat_get(fd, &prestat); | |
446cb3f1 | 555 | if (ret == __WASI_ERRNO_BADF) |
70099d4d | 556 | break; |
446cb3f1 | 557 | if (ret != __WASI_ERRNO_SUCCESS) |
410c6607 | 558 | goto oserr; |
c2ae180d | 559 | switch (prestat.tag) { |
70099d4d DG |
560 | case __WASI_PREOPENTYPE_DIR: { |
561 | char *path = malloc(prestat.u.dir.pr_name_len + 1); | |
562 | if (path == NULL) | |
410c6607 | 563 | goto software; |
70099d4d | 564 | |
446cb3f1 DG |
565 | // TODO: Remove the cast on `path` once the witx is updated with char8 support. |
566 | ret = __wasi_fd_prestat_dir_name(fd, (uint8_t *)path, prestat.u.dir.pr_name_len); | |
567 | if (ret != __WASI_ERRNO_SUCCESS) { | |
410c6607 | 568 | goto oserr; |
70099d4d DG |
569 | } |
570 | path[prestat.u.dir.pr_name_len] = '\0'; | |
571 | ||
5216983a | 572 | if (internal_register_preopened_fd(fd, path) != 0) { |
410c6607 | 573 | goto software; |
70099d4d DG |
574 | } |
575 | ||
70099d4d DG |
576 | break; |
577 | } | |
578 | default: | |
579 | break; | |
580 | } | |
581 | } | |
582 | ||
410c6607 DG |
583 | return; |
584 | oserr: | |
585 | _Exit(EX_OSERR); | |
586 | software: | |
587 | _Exit(EX_SOFTWARE); | |
70099d4d | 588 | } |