1 /*
2 * Run a PAM interaction script for testing.
3 *
4 * Provides an interface that loads a PAM interaction script from a file and
5 * runs through that script, calling the internal PAM module functions and
6 * checking their results. This allows automation of PAM testing through
7 * external data files instead of coding everything in C.
8 *
9 * The canonical version of this file is maintained in the rra-c-util package,
10 * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
11 *
12 * Written by Russ Allbery <eagle@eyrie.org>
13 * Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org>
14 * Copyright 2011-2012, 2014
15 * The Board of Trustees of the Leland Stanford Junior University
16 *
17 * Permission is hereby granted, free of charge, to any person obtaining a
18 * copy of this software and associated documentation files (the "Software"),
19 * to deal in the Software without restriction, including without limitation
20 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
21 * and/or sell copies of the Software, and to permit persons to whom the
22 * Software is furnished to do so, subject to the following conditions:
23 *
24 * The above copyright notice and this permission notice shall be included in
25 * all copies or substantial portions of the Software.
26 *
27 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
30 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
33 * DEALINGS IN THE SOFTWARE.
34 *
35 * SPDX-License-Identifier: MIT
36 */
37
38 #include <config.h>
39 #include <portable/pam.h>
40 #include <portable/system.h>
41
42 #include <assert.h>
43 #include <ctype.h>
44 #include <dirent.h>
45 #include <errno.h>
46 #include <syslog.h>
47
48 #include <tests/fakepam/internal.h>
49 #include <tests/fakepam/script.h>
50 #include <tests/tap/basic.h>
51 #include <tests/tap/string.h>
52
53 /* Used for enumerating arrays. */
54 #define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
55
56 /* Mapping of strings to PAM function pointers and group numbers. */
57 static const struct {
58 const char *name;
59 pam_call call;
60 enum group_type group;
61 } CALLS[] = {
62 /* clang-format off */
63 {"acct_mgmt", pam_sm_acct_mgmt, GROUP_ACCOUNT },
64 {"authenticate", pam_sm_authenticate, GROUP_AUTH },
65 {"setcred", pam_sm_setcred, GROUP_AUTH },
66 {"chauthtok", pam_sm_chauthtok, GROUP_PASSWORD},
67 {"open_session", pam_sm_open_session, GROUP_SESSION },
68 {"close_session", pam_sm_close_session, GROUP_SESSION },
69 /* clang-format on */
70 };
71
72 /* Mapping of PAM flag names without the leading PAM_ to values. */
73 static const struct {
74 const char *name;
75 int value;
76 } FLAGS[] = {
77 /* clang-format off */
78 {"CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK},
79 {"DELETE_CRED", PAM_DELETE_CRED },
80 {"DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK },
81 {"ESTABLISH_CRED", PAM_ESTABLISH_CRED },
82 {"PRELIM_CHECK", PAM_PRELIM_CHECK },
83 {"REFRESH_CRED", PAM_REFRESH_CRED },
84 {"REINITIALIZE_CRED", PAM_REINITIALIZE_CRED },
85 {"SILENT", PAM_SILENT },
86 {"UPDATE_AUTHTOK", PAM_UPDATE_AUTHTOK },
87 /* clang-format on */
88 };
89
90 /* Mapping of strings to PAM groups. */
91 static const struct {
92 const char *name;
93 enum group_type group;
94 } GROUPS[] = {
95 /* clang-format off */
96 {"account", GROUP_ACCOUNT },
97 {"auth", GROUP_AUTH },
98 {"password", GROUP_PASSWORD},
99 {"session", GROUP_SESSION },
100 /* clang-format on */
101 };
102
103 /* Mapping of strings to PAM return values. */
104 static const struct {
105 const char *name;
106 int status;
107 } RETURNS[] = {
108 /* clang-format off */
109 {"PAM_AUTH_ERR", PAM_AUTH_ERR },
110 {"PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL},
111 {"PAM_AUTHTOK_ERR", PAM_AUTHTOK_ERR },
112 {"PAM_DATA_SILENT", PAM_DATA_SILENT },
113 {"PAM_IGNORE", PAM_IGNORE },
114 {"PAM_NEW_AUTHTOK_REQD", PAM_NEW_AUTHTOK_REQD},
115 {"PAM_SESSION_ERR", PAM_SESSION_ERR },
116 {"PAM_SUCCESS", PAM_SUCCESS },
117 {"PAM_USER_UNKNOWN", PAM_USER_UNKNOWN },
118 /* clang-format on */
119 };
120
121 /* Mapping of PAM prompt styles to their values. */
122 static const struct {
123 const char *name;
124 int style;
125 } STYLES[] = {
126 /* clang-format off */
127 {"echo_off", PAM_PROMPT_ECHO_OFF},
128 {"echo_on", PAM_PROMPT_ECHO_ON },
129 {"error_msg", PAM_ERROR_MSG },
130 {"info", PAM_TEXT_INFO },
131 /* clang-format on */
132 };
133
134 /* Mappings of strings to syslog priorities. */
135 static const struct {
136 const char *name;
137 int priority;
138 } PRIORITIES[] = {
139 /* clang-format off */
140 {"DEBUG", LOG_DEBUG },
141 {"INFO", LOG_INFO },
142 {"NOTICE", LOG_NOTICE},
143 {"ERR", LOG_ERR },
144 {"CRIT", LOG_CRIT },
145 /* clang-format on */
146 };
147
148
149 /*
150 * Given a pointer to a string, skip any leading whitespace and return a
151 * pointer to the first non-whitespace character.
152 */
153 static char *
skip_whitespace(char * p)154 skip_whitespace(char *p)
155 {
156 while (isspace((unsigned char) (*p)))
157 p++;
158 return p;
159 }
160
161
162 /*
163 * Read a line from a file into a BUFSIZ buffer, failing if the line was too
164 * long to fit into the buffer, and returns a copy of that line in newly
165 * allocated memory. Ignores blank lines and comments. Caller is responsible
166 * for freeing. Returns NULL on end of file and fails on read errors.
167 */
168 static char *
readline(FILE * file)169 readline(FILE *file)
170 {
171 char buffer[BUFSIZ];
172 char *line, *first;
173
174 do {
175 line = fgets(buffer, sizeof(buffer), file);
176 if (line == NULL) {
177 if (feof(file))
178 return NULL;
179 sysbail("cannot read line from script");
180 }
181 if (buffer[strlen(buffer) - 1] != '\n')
182 bail("script line too long");
183 buffer[strlen(buffer) - 1] = '\0';
184 first = skip_whitespace(buffer);
185 } while (first[0] == '#' || first[0] == '\0');
186 line = bstrdup(buffer);
187 return line;
188 }
189
190
191 /*
192 * Given the name of a PAM call, map it to a call enum. This is used later in
193 * switch statements to determine which function to call. Fails on any
194 * unrecognized string. If the optional second argument is not NULL, also
195 * store the group number in that argument.
196 */
197 static pam_call
string_to_call(const char * name,enum group_type * group)198 string_to_call(const char *name, enum group_type *group)
199 {
200 size_t i;
201
202 for (i = 0; i < ARRAY_SIZE(CALLS); i++)
203 if (strcmp(name, CALLS[i].name) == 0) {
204 if (group != NULL)
205 *group = CALLS[i].group;
206 return CALLS[i].call;
207 }
208 bail("unrecognized PAM call %s", name);
209 }
210
211
212 /*
213 * Given a PAM flag value without the leading PAM_, map it to the numeric
214 * value of that flag. Fails on any unrecognized string.
215 */
216 static int
string_to_flag(const char * name)217 string_to_flag(const char *name)
218 {
219 size_t i;
220
221 for (i = 0; i < ARRAY_SIZE(FLAGS); i++)
222 if (strcmp(name, FLAGS[i].name) == 0)
223 return FLAGS[i].value;
224 bail("unrecognized PAM flag %s", name);
225 }
226
227
228 /*
229 * Given a PAM group name, map it to the array index for the options array for
230 * that group. Fails on any unrecognized string.
231 */
232 static enum group_type
string_to_group(const char * name)233 string_to_group(const char *name)
234 {
235 size_t i;
236
237 for (i = 0; i < ARRAY_SIZE(GROUPS); i++)
238 if (strcmp(name, GROUPS[i].name) == 0)
239 return GROUPS[i].group;
240 bail("unrecognized PAM group %s", name);
241 }
242
243
244 /*
245 * Given a syslog priority name, map it to the numeric value of that priority.
246 * Fails on any unrecognized string.
247 */
248 static int
string_to_priority(const char * name)249 string_to_priority(const char *name)
250 {
251 size_t i;
252
253 for (i = 0; i < ARRAY_SIZE(PRIORITIES); i++)
254 if (strcmp(name, PRIORITIES[i].name) == 0)
255 return PRIORITIES[i].priority;
256 bail("unrecognized syslog priority %s", name);
257 }
258
259
260 /*
261 * Given a PAM return status, map it to the actual expected value. Fails on
262 * any unrecognized string.
263 */
264 static int
string_to_status(const char * name)265 string_to_status(const char *name)
266 {
267 size_t i;
268
269 if (name == NULL)
270 bail("no PAM status on line");
271 for (i = 0; i < ARRAY_SIZE(RETURNS); i++)
272 if (strcmp(name, RETURNS[i].name) == 0)
273 return RETURNS[i].status;
274 bail("unrecognized PAM status %s", name);
275 }
276
277
278 /*
279 * Given a PAM prompt style value without the leading PAM_PROMPT_, map it to
280 * the numeric value of that flag. Fails on any unrecognized string.
281 */
282 static int
string_to_style(const char * name)283 string_to_style(const char *name)
284 {
285 size_t i;
286
287 for (i = 0; i < ARRAY_SIZE(STYLES); i++)
288 if (strcmp(name, STYLES[i].name) == 0)
289 return STYLES[i].style;
290 bail("unrecognized PAM prompt style %s", name);
291 }
292
293
294 /*
295 * We found a section delimiter while parsing another section. Rewind our
296 * input file back before the section delimiter so that we'll read it again.
297 * Takes the length of the line we read, which is used to determine how far to
298 * rewind.
299 */
300 static void
rewind_section(FILE * script,size_t length)301 rewind_section(FILE *script, size_t length)
302 {
303 if (fseek(script, -length - 1, SEEK_CUR) != 0)
304 sysbail("cannot rewind file");
305 }
306
307
308 /*
309 * Given a string that may contain %-escapes, expand it into the resulting
310 * value. The following escapes are supported:
311 *
312 * %i current UID (not target user UID)
313 * %n new password
314 * %p password
315 * %u username
316 * %0 user-supplied string
317 * ...
318 * %9 user-supplied string
319 *
320 * The %* escape is preserved as-is, as it has to be interpreted at the time
321 * of checking output. Returns the expanded string in newly-allocated memory.
322 */
323 static char *
expand_string(const char * template,const struct script_config * config)324 expand_string(const char *template, const struct script_config *config)
325 {
326 size_t length = 0;
327 const char *p, *extra;
328 char *output, *out;
329 char *uid = NULL;
330
331 length = 0;
332 for (p = template; *p != '\0'; p++) {
333 if (*p != '%')
334 length++;
335 else {
336 p++;
337 switch (*p) {
338 case 'i':
339 if (uid == NULL)
340 basprintf(&uid, "%lu", (unsigned long) getuid());
341 length += strlen(uid);
342 break;
343 case 'n':
344 if (config->newpass == NULL)
345 bail("new password not set");
346 length += strlen(config->newpass);
347 break;
348 case 'p':
349 if (config->password == NULL)
350 bail("password not set");
351 length += strlen(config->password);
352 break;
353 case 'u':
354 length += strlen(config->user);
355 break;
356 case '0':
357 case '1':
358 case '2':
359 case '3':
360 case '4':
361 case '5':
362 case '6':
363 case '7':
364 case '8':
365 case '9':
366 if (config->extra[*p - '0'] == NULL)
367 bail("extra script parameter %%%c not set", *p);
368 length += strlen(config->extra[*p - '0']);
369 break;
370 case '*':
371 length += 2;
372 break;
373 default:
374 length++;
375 break;
376 }
377 }
378 }
379 output = bmalloc(length + 1);
380 for (p = template, out = output; *p != '\0'; p++) {
381 if (*p != '%')
382 *out++ = *p;
383 else {
384 p++;
385 switch (*p) {
386 case 'i':
387 assert(uid != NULL);
388 memcpy(out, uid, strlen(uid));
389 out += strlen(uid);
390 break;
391 case 'n':
392 memcpy(out, config->newpass, strlen(config->newpass));
393 out += strlen(config->newpass);
394 break;
395 case 'p':
396 memcpy(out, config->password, strlen(config->password));
397 out += strlen(config->password);
398 break;
399 case 'u':
400 memcpy(out, config->user, strlen(config->user));
401 out += strlen(config->user);
402 break;
403 case '0':
404 case '1':
405 case '2':
406 case '3':
407 case '4':
408 case '5':
409 case '6':
410 case '7':
411 case '8':
412 case '9':
413 extra = config->extra[*p - '0'];
414 memcpy(out, extra, strlen(extra));
415 out += strlen(extra);
416 break;
417 case '*':
418 *out++ = '%';
419 *out++ = '*';
420 break;
421 default:
422 *out++ = *p;
423 break;
424 }
425 }
426 }
427 *out = '\0';
428 free(uid);
429 return output;
430 }
431
432
433 /*
434 * Given a whitespace-delimited string of PAM options, split it into an argv
435 * array and argc count and store it in the provided option struct.
436 */
437 static void
split_options(char * string,struct options * options,const struct script_config * config)438 split_options(char *string, struct options *options,
439 const struct script_config *config)
440 {
441 char *opt;
442 size_t size, count;
443
444 for (opt = strtok(string, " "); opt != NULL; opt = strtok(NULL, " ")) {
445 if (options->argv == NULL) {
446 options->argv = bcalloc(2, sizeof(const char *));
447 options->argv[0] = expand_string(opt, config);
448 options->argc = 1;
449 } else {
450 count = (options->argc + 2);
451 size = sizeof(const char *);
452 options->argv = breallocarray(options->argv, count, size);
453 options->argv[options->argc] = expand_string(opt, config);
454 options->argv[options->argc + 1] = NULL;
455 options->argc++;
456 }
457 }
458 }
459
460
461 /*
462 * Parse the options section of a PAM script. This consists of one or more
463 * lines in the format:
464 *
465 * <group> = <options>
466 *
467 * where options are either option names or option=value pairs, where the
468 * value may not contain whitespace. Returns an options struct, which stores
469 * argc and argv values for each group.
470 *
471 * Takes the work struct as an argument and puts values into its array.
472 */
473 static void
parse_options(FILE * script,struct work * work,const struct script_config * config)474 parse_options(FILE *script, struct work *work,
475 const struct script_config *config)
476 {
477 char *line, *group, *token;
478 size_t length = 0;
479 enum group_type type;
480
481 for (line = readline(script); line != NULL; line = readline(script)) {
482 length = strlen(line);
483 group = strtok(line, " ");
484 if (group == NULL)
485 bail("malformed script line");
486 if (group[0] == '[')
487 break;
488 type = string_to_group(group);
489 token = strtok(NULL, " ");
490 if (token == NULL)
491 bail("malformed action line");
492 if (strcmp(token, "=") != 0)
493 bail("malformed action line near %s", token);
494 token = strtok(NULL, "");
495 split_options(token, &work->options[type], config);
496 free(line);
497 }
498 if (line != NULL) {
499 free(line);
500 rewind_section(script, length);
501 }
502 }
503
504
505 /*
506 * Parse the call portion of a PAM call in the run section of a PAM script.
507 * This handles parsing the PAM flags that optionally may be given as part of
508 * the call. Takes the token representing the call and a pointer to the
509 * action struct to fill in with the call and the option flags.
510 */
511 static void
parse_call(char * token,struct action * action)512 parse_call(char *token, struct action *action)
513 {
514 char *flags, *flag;
515
516 action->flags = 0;
517 flags = strchr(token, '(');
518 if (flags != NULL) {
519 *flags = '\0';
520 flags++;
521 for (flag = strtok(flags, "|,)"); flag != NULL;
522 flag = strtok(NULL, "|,)")) {
523 action->flags |= string_to_flag(flag);
524 }
525 }
526 action->call = string_to_call(token, &action->group);
527 }
528
529
530 /*
531 * Parse the run section of a PAM script. This consists of one or more lines
532 * in the format:
533 *
534 * <call> = <status>
535 *
536 * where <call> is a PAM call and <status> is what it should return. Returns
537 * a linked list of actions. Fails on any error in parsing.
538 */
539 static struct action *
parse_run(FILE * script)540 parse_run(FILE *script)
541 {
542 struct action *head = NULL, *current = NULL, *next;
543 char *line, *token, *call;
544 size_t length = 0;
545
546 for (line = readline(script); line != NULL; line = readline(script)) {
547 length = strlen(line);
548 token = strtok(line, " ");
549 if (token[0] == '[')
550 break;
551 next = bmalloc(sizeof(struct action));
552 next->next = NULL;
553 if (head == NULL)
554 head = next;
555 else
556 current->next = next;
557 next->name = bstrdup(token);
558 call = token;
559 token = strtok(NULL, " ");
560 if (token == NULL)
561 bail("malformed action line");
562 if (strcmp(token, "=") != 0)
563 bail("malformed action line near %s", token);
564 token = strtok(NULL, " ");
565 next->status = string_to_status(token);
566 parse_call(call, next);
567 free(line);
568 current = next;
569 }
570 if (head == NULL)
571 bail("empty run section in script");
572 if (line != NULL) {
573 free(line);
574 rewind_section(script, length);
575 }
576 return head;
577 }
578
579
580 /*
581 * Parse the end section of a PAM script. There is one supported line in the
582 * format:
583 *
584 * flags = <flag>|<flag>
585 *
586 * where <flag> is a flag to pass to pam_end. Returns the flags.
587 */
588 static int
parse_end(FILE * script)589 parse_end(FILE *script)
590 {
591 char *line, *token, *flag;
592 size_t length = 0;
593 int flags = PAM_SUCCESS;
594
595 for (line = readline(script); line != NULL; line = readline(script)) {
596 length = strlen(line);
597 token = strtok(line, " ");
598 if (token[0] == '[')
599 break;
600 if (strcmp(token, "flags") != 0)
601 bail("unknown end setting %s", token);
602 token = strtok(NULL, " ");
603 if (token == NULL)
604 bail("malformed end line");
605 if (strcmp(token, "=") != 0)
606 bail("malformed end line near %s", token);
607 token = strtok(NULL, " ");
608 flag = strtok(token, "|");
609 while (flag != NULL) {
610 flags |= string_to_status(flag);
611 flag = strtok(NULL, "|");
612 }
613 free(line);
614 }
615 if (line != NULL) {
616 free(line);
617 rewind_section(script, length);
618 }
619 return flags;
620 }
621
622
623 /*
624 * Parse the output section of a PAM script. This consists of zero or more
625 * lines in the format:
626 *
627 * PRIORITY some output information
628 * PRIORITY /output regex/
629 *
630 * where PRIORITY is replaced by the numeric syslog priority corresponding to
631 * that priority and the rest of the output undergoes %-esacape expansion.
632 * Returns the accumulated output as a vector.
633 */
634 static struct output *
parse_output(FILE * script,const struct script_config * config)635 parse_output(FILE *script, const struct script_config *config)
636 {
637 char *line, *token, *message;
638 struct output *output;
639 int priority;
640
641 output = output_new();
642 if (output == NULL)
643 sysbail("cannot allocate vector");
644 for (line = readline(script); line != NULL; line = readline(script)) {
645 token = strtok(line, " ");
646 priority = string_to_priority(token);
647 token = strtok(NULL, "");
648 if (token == NULL)
649 bail("malformed line %s", line);
650 message = expand_string(token, config);
651 output_add(output, priority, message);
652 free(message);
653 free(line);
654 }
655 return output;
656 }
657
658
659 /*
660 * Parse the prompts section of a PAM script. This consists of zero or more
661 * lines in one of the formats:
662 *
663 * type = prompt
664 * type = /prompt/
665 * type = prompt|response
666 * type = /prompt/|response
667 *
668 * If the type is error_msg or info, there is no response. Otherwise,
669 * everything after the last | is taken to be the response that should be
670 * provided to that prompt. The response undergoes %-escape expansion.
671 */
672 static struct prompts *
parse_prompts(FILE * script,const struct script_config * config)673 parse_prompts(FILE *script, const struct script_config *config)
674 {
675 struct prompts *prompts = NULL;
676 struct prompt *prompt;
677 char *line, *token, *style, *end;
678 size_t size, count, i;
679 size_t length = 0;
680
681 for (line = readline(script); line != NULL; line = readline(script)) {
682 length = strlen(line);
683 token = strtok(line, " ");
684 if (token[0] == '[')
685 break;
686 if (prompts == NULL) {
687 prompts = bcalloc(1, sizeof(struct prompts));
688 prompts->prompts = bcalloc(1, sizeof(struct prompt));
689 prompts->allocated = 1;
690 } else if (prompts->allocated == prompts->size) {
691 count = prompts->allocated * 2;
692 size = sizeof(struct prompt);
693 prompts->prompts = breallocarray(prompts->prompts, count, size);
694 prompts->allocated = count;
695 for (i = prompts->size; i < prompts->allocated; i++) {
696 prompts->prompts[i].prompt = NULL;
697 prompts->prompts[i].response = NULL;
698 }
699 }
700 prompt = &prompts->prompts[prompts->size];
701 style = token;
702 token = strtok(NULL, " ");
703 if (token == NULL)
704 bail("malformed prompt line");
705 if (strcmp(token, "=") != 0)
706 bail("malformed prompt line near %s", token);
707 prompt->style = string_to_style(style);
708 token = strtok(NULL, "");
709 if (prompt->style == PAM_ERROR_MSG || prompt->style == PAM_TEXT_INFO)
710 prompt->prompt = expand_string(token, config);
711 else {
712 end = strrchr(token, '|');
713 if (end == NULL)
714 bail("malformed prompt line near %s", token);
715 *end = '\0';
716 prompt->prompt = expand_string(token, config);
717 token = end + 1;
718 prompt->response = expand_string(token, config);
719 }
720 prompts->size++;
721 free(line);
722 }
723 if (line != NULL) {
724 free(line);
725 rewind_section(script, length);
726 }
727 return prompts;
728 }
729
730
731 /*
732 * Parse a PAM interaction script. This handles parsing of the top-level
733 * section markers and dispatches the parsing to other functions. Returns the
734 * total work to do as a work struct.
735 */
736 struct work *
parse_script(FILE * script,const struct script_config * config)737 parse_script(FILE *script, const struct script_config *config)
738 {
739 struct work *work;
740 char *line, *token;
741
742 work = bmalloc(sizeof(struct work));
743 memset(work, 0, sizeof(struct work));
744 work->end_flags = PAM_SUCCESS;
745 for (line = readline(script); line != NULL; line = readline(script)) {
746 token = strtok(line, " ");
747 if (token[0] != '[')
748 bail("line outside of section: %s", line);
749 if (strcmp(token, "[options]") == 0)
750 parse_options(script, work, config);
751 else if (strcmp(token, "[run]") == 0)
752 work->actions = parse_run(script);
753 else if (strcmp(token, "[end]") == 0)
754 work->end_flags = parse_end(script);
755 else if (strcmp(token, "[output]") == 0)
756 work->output = parse_output(script, config);
757 else if (strcmp(token, "[prompts]") == 0)
758 work->prompts = parse_prompts(script, config);
759 else
760 bail("unknown section: %s", token);
761 free(line);
762 }
763 if (work->actions == NULL)
764 bail("no run section defined");
765 return work;
766 }
767