xref: /openbsd/usr.bin/doas/parse.y (revision 905646f0)
1 /* $OpenBSD: parse.y,v 1.28 2020/10/09 07:43:38 kn 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 int nrules;
51 static int 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 = 63;
99 				else
100 					maxrules *= 2;
101 				if (!(rules = reallocarray(rules, maxrules,
102 				    sizeof(*rules))))
103 					errx(1, "can't allocate rules");
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 i, c, quotes = 0, escape = 0, qpos = -1, nonkw = 0;
226 
227 	p = buf;
228 	ebuf = buf + sizeof(buf);
229 
230 repeat:
231 	/* skip whitespace first */
232 	for (c = getc(yyfp); c == ' ' || c == '\t'; c = getc(yyfp))
233 		yylval.colno++;
234 
235 	/* check for special one-character constructions */
236 	switch (c) {
237 		case '\n':
238 			yylval.colno = 0;
239 			yylval.lineno++;
240 			/* FALLTHROUGH */
241 		case '{':
242 		case '}':
243 			return c;
244 		case '#':
245 			/* skip comments; NUL is allowed; no continuation */
246 			while ((c = getc(yyfp)) != '\n')
247 				if (c == EOF)
248 					goto eof;
249 			yylval.colno = 0;
250 			yylval.lineno++;
251 			return c;
252 		case EOF:
253 			goto eof;
254 	}
255 
256 	/* parsing next word */
257 	for (;; c = getc(yyfp), yylval.colno++) {
258 		switch (c) {
259 		case '\0':
260 			yyerror("unallowed character NUL in column %d",
261 			    yylval.colno + 1);
262 			escape = 0;
263 			continue;
264 		case '\\':
265 			escape = !escape;
266 			if (escape)
267 				continue;
268 			break;
269 		case '\n':
270 			if (quotes)
271 				yyerror("unterminated quotes in column %d",
272 				    qpos + 1);
273 			if (escape) {
274 				nonkw = 1;
275 				escape = 0;
276 				yylval.colno = 0;
277 				yylval.lineno++;
278 				continue;
279 			}
280 			goto eow;
281 		case EOF:
282 			if (escape)
283 				yyerror("unterminated escape in column %d",
284 				    yylval.colno);
285 			if (quotes)
286 				yyerror("unterminated quotes in column %d",
287 				    qpos + 1);
288 			goto eow;
289 			/* FALLTHROUGH */
290 		case '{':
291 		case '}':
292 		case '#':
293 		case ' ':
294 		case '\t':
295 			if (!escape && !quotes)
296 				goto eow;
297 			break;
298 		case '"':
299 			if (!escape) {
300 				quotes = !quotes;
301 				if (quotes) {
302 					nonkw = 1;
303 					qpos = yylval.colno;
304 				}
305 				continue;
306 			}
307 		}
308 		*p++ = c;
309 		if (p == ebuf) {
310 			yyerror("too long line");
311 			p = buf;
312 		}
313 		escape = 0;
314 	}
315 
316 eow:
317 	*p = 0;
318 	if (c != EOF)
319 		ungetc(c, yyfp);
320 	if (p == buf) {
321 		/*
322 		 * There could be a number of reasons for empty buffer,
323 		 * and we handle all of them here, to avoid cluttering
324 		 * the main loop.
325 		 */
326 		if (c == EOF)
327 			goto eof;
328 		else if (qpos == -1)    /* accept, e.g., empty args: cmd foo args "" */
329 			goto repeat;
330 	}
331 	if (!nonkw) {
332 		for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
333 			if (strcmp(buf, keywords[i].word) == 0)
334 				return keywords[i].token;
335 		}
336 	}
337 	if ((str = strdup(buf)) == NULL)
338 		err(1, "%s", __func__);
339 	yylval.str = str;
340 	return TSTRING;
341 
342 eof:
343 	if (ferror(yyfp))
344 		yyerror("input error reading config");
345 	return 0;
346 }
347