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
31 typedef 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
48 FILE *yyfp;
49
50 struct rule **rules;
51 size_t nrules;
52 static size_t maxrules;
53
54 int parse_error = 0;
55
56 static void yyerror(const char *, ...);
57 static int yylex(void);
58
59 static size_t
arraylen(const char ** arr)60 arraylen(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
79 grammar: /* empty */
80 | grammar '\n'
81 | grammar rule '\n'
82 | error '\n'
83 ;
84
85 rule: 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
110 action: 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
120 options: /* 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 } ;
138 option: 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
155 strlist: /* 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
169 ident: TSTRING {
170 $$.str = $1.str;
171 } ;
172
173 target: /* optional */ {
174 $$.str = NULL;
175 } | TAS TSTRING {
176 $$.str = $2.str;
177 } ;
178
179 cmd: /* optional */ {
180 $$.cmd = NULL;
181 $$.cmdargs = NULL;
182 } | TCMD TSTRING args {
183 $$.cmd = $2.str;
184 $$.cmdargs = $3.cmdargs;
185 } ;
186
187 args: /* empty */ {
188 $$.cmdargs = NULL;
189 } | TARGS strlist {
190 $$.cmdargs = $2.strlist;
191 } ;
192
193 %%
194
195 void
196 yyerror(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
208 static 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
224 int
yylex(void)225 yylex(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
235 repeat:
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
324 eow:
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
350 eof:
351 if (ferror(yyfp))
352 yyerror("input error reading config");
353 return 0;
354 }
355