xref: /openbsd/usr.bin/doas/parse.y (revision 097a140d)
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