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