1/* $OpenBSD: parse.y,v 1.31 2022/03/22 20:36:49 deraadt Exp $ */ 2/* 3 * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org> 4 * 5 * Permission to use, copy, modify, and distribute this software for any 6 * purpose with or without fee is hereby granted, provided that the above 7 * copyright notice and this permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 18%{ 19#include <sys/types.h> 20#include <ctype.h> 21#include <limits.h> 22#include <unistd.h> 23#include <stdint.h> 24#include <stdarg.h> 25#include <stdio.h> 26#include <string.h> 27#include <err.h> 28 29#include "doas.h" 30 31typedef struct { 32 union { 33 struct { 34 int action; 35 int options; 36 const char *cmd; 37 const char **cmdargs; 38 const char **envlist; 39 }; 40 const char **strlist; 41 const char *str; 42 }; 43 unsigned long lineno; 44 unsigned long colno; 45} yystype; 46#define YYSTYPE yystype 47 48FILE *yyfp; 49 50struct rule **rules; 51size_t nrules; 52static size_t maxrules; 53 54int parse_error = 0; 55 56static void yyerror(const char *, ...); 57static int yylex(void); 58 59static size_t 60arraylen(const char **arr) 61{ 62 size_t cnt = 0; 63 64 while (*arr) { 65 cnt++; 66 arr++; 67 } 68 return cnt; 69} 70 71%} 72 73%token TPERMIT TDENY TAS TCMD TARGS 74%token TNOPASS TNOLOG TPERSIST TKEEPENV TSETENV 75%token TSTRING 76 77%% 78 79grammar: /* empty */ 80 | grammar '\n' 81 | grammar rule '\n' 82 | error '\n' 83 ; 84 85rule: action ident target cmd { 86 struct rule *r; 87 88 r = calloc(1, sizeof(*r)); 89 if (!r) 90 errx(1, "can't allocate rule"); 91 r->action = $1.action; 92 r->options = $1.options; 93 r->envlist = $1.envlist; 94 r->ident = $2.str; 95 r->target = $3.str; 96 r->cmd = $4.cmd; 97 r->cmdargs = $4.cmdargs; 98 if (nrules == maxrules) { 99 if (maxrules == 0) 100 maxrules = 32; 101 rules = reallocarray(rules, maxrules, 102 2 * sizeof(*rules)); 103 if (!rules) 104 errx(1, "can't allocate rules"); 105 maxrules *= 2; 106 } 107 rules[nrules++] = r; 108 } ; 109 110action: TPERMIT options { 111 $$.action = PERMIT; 112 $$.options = $2.options; 113 $$.envlist = $2.envlist; 114 } | TDENY { 115 $$.action = DENY; 116 $$.options = 0; 117 $$.envlist = NULL; 118 } ; 119 120options: /* none */ { 121 $$.options = 0; 122 $$.envlist = NULL; 123 } | options option { 124 $$.options = $1.options | $2.options; 125 $$.envlist = $1.envlist; 126 if (($$.options & (NOPASS|PERSIST)) == (NOPASS|PERSIST)) { 127 yyerror("can't combine nopass and persist"); 128 YYERROR; 129 } 130 if ($2.envlist) { 131 if ($$.envlist) { 132 yyerror("can't have two setenv sections"); 133 YYERROR; 134 } else 135 $$.envlist = $2.envlist; 136 } 137 } ; 138option: TNOPASS { 139 $$.options = NOPASS; 140 $$.envlist = NULL; 141 } | TNOLOG { 142 $$.options = NOLOG; 143 $$.envlist = NULL; 144 } | TPERSIST { 145 $$.options = PERSIST; 146 $$.envlist = NULL; 147 } | TKEEPENV { 148 $$.options = KEEPENV; 149 $$.envlist = NULL; 150 } | TSETENV '{' strlist '}' { 151 $$.options = 0; 152 $$.envlist = $3.strlist; 153 } ; 154 155strlist: /* empty */ { 156 if (!($$.strlist = calloc(1, sizeof(char *)))) 157 errx(1, "can't allocate strlist"); 158 } | strlist TSTRING { 159 int nstr = arraylen($1.strlist); 160 161 if (!($$.strlist = reallocarray($1.strlist, nstr + 2, 162 sizeof(char *)))) 163 errx(1, "can't allocate strlist"); 164 $$.strlist[nstr] = $2.str; 165 $$.strlist[nstr + 1] = NULL; 166 } ; 167 168 169ident: TSTRING { 170 $$.str = $1.str; 171 } ; 172 173target: /* optional */ { 174 $$.str = NULL; 175 } | TAS TSTRING { 176 $$.str = $2.str; 177 } ; 178 179cmd: /* optional */ { 180 $$.cmd = NULL; 181 $$.cmdargs = NULL; 182 } | TCMD TSTRING args { 183 $$.cmd = $2.str; 184 $$.cmdargs = $3.cmdargs; 185 } ; 186 187args: /* empty */ { 188 $$.cmdargs = NULL; 189 } | TARGS strlist { 190 $$.cmdargs = $2.strlist; 191 } ; 192 193%% 194 195void 196yyerror(const char *fmt, ...) 197{ 198 va_list va; 199 200 fprintf(stderr, "doas: "); 201 va_start(va, fmt); 202 vfprintf(stderr, fmt, va); 203 va_end(va); 204 fprintf(stderr, " at line %lu\n", yylval.lineno + 1); 205 parse_error = 1; 206} 207 208static struct keyword { 209 const char *word; 210 int token; 211} keywords[] = { 212 { "deny", TDENY }, 213 { "permit", TPERMIT }, 214 { "as", TAS }, 215 { "cmd", TCMD }, 216 { "args", TARGS }, 217 { "nopass", TNOPASS }, 218 { "nolog", TNOLOG }, 219 { "persist", TPERSIST }, 220 { "keepenv", TKEEPENV }, 221 { "setenv", TSETENV }, 222}; 223 224int 225yylex(void) 226{ 227 char buf[1024], *ebuf, *p, *str; 228 int c, quoted = 0, quotes = 0, qerr = 0, escape = 0, nonkw = 0; 229 unsigned long qpos = 0; 230 size_t i; 231 232 p = buf; 233 ebuf = buf + sizeof(buf); 234 235repeat: 236 /* skip whitespace first */ 237 for (c = getc(yyfp); c == ' ' || c == '\t'; c = getc(yyfp)) 238 yylval.colno++; 239 240 /* check for special one-character constructions */ 241 switch (c) { 242 case '\n': 243 yylval.colno = 0; 244 yylval.lineno++; 245 /* FALLTHROUGH */ 246 case '{': 247 case '}': 248 return c; 249 case '#': 250 /* skip comments; NUL is allowed; no continuation */ 251 while ((c = getc(yyfp)) != '\n') 252 if (c == EOF) 253 goto eof; 254 yylval.colno = 0; 255 yylval.lineno++; 256 return c; 257 case EOF: 258 goto eof; 259 } 260 261 /* parsing next word */ 262 for (;; c = getc(yyfp), yylval.colno++) { 263 switch (c) { 264 case '\0': 265 yyerror("unallowed character NUL in column %lu", 266 yylval.colno + 1); 267 escape = 0; 268 continue; 269 case '\\': 270 escape = !escape; 271 if (escape) 272 continue; 273 break; 274 case '\n': 275 if (quotes && !qerr) { 276 yyerror("unterminated quotes in column %lu", 277 qpos + 1); 278 qerr = 1; 279 } 280 if (escape) { 281 nonkw = 1; 282 escape = 0; 283 yylval.colno = ULONG_MAX; 284 yylval.lineno++; 285 continue; 286 } 287 goto eow; 288 case EOF: 289 if (escape) 290 yyerror("unterminated escape in column %lu", 291 yylval.colno); 292 if (quotes && !qerr) 293 yyerror("unterminated quotes in column %lu", 294 qpos + 1); 295 goto eow; 296 case '{': 297 case '}': 298 case '#': 299 case ' ': 300 case '\t': 301 if (!escape && !quotes) 302 goto eow; 303 break; 304 case '"': 305 if (!escape) { 306 quoted = 1; 307 quotes = !quotes; 308 if (quotes) { 309 nonkw = 1; 310 qerr = 0; 311 qpos = yylval.colno; 312 } 313 continue; 314 } 315 } 316 *p++ = c; 317 if (p == ebuf) { 318 yyerror("too long line"); 319 p = buf; 320 } 321 escape = 0; 322 } 323 324eow: 325 *p = 0; 326 if (c != EOF) 327 ungetc(c, yyfp); 328 if (p == buf) { 329 /* 330 * There could be a number of reasons for empty buffer, 331 * and we handle all of them here, to avoid cluttering 332 * the main loop. 333 */ 334 if (c == EOF) 335 goto eof; 336 else if (!quoted) /* accept, e.g., empty args: cmd foo args "" */ 337 goto repeat; 338 } 339 if (!nonkw) { 340 for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) { 341 if (strcmp(buf, keywords[i].word) == 0) 342 return keywords[i].token; 343 } 344 } 345 if ((str = strdup(buf)) == NULL) 346 err(1, "%s", __func__); 347 yylval.str = str; 348 return TSTRING; 349 350eof: 351 if (ferror(yyfp)) 352 yyerror("input error reading config"); 353 return 0; 354} 355