root/usr.bin/doas/parse.y
/* $OpenBSD: parse.y,v 1.31 2022/03/22 20:36:49 deraadt Exp $ */
/*
 * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org>
 *
 * 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.
 */

%{
#include <sys/types.h>
#include <ctype.h>
#include <limits.h>
#include <unistd.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <err.h>

#include "doas.h"

typedef struct {
        union {
                struct {
                        int action;
                        int options;
                        const char *cmd;
                        const char **cmdargs;
                        const char **envlist;
                };
                const char **strlist;
                const char *str;
        };
        unsigned long lineno;
        unsigned long colno;
} yystype;
#define YYSTYPE yystype

FILE *yyfp;

struct rule **rules;
size_t nrules;
static size_t maxrules;

int parse_error = 0;

static void yyerror(const char *, ...);
static int yylex(void);

static size_t
arraylen(const char **arr)
{
        size_t cnt = 0;

        while (*arr) {
                cnt++;
                arr++;
        }
        return cnt;
}

%}

%token TPERMIT TDENY TAS TCMD TARGS
%token TNOPASS TNOLOG TPERSIST TKEEPENV TSETENV
%token TSTRING

%%

grammar:        /* empty */
                | grammar '\n'
                | grammar rule '\n'
                | error '\n'
                ;

rule:           action ident target cmd {
                        struct rule *r;

                        r = calloc(1, sizeof(*r));
                        if (!r)
                                errx(1, "can't allocate rule");
                        r->action = $1.action;
                        r->options = $1.options;
                        r->envlist = $1.envlist;
                        r->ident = $2.str;
                        r->target = $3.str;
                        r->cmd = $4.cmd;
                        r->cmdargs = $4.cmdargs;
                        if (nrules == maxrules) {
                                if (maxrules == 0)
                                        maxrules = 32;
                                rules = reallocarray(rules, maxrules,
                                    2 * sizeof(*rules));
                                if (!rules)
                                        errx(1, "can't allocate rules");
                                maxrules *= 2;
                        }
                        rules[nrules++] = r;
                } ;

action:         TPERMIT options {
                        $$.action = PERMIT;
                        $$.options = $2.options;
                        $$.envlist = $2.envlist;
                } | TDENY {
                        $$.action = DENY;
                        $$.options = 0;
                        $$.envlist = NULL;
                } ;

options:        /* none */ {
                        $$.options = 0;
                        $$.envlist = NULL;
                } | options option {
                        $$.options = $1.options | $2.options;
                        $$.envlist = $1.envlist;
                        if (($$.options & (NOPASS|PERSIST)) == (NOPASS|PERSIST)) {
                                yyerror("can't combine nopass and persist");
                                YYERROR;
                        }
                        if ($2.envlist) {
                                if ($$.envlist) {
                                        yyerror("can't have two setenv sections");
                                        YYERROR;
                                } else
                                        $$.envlist = $2.envlist;
                        }
                } ;
option:         TNOPASS {
                        $$.options = NOPASS;
                        $$.envlist = NULL;
                } | TNOLOG {
                        $$.options = NOLOG;
                        $$.envlist = NULL;
                } | TPERSIST {
                        $$.options = PERSIST;
                        $$.envlist = NULL;
                } | TKEEPENV {
                        $$.options = KEEPENV;
                        $$.envlist = NULL;
                } | TSETENV '{' strlist '}' {
                        $$.options = 0;
                        $$.envlist = $3.strlist;
                } ;

strlist:        /* empty */ {
                        if (!($$.strlist = calloc(1, sizeof(char *))))
                                errx(1, "can't allocate strlist");
                } | strlist TSTRING {
                        int nstr = arraylen($1.strlist);

                        if (!($$.strlist = reallocarray($1.strlist, nstr + 2,
                            sizeof(char *))))
                                errx(1, "can't allocate strlist");
                        $$.strlist[nstr] = $2.str;
                        $$.strlist[nstr + 1] = NULL;
                } ;


ident:          TSTRING {
                        $$.str = $1.str;
                } ;

target:         /* optional */ {
                        $$.str = NULL;
                } | TAS TSTRING {
                        $$.str = $2.str;
                } ;

cmd:            /* optional */ {
                        $$.cmd = NULL;
                        $$.cmdargs = NULL;
                } | TCMD TSTRING args {
                        $$.cmd = $2.str;
                        $$.cmdargs = $3.cmdargs;
                } ;

args:           /* empty */ {
                        $$.cmdargs = NULL;
                } | TARGS strlist {
                        $$.cmdargs = $2.strlist;
                } ;

%%

void
yyerror(const char *fmt, ...)
{
        va_list va;

        fprintf(stderr, "doas: ");
        va_start(va, fmt);
        vfprintf(stderr, fmt, va);
        va_end(va);
        fprintf(stderr, " at line %lu\n", yylval.lineno + 1);
        parse_error = 1;
}

static struct keyword {
        const char *word;
        int token;
} keywords[] = {
        { "deny", TDENY },
        { "permit", TPERMIT },
        { "as", TAS },
        { "cmd", TCMD },
        { "args", TARGS },
        { "nopass", TNOPASS },
        { "nolog", TNOLOG },
        { "persist", TPERSIST },
        { "keepenv", TKEEPENV },
        { "setenv", TSETENV },
};

int
yylex(void)
{
        char buf[1024], *ebuf, *p, *str;
        int c, quoted = 0, quotes = 0, qerr = 0, escape = 0, nonkw = 0;
        unsigned long qpos = 0;
        size_t i;

        p = buf;
        ebuf = buf + sizeof(buf);

repeat:
        /* skip whitespace first */
        for (c = getc(yyfp); c == ' ' || c == '\t'; c = getc(yyfp))
                yylval.colno++;

        /* check for special one-character constructions */
        switch (c) {
                case '\n':
                        yylval.colno = 0;
                        yylval.lineno++;
                        /* FALLTHROUGH */
                case '{':
                case '}':
                        return c;
                case '#':
                        /* skip comments; NUL is allowed; no continuation */
                        while ((c = getc(yyfp)) != '\n')
                                if (c == EOF)
                                        goto eof;
                        yylval.colno = 0;
                        yylval.lineno++;
                        return c;
                case EOF:
                        goto eof;
        }

        /* parsing next word */
        for (;; c = getc(yyfp), yylval.colno++) {
                switch (c) {
                case '\0':
                        yyerror("unallowed character NUL in column %lu",
                            yylval.colno + 1);
                        escape = 0;
                        continue;
                case '\\':
                        escape = !escape;
                        if (escape)
                                continue;
                        break;
                case '\n':
                        if (quotes && !qerr) {
                                yyerror("unterminated quotes in column %lu",
                                    qpos + 1);
                                qerr = 1;
                        }
                        if (escape) {
                                nonkw = 1;
                                escape = 0;
                                yylval.colno = ULONG_MAX;
                                yylval.lineno++;
                                continue;
                        }
                        goto eow;
                case EOF:
                        if (escape)
                                yyerror("unterminated escape in column %lu",
                                    yylval.colno);
                        if (quotes && !qerr)
                                yyerror("unterminated quotes in column %lu",
                                    qpos + 1);
                        goto eow;
                case '{':
                case '}':
                case '#':
                case ' ':
                case '\t':
                        if (!escape && !quotes)
                                goto eow;
                        break;
                case '"':
                        if (!escape) {
                                quoted = 1;
                                quotes = !quotes;
                                if (quotes) {
                                        nonkw = 1;
                                        qerr = 0;
                                        qpos = yylval.colno;
                                }
                                continue;
                        }
                }
                *p++ = c;
                if (p == ebuf) {
                        yyerror("too long line");
                        p = buf;
                }
                escape = 0;
        }

eow:
        *p = 0;
        if (c != EOF)
                ungetc(c, yyfp);
        if (p == buf) {
                /*
                 * There could be a number of reasons for empty buffer,
                 * and we handle all of them here, to avoid cluttering
                 * the main loop.
                 */
                if (c == EOF)
                        goto eof;
                else if (!quoted)    /* accept, e.g., empty args: cmd foo args "" */
                        goto repeat;
        }
        if (!nonkw) {
                for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
                        if (strcmp(buf, keywords[i].word) == 0)
                                return keywords[i].token;
                }
        }
        if ((str = strdup(buf)) == NULL)
                err(1, "%s", __func__);
        yylval.str = str;
        return TSTRING;

eof:
        if (ferror(yyfp))
                yyerror("input error reading config");
        return 0;
}