/* * Copyright (c) 2004-2005, 2007-2013 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #ifdef STDC_HEADERS # include # include #else # ifdef HAVE_STDLIB_H # include # endif #endif /* STDC_HEADERS */ #ifdef HAVE_STRING_H # include #endif /* HAVE_STRING_H */ #ifdef HAVE_STRINGS_H # include #endif /* HAVE_STRINGS_H */ #ifdef HAVE_UNISTD_H # include #endif /* HAVE_UNISTD_H */ #include #include #include #include "sudoers.h" #include "parse.h" #include "lbuf.h" #include /* Characters that must be quoted in sudoers */ #define SUDOERS_QUOTED ":\\,=#\"" /* sudoers nsswitch routines */ struct sudo_nss sudo_nss_file = { &sudo_nss_file, NULL, sudo_file_open, sudo_file_close, sudo_file_parse, sudo_file_setdefs, sudo_file_lookup, sudo_file_display_cmnd, sudo_file_display_defaults, sudo_file_display_bound_defaults, sudo_file_display_privs }; /* * Parser externs. */ extern FILE *sudoersin; extern char *errorfile; extern int errorlineno; extern bool parse_error; /* * Local prototypes. */ static int display_bound_defaults(int dtype, struct lbuf *lbuf); static void print_member(struct lbuf *lbuf, struct member *m, int alias_type); static void print_member2(struct lbuf *lbuf, struct member *m, const char *separator, int alias_type); int sudo_file_open(struct sudo_nss *nss) { debug_decl(sudo_file_open, SUDO_DEBUG_NSS) if (def_ignore_local_sudoers) debug_return_int(-1); nss->handle = open_sudoers(sudoers_file, false, NULL); debug_return_int(nss->handle ? 0 : -1); } int sudo_file_close(struct sudo_nss *nss) { debug_decl(sudo_file_close, SUDO_DEBUG_NSS) /* Free parser data structures and close sudoers file. */ init_parser(NULL, false); if (nss->handle != NULL) { fclose(nss->handle); nss->handle = NULL; sudoersin = NULL; } debug_return_int(0); } /* * Parse the specified sudoers file. */ int sudo_file_parse(struct sudo_nss *nss) { debug_decl(sudo_file_close, SUDO_DEBUG_NSS) if (nss->handle == NULL) debug_return_int(-1); init_parser(sudoers_file, false); sudoersin = nss->handle; if (sudoersparse() != 0 || parse_error) { if (errorlineno != -1) { log_warning(0, N_("parse error in %s near line %d"), errorfile, errorlineno); } else { log_warning(0, N_("parse error in %s"), errorfile); } debug_return_int(-1); } debug_return_int(0); } /* * Wrapper around update_defaults() for nsswitch code. */ int sudo_file_setdefs(struct sudo_nss *nss) { debug_decl(sudo_file_setdefs, SUDO_DEBUG_NSS) if (nss->handle == NULL) debug_return_int(-1); if (!update_defaults(SETDEF_GENERIC|SETDEF_HOST|SETDEF_USER)) debug_return_int(-1); debug_return_int(0); } /* * Look up the user in the parsed sudoers file and check to see if they are * allowed to run the specified command on this host as the target user. */ int sudo_file_lookup(struct sudo_nss *nss, int validated, int pwflag) { int match, host_match, runas_match, cmnd_match; struct cmndspec *cs; struct cmndtag *tags = NULL; struct privilege *priv; struct userspec *us; struct member *matching_user; debug_decl(sudo_file_lookup, SUDO_DEBUG_NSS) if (nss->handle == NULL) debug_return_int(validated); /* * Only check the actual command if pwflag is not set. * It is set for the "validate", "list" and "kill" pseudo-commands. * Always check the host and user. */ if (pwflag) { int nopass; enum def_tuple pwcheck; pwcheck = (pwflag == -1) ? never : sudo_defs_table[pwflag].sd_un.tuple; nopass = (pwcheck == all) ? true : false; if (list_pw == NULL) SET(validated, FLAG_NO_CHECK); CLR(validated, FLAG_NO_USER); CLR(validated, FLAG_NO_HOST); match = DENY; tq_foreach_fwd(&userspecs, us) { if (userlist_matches(sudo_user.pw, &us->users) != ALLOW) continue; tq_foreach_fwd(&us->privileges, priv) { if (hostlist_matches(&priv->hostlist) != ALLOW) continue; tq_foreach_fwd(&priv->cmndlist, cs) { /* Only check the command when listing another user. */ if (user_uid == 0 || list_pw == NULL || user_uid == list_pw->pw_uid || cmnd_matches(cs->cmnd) == ALLOW) match = ALLOW; if ((pwcheck == any && cs->tags.nopasswd == true) || (pwcheck == all && cs->tags.nopasswd != true)) nopass = cs->tags.nopasswd; } } } if (match == ALLOW || user_uid == 0) { /* User has an entry for this host. */ SET(validated, VALIDATE_OK); } else if (match == DENY) SET(validated, VALIDATE_NOT_OK); if (pwcheck == always && def_authenticate) SET(validated, FLAG_CHECK_USER); else if (pwcheck == never || nopass == true) def_authenticate = false; debug_return_int(validated); } /* Need to be runas user while stat'ing things. */ set_perms(PERM_RUNAS); match = UNSPEC; tq_foreach_rev(&userspecs, us) { if (userlist_matches(sudo_user.pw, &us->users) != ALLOW) continue; CLR(validated, FLAG_NO_USER); tq_foreach_rev(&us->privileges, priv) { host_match = hostlist_matches(&priv->hostlist); if (host_match == ALLOW) CLR(validated, FLAG_NO_HOST); else continue; tq_foreach_rev(&priv->cmndlist, cs) { matching_user = NULL; runas_match = runaslist_matches(&cs->runasuserlist, &cs->runasgrouplist, &matching_user, NULL); if (runas_match == ALLOW) { cmnd_match = cmnd_matches(cs->cmnd); if (cmnd_match != UNSPEC) { match = cmnd_match; tags = &cs->tags; #ifdef HAVE_SELINUX /* Set role and type if not specified on command line. */ if (user_role == NULL) user_role = cs->role ? estrdup(cs->role) : def_role; if (user_type == NULL) user_type = cs->type ? estrdup(cs->type) : def_type; #endif /* HAVE_SELINUX */ #ifdef HAVE_PRIV_SET /* Set Solaris privilege sets */ if (runas_privs == NULL) runas_privs = cs->privs ? estrdup(cs->privs) : def_privs; if (runas_limitprivs == NULL) runas_limitprivs = cs->limitprivs ? estrdup(cs->limitprivs) : def_limitprivs; #endif /* HAVE_PRIV_SET */ /* * If user is running command as himself, * set runas_pw = sudo_user.pw. * XXX - hack, want more general solution */ if (matching_user && matching_user->type == MYSELF) { sudo_pw_delref(runas_pw); sudo_pw_addref(sudo_user.pw); runas_pw = sudo_user.pw; } goto matched2; } } } } } matched2: if (match == ALLOW) { SET(validated, VALIDATE_OK); CLR(validated, VALIDATE_NOT_OK); if (tags != NULL) { if (tags->nopasswd != UNSPEC) def_authenticate = !tags->nopasswd; if (tags->noexec != UNSPEC) def_noexec = tags->noexec; if (tags->setenv != UNSPEC) def_setenv = tags->setenv; if (tags->log_input != UNSPEC) def_log_input = tags->log_input; if (tags->log_output != UNSPEC) def_log_output = tags->log_output; } } else if (match == DENY) { SET(validated, VALIDATE_NOT_OK); CLR(validated, VALIDATE_OK); if (tags != NULL && tags->nopasswd != UNSPEC) def_authenticate = !tags->nopasswd; } restore_perms(); debug_return_int(validated); } #define TAG_SET(tt) \ ((tt) != UNSPEC && (tt) != IMPLIED) #define TAG_CHANGED(t) \ (TAG_SET(cs->tags.t) && cs->tags.t != tags->t) static void sudo_file_append_cmnd(struct cmndspec *cs, struct cmndtag *tags, struct lbuf *lbuf) { debug_decl(sudo_file_append_cmnd, SUDO_DEBUG_NSS) #ifdef HAVE_PRIV_SET if (cs->privs) lbuf_append(lbuf, "PRIVS=\"%s\" ", cs->privs); if (cs->limitprivs) lbuf_append(lbuf, "LIMITPRIVS=\"%s\" ", cs->limitprivs); #endif /* HAVE_PRIV_SET */ #ifdef HAVE_SELINUX if (cs->role) lbuf_append(lbuf, "ROLE=%s ", cs->role); if (cs->type) lbuf_append(lbuf, "TYPE=%s ", cs->type); #endif /* HAVE_SELINUX */ if (TAG_CHANGED(setenv)) { lbuf_append(lbuf, cs->tags.setenv ? "SETENV: " : "NOSETENV: "); tags->setenv = cs->tags.setenv; } if (TAG_CHANGED(noexec)) { lbuf_append(lbuf, cs->tags.noexec ? "NOEXEC: " : "EXEC: "); tags->noexec = cs->tags.noexec; } if (TAG_CHANGED(nopasswd)) { lbuf_append(lbuf, cs->tags.nopasswd ? "NOPASSWD: " : "PASSWD: "); tags->nopasswd = cs->tags.nopasswd; } if (TAG_CHANGED(log_input)) { lbuf_append(lbuf, cs->tags.log_input ? "LOG_INPUT: " : "NOLOG_INPUT: "); tags->log_input = cs->tags.log_input; } if (TAG_CHANGED(log_output)) { lbuf_append(lbuf, cs->tags.log_output ? "LOG_OUTPUT: " : "NOLOG_OUTPUT: "); tags->log_output = cs->tags.log_output; } print_member(lbuf, cs->cmnd, CMNDALIAS); debug_return; } #define RUNAS_CHANGED(cs1, cs2) \ (cs1 == NULL || cs2 == NULL || \ cs1->runasuserlist.first != cs2->runasuserlist.first || \ cs1->runasuserlist.last != cs2->runasuserlist.last || \ cs1->runasgrouplist.first != cs2->runasgrouplist.first || \ cs1->runasgrouplist.last != cs2->runasgrouplist.last) static int sudo_file_display_priv_short(struct passwd *pw, struct userspec *us, struct lbuf *lbuf) { struct cmndspec *cs, *prev_cs; struct member *m; struct privilege *priv; struct cmndtag tags; int nfound = 0; debug_decl(sudo_file_display_priv_short, SUDO_DEBUG_NSS) /* gcc -Wuninitialized false positive */ tags.noexec = UNSPEC; tags.setenv = UNSPEC; tags.nopasswd = UNSPEC; tags.log_input = UNSPEC; tags.log_output = UNSPEC; tq_foreach_fwd(&us->privileges, priv) { if (hostlist_matches(&priv->hostlist) != ALLOW) continue; prev_cs = NULL; tq_foreach_fwd(&priv->cmndlist, cs) { if (RUNAS_CHANGED(cs, prev_cs)) { if (cs != tq_first(&priv->cmndlist)) lbuf_append(lbuf, "\n"); lbuf_append(lbuf, " ("); if (!tq_empty(&cs->runasuserlist)) { tq_foreach_fwd(&cs->runasuserlist, m) { if (m != tq_first(&cs->runasuserlist)) lbuf_append(lbuf, ", "); print_member(lbuf, m, RUNASALIAS); } } else if (tq_empty(&cs->runasgrouplist)) { lbuf_append(lbuf, "%s", def_runas_default); } else { lbuf_append(lbuf, "%s", pw->pw_name); } if (!tq_empty(&cs->runasgrouplist)) { lbuf_append(lbuf, " : "); tq_foreach_fwd(&cs->runasgrouplist, m) { if (m != tq_first(&cs->runasgrouplist)) lbuf_append(lbuf, ", "); print_member(lbuf, m, RUNASALIAS); } } lbuf_append(lbuf, ") "); tags.noexec = UNSPEC; tags.setenv = UNSPEC; tags.nopasswd = UNSPEC; tags.log_input = UNSPEC; tags.log_output = UNSPEC; } else if (cs != tq_first(&priv->cmndlist)) { lbuf_append(lbuf, ", "); } sudo_file_append_cmnd(cs, &tags, lbuf); prev_cs = cs; nfound++; } lbuf_append(lbuf, "\n"); } debug_return_int(nfound); } #define TAGS_CHANGED(ot, nt) \ ((TAG_SET((nt).setenv) && (nt).setenv != (ot).setenv) || \ (TAG_SET((nt).noexec) && (nt).noexec != (ot).noexec) || \ (TAG_SET((nt).nopasswd) && (nt).nopasswd != (ot).nopasswd) || \ (TAG_SET((nt).log_input) && (nt).log_input != (ot).log_input) || \ (TAG_SET((nt).log_output) && (nt).log_output != (ot).log_output)) /* * Compare the current cmndspec with the previous one to determine * whether we need to start a new long entry for "sudo -ll". * Returns true if we should start a new long entry, else false. */ static bool new_long_entry(struct cmndspec *cs, struct cmndspec *prev_cs) { if (prev_cs == NULL) return true; if (RUNAS_CHANGED(cs, prev_cs) || TAGS_CHANGED(cs->tags, prev_cs->tags)) return true; #ifdef HAVE_PRIV_SET if (cs->privs && (!prev_cs->privs || strcmp(cs->privs, prev_cs->privs) != 0)) return true; if (cs->limitprivs && (!prev_cs->limitprivs || strcmp(cs->limitprivs, prev_cs->limitprivs) != 0)) return true; #endif /* HAVE_PRIV_SET */ #ifdef HAVE_SELINUX if (cs->role && (!prev_cs->role || strcmp(cs->role, prev_cs->role) != 0)) return true; if (cs->type && (!prev_cs->type || strcmp(cs->type, prev_cs->type) != 0)) return true; #endif /* HAVE_SELINUX */ return false; } static int sudo_file_display_priv_long(struct passwd *pw, struct userspec *us, struct lbuf *lbuf) { struct cmndspec *cs, *prev_cs; struct member *m; struct privilege *priv; int nfound = 0, olen; debug_decl(sudo_file_display_priv_long, SUDO_DEBUG_NSS) tq_foreach_fwd(&us->privileges, priv) { if (hostlist_matches(&priv->hostlist) != ALLOW) continue; prev_cs = NULL; tq_foreach_fwd(&priv->cmndlist, cs) { if (new_long_entry(cs, prev_cs)) { lbuf_append(lbuf, _("\nSudoers entry:\n")); lbuf_append(lbuf, _(" RunAsUsers: ")); if (!tq_empty(&cs->runasuserlist)) { tq_foreach_fwd(&cs->runasuserlist, m) { if (m != tq_first(&cs->runasuserlist)) lbuf_append(lbuf, ", "); print_member(lbuf, m, RUNASALIAS); } } else if (tq_empty(&cs->runasgrouplist)) { lbuf_append(lbuf, "%s", def_runas_default); } else { lbuf_append(lbuf, "%s", pw->pw_name); } lbuf_append(lbuf, "\n"); if (!tq_empty(&cs->runasgrouplist)) { lbuf_append(lbuf, _(" RunAsGroups: ")); tq_foreach_fwd(&cs->runasgrouplist, m) { if (m != tq_first(&cs->runasgrouplist)) lbuf_append(lbuf, ", "); print_member(lbuf, m, RUNASALIAS); } lbuf_append(lbuf, "\n"); } olen = lbuf->len; lbuf_append(lbuf, _(" Options: ")); if (TAG_SET(cs->tags.setenv)) lbuf_append(lbuf, "%ssetenv, ", cs->tags.setenv ? "" : "!"); if (TAG_SET(cs->tags.noexec)) lbuf_append(lbuf, "%snoexec, ", cs->tags.noexec ? "" : "!"); if (TAG_SET(cs->tags.nopasswd)) lbuf_append(lbuf, "%sauthenticate, ", cs->tags.nopasswd ? "!" : ""); if (TAG_SET(cs->tags.log_input)) lbuf_append(lbuf, "%slog_input, ", cs->tags.log_input ? "" : "!"); if (TAG_SET(cs->tags.log_output)) lbuf_append(lbuf, "%slog_output, ", cs->tags.log_output ? "" : "!"); if (lbuf->buf[lbuf->len - 2] == ',') { lbuf->len -= 2; /* remove trailing ", " */ lbuf_append(lbuf, "\n"); } else { lbuf->len = olen; /* no options */ } #ifdef HAVE_PRIV_SET if (cs->privs) lbuf_append(lbuf, " Privs: %s\n", cs->privs); if (cs->limitprivs) lbuf_append(lbuf, " Limitprivs: %s\n", cs->limitprivs); #endif /* HAVE_PRIV_SET */ #ifdef HAVE_SELINUX if (cs->role) lbuf_append(lbuf, " Role: %s\n", cs->role); if (cs->type) lbuf_append(lbuf, " Type: %s\n", cs->type); #endif /* HAVE_SELINUX */ lbuf_append(lbuf, _(" Commands:\n")); } lbuf_append(lbuf, "\t"); print_member2(lbuf, cs->cmnd, "\n\t", CMNDALIAS); lbuf_append(lbuf, "\n"); prev_cs = cs; nfound++; } } debug_return_int(nfound); } int sudo_file_display_privs(struct sudo_nss *nss, struct passwd *pw, struct lbuf *lbuf) { struct userspec *us; int nfound = 0; debug_decl(sudo_file_display_priv, SUDO_DEBUG_NSS) if (nss->handle == NULL) goto done; tq_foreach_fwd(&userspecs, us) { if (userlist_matches(pw, &us->users) != ALLOW) continue; if (long_list) nfound += sudo_file_display_priv_long(pw, us, lbuf); else nfound += sudo_file_display_priv_short(pw, us, lbuf); } done: debug_return_int(nfound); } /* * Display matching Defaults entries for the given user on this host. */ int sudo_file_display_defaults(struct sudo_nss *nss, struct passwd *pw, struct lbuf *lbuf) { struct defaults *d; char *prefix; int nfound = 0; debug_decl(sudo_file_display_defaults, SUDO_DEBUG_NSS) if (nss->handle == NULL) goto done; if (lbuf->len == 0 || isspace((unsigned char)lbuf->buf[lbuf->len - 1])) prefix = " "; else prefix = ", "; tq_foreach_fwd(&defaults, d) { switch (d->type) { case DEFAULTS_HOST: if (hostlist_matches(&d->binding) != ALLOW) continue; break; case DEFAULTS_USER: if (userlist_matches(pw, &d->binding) != ALLOW) continue; break; case DEFAULTS_RUNAS: case DEFAULTS_CMND: continue; } if (d->val != NULL) { lbuf_append(lbuf, "%s%s%s", prefix, d->var, d->op == '+' ? "+=" : d->op == '-' ? "-=" : "="); if (strpbrk(d->val, " \t") != NULL) { lbuf_append(lbuf, "\""); lbuf_append_quoted(lbuf, "\"", "%s", d->val); lbuf_append(lbuf, "\""); } else lbuf_append_quoted(lbuf, SUDOERS_QUOTED, "%s", d->val); } else lbuf_append(lbuf, "%s%s%s", prefix, d->op == false ? "!" : "", d->var); prefix = ", "; nfound++; } done: debug_return_int(nfound); } /* * Display Defaults entries that are per-runas or per-command */ int sudo_file_display_bound_defaults(struct sudo_nss *nss, struct passwd *pw, struct lbuf *lbuf) { int nfound = 0; debug_decl(sudo_file_display_bound_defaults, SUDO_DEBUG_NSS) /* XXX - should only print ones that match what the user can do. */ nfound += display_bound_defaults(DEFAULTS_RUNAS, lbuf); nfound += display_bound_defaults(DEFAULTS_CMND, lbuf); debug_return_int(nfound); } /* * Display Defaults entries of the given type. */ static int display_bound_defaults(int dtype, struct lbuf *lbuf) { struct defaults *d; struct member *m, *binding = NULL; char *dsep; int atype, nfound = 0; debug_decl(display_bound_defaults, SUDO_DEBUG_NSS) switch (dtype) { case DEFAULTS_HOST: atype = HOSTALIAS; dsep = "@"; break; case DEFAULTS_USER: atype = USERALIAS; dsep = ":"; break; case DEFAULTS_RUNAS: atype = RUNASALIAS; dsep = ">"; break; case DEFAULTS_CMND: atype = CMNDALIAS; dsep = "!"; break; default: debug_return_int(-1); } tq_foreach_fwd(&defaults, d) { if (d->type != dtype) continue; nfound++; if (binding != tq_first(&d->binding)) { binding = tq_first(&d->binding); if (nfound != 1) lbuf_append(lbuf, "\n"); lbuf_append(lbuf, " Defaults%s", dsep); for (m = binding; m != NULL; m = m->next) { if (m != binding) lbuf_append(lbuf, ","); print_member(lbuf, m, atype); lbuf_append(lbuf, " "); } } else lbuf_append(lbuf, ", "); if (d->val != NULL) { lbuf_append(lbuf, "%s%s%s", d->var, d->op == '+' ? "+=" : d->op == '-' ? "-=" : "=", d->val); } else lbuf_append(lbuf, "%s%s", d->op == false ? "!" : "", d->var); } debug_return_int(nfound); } int sudo_file_display_cmnd(struct sudo_nss *nss, struct passwd *pw) { struct cmndspec *cs; struct member *match; struct privilege *priv; struct userspec *us; int rval = 1; int host_match, runas_match, cmnd_match; debug_decl(sudo_file_display_cmnd, SUDO_DEBUG_NSS) if (nss->handle == NULL) goto done; match = NULL; tq_foreach_rev(&userspecs, us) { if (userlist_matches(pw, &us->users) != ALLOW) continue; tq_foreach_rev(&us->privileges, priv) { host_match = hostlist_matches(&priv->hostlist); if (host_match != ALLOW) continue; tq_foreach_rev(&priv->cmndlist, cs) { runas_match = runaslist_matches(&cs->runasuserlist, &cs->runasgrouplist, NULL, NULL); if (runas_match == ALLOW) { cmnd_match = cmnd_matches(cs->cmnd); if (cmnd_match != UNSPEC) { match = host_match && runas_match ? cs->cmnd : NULL; goto matched; } } } } } matched: if (match != NULL && !match->negated) { sudo_printf(SUDO_CONV_INFO_MSG, "%s%s%s\n", safe_cmnd, user_args ? " " : "", user_args ? user_args : ""); rval = 0; } done: debug_return_int(rval); } /* * Print the contents of a struct member to stdout */ static void _print_member(struct lbuf *lbuf, char *name, int type, int negated, const char *separator, int alias_type) { struct alias *a; struct member *m; struct sudo_command *c; debug_decl(_print_member, SUDO_DEBUG_NSS) switch (type) { case ALL: lbuf_append(lbuf, "%sALL", negated ? "!" : ""); break; case MYSELF: lbuf_append(lbuf, "%s%s", negated ? "!" : "", user_name); break; case COMMAND: c = (struct sudo_command *) name; if (negated) lbuf_append(lbuf, "!"); lbuf_append_quoted(lbuf, SUDOERS_QUOTED, "%s", c->cmnd); if (c->args) { lbuf_append(lbuf, " "); lbuf_append_quoted(lbuf, SUDOERS_QUOTED, "%s", c->args); } break; case ALIAS: if ((a = alias_get(name, alias_type)) != NULL) { tq_foreach_fwd(&a->members, m) { if (m != tq_first(&a->members)) lbuf_append(lbuf, "%s", separator); _print_member(lbuf, m->name, m->type, negated ? !m->negated : m->negated, separator, alias_type); } alias_put(a); break; } /* FALLTHROUGH */ default: lbuf_append(lbuf, "%s%s", negated ? "!" : "", name); break; } debug_return; } static void print_member(struct lbuf *lbuf, struct member *m, int alias_type) { _print_member(lbuf, m->name, m->type, m->negated, ", ", alias_type); } static void print_member2(struct lbuf *lbuf, struct member *m, const char *separator, int alias_type) { _print_member(lbuf, m->name, m->type, m->negated, separator, alias_type); }