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