xref: /freebsd/contrib/pam-krb5/tests/fakepam/config.c (revision 24e4dcf4ba5e9dedcf89efd358ea3e1fe5867020)
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 *
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 *
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
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
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
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
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
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
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
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 *
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
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
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
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 *
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
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 *
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 *
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 *
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