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