apparmor/parser/parser_regex.c

700 lines
16 KiB
C
Raw Normal View History

/* $Id$ */
/*
2007-04-11 08:12:51 +00:00
* Copyright (c) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007
* NOVELL (All rights reserved)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public
* License published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*/
#define _GNU_SOURCE /* for strndup(3) */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <libintl.h>
#define _(s) gettext(s)
/* #define DEBUG */
#include "parser.h"
2007-02-27 02:29:16 +00:00
#include "libapparmor_re/apparmor_re.h"
enum error_type {
e_no_error,
e_parse_error,
e_buffer_overflow
};
/* filter_escapes: scan input for any escape characters
* and remove, and reduce double \\ to a single
* NOTE: modifies in place the contents of the name argument */
static void filter_escapes(char *name)
{
char *sptr, *dptr;
BOOL bEscape = 0;
if (!name) /* shouldn't happen */
return;
sptr = dptr = name;
while (*sptr) {
if (*sptr == '\\') {
if (bEscape) {
*dptr++ = *sptr++;
} else {
++sptr;
bEscape = TRUE;
continue;
}
} else if (dptr < sptr) {
*dptr++ = *sptr++;
} else {
dptr++;
sptr++;
}
bEscape = 0;
}
*dptr = 0;
}
/* Filters out multiple slashes (except if the first two are slashes,
* that's a distinct namespace in linux) and trailing slashes.
* NOTE: modifies in place the contents of the path argument */
static void filter_slashes(char *path)
{
char *sptr, *dptr;
BOOL seen_slash = 0;
int len;
if (!path || (strlen(path) < 2))
return;
sptr = dptr = path;
/* special case for linux // namespace */
if (sptr[0] == '/' && sptr[1] == '/' && sptr[2] != '/') {
sptr += 2;
dptr += 2;
}
while (*sptr) {
if (*sptr == '/') {
if (seen_slash) {
++sptr;
} else {
*dptr++ = *sptr++;
seen_slash = TRUE;
}
} else {
seen_slash = 0;
if (dptr < sptr) {
*dptr++ = *sptr++;
} else {
dptr++;
sptr++;
}
}
}
*dptr = 0;
if (regex_type != AARE_DFA) {
/* eliminate trailing slashes for versions of apparmor that
* do not use the dfa engine.
* Versions of apparmor which use the dfa engine use the
* trailing / to differentiate between file and directory
* matches
*/
len = strlen(path);
if (len > 2 && path[len -1] == '/') {
path[len - 1] = 0;
}
}
}
2007-02-27 02:29:16 +00:00
static pattern_t convert_aaregex_to_pcre(const char *aare, int anchor,
char *pcre, size_t pcre_size)
{
#define STORE(_src, _dest, _len) \
2007-02-27 02:29:16 +00:00
if ((const char*)_dest + _len > (pcre + pcre_size)){ \
error = e_buffer_overflow; \
} else { \
memcpy(_dest, _src, _len); \
_dest += _len; \
}
int ret = TRUE;
/* flag to indicate input error */
enum error_type error;
const char *sptr;
char *dptr;
pattern_t ptype;
BOOL bEscape = 0; /* flag to indicate escape */
int ingrouping = 0; /* flag to indicate {} context */
int incharclass = 0; /* flag to indicate [ ] context */
error = e_no_error;
ptype = ePatternBasic; /* assume no regex */
2007-02-27 02:29:16 +00:00
sptr = aare;
dptr = pcre;
2007-02-27 02:29:16 +00:00
if (anchor)
/* anchor beginning of regular expression */
*dptr++ = '^';
while (error == e_no_error && *sptr) {
switch (*sptr) {
case '\\':
/* concurrent escapes are allowed now and
* output as two consecutive escapes so that
* pcre won't interpret them
* \\\{...\\\} will be emitted as \\\{...\\\}
* for pcre matching. For string matching
* and globbing only one escape is output
* this is done by stripping later
*/
if (bEscape) {
STORE("\\\\", dptr, 2);
} else {
bEscape = TRUE;
++sptr;
continue; /*skip turning bEscape off */
} /* bEscape */
2007-02-01 20:18:50 +00:00
break;
case '*':
if (bEscape) {
/* '*' is a PCRE special character */
/* We store an escaped *, in case we
* end up using this regex buffer (i.e another
* non-escaped regex follows)
*/
STORE("\\*", dptr, 2);
} else {
if ((dptr > pcre) && *(dptr - 1) == '/') {
#if 0
/* handle comment containing use
* of C comment characters
* /* /*/ and /** to describe paths
*
* modify what is emitted for * and **
* when used as the only path
* component
* ex.
* /* /*/ /**/ /**
* this prevents these expressions
* from matching directories or
* invalid paths
* in these case * and ** must
* match at least 1 character to
* get a valid path element.
* ex.
* /foo/* -> should not match /foo/
* /foo/*bar -> should match /foo/bar
* /*/foo -> should not match //foo
*/
#endif
const char *s = sptr;
while (*s == '*')
s++;
if (*s == '/' || !*s) {
STORE("[^/]", dptr, 4);
}
}
if (*(sptr + 1) == '*') {
/* is this the first regex form we
* have seen and also the end of
* pattern? If so, we can use
* optimised tail globbing rather
* than full regex.
*/
if (*(sptr + 2) == '\0' &&
ptype == ePatternBasic) {
ptype = ePatternTailGlob;
} else {
ptype = ePatternRegex;
}
STORE(".*", dptr, 2);
sptr++;
} else {
ptype = ePatternRegex;
STORE("[^/]*", dptr, 5);
} /* *(sptr+1) == '*' */
} /* bEscape */
break;
case '?':
if (bEscape) {
/* ? is not a PCRE regex character
* so no need to escape, just skip
* transform
*/
STORE(sptr, dptr, 1);
} else {
ptype = ePatternRegex;
STORE("[^/]", dptr, 4);
}
break;
case '[':
if (bEscape) {
/* [ is a PCRE special character */
STORE("\\[", dptr, 2);
} else {
incharclass = 1;
ptype = ePatternRegex;
STORE(sptr, dptr, 1);
}
break;
case ']':
if (bEscape) {
/* ] is a PCRE special character */
STORE("\\]", dptr, 2);
} else {
incharclass = 0;
STORE(sptr, dptr, 1);
}
break;
case '{':
if (bEscape) {
/* { is a PCRE special character */
STORE("\\{", dptr, 2);
} else {
if (ingrouping) {
error = e_parse_error;
PERROR(_("%s: Illegal open {, nesting groupings not allowed\n"),
progname);
} else {
ingrouping = 1;
ptype = ePatternRegex;
STORE("(", dptr, 1);
}
}
break;
case '}':
if (bEscape) {
/* { is a PCRE special character */
STORE("\\}", dptr, 2);
} else {
if (ingrouping <= 1) {
error = e_parse_error;
if (ingrouping == 1) {
PERROR(_("%s: Regex grouping error: Invalid number of items between {}\n"),
progname);
ingrouping = 0; /* prevent further errors */
} else { /* ingrouping == 0 */
PERROR(_("%s: Regex grouping error: Invalid close }, no matching open { detected\n"),
progname);
}
} else { /* ingrouping > 1 */
ingrouping = 0;
STORE(")", dptr, 1);
}
} /* bEscape */
break;
case ',':
if (bEscape) {
/* , is not a PCRE regex character
* so no need to escape, just skip
* transform
*/
STORE(sptr, dptr, 1);
} else {
if (ingrouping) {
++ingrouping;
STORE("|", dptr, 1);
} else {
STORE(sptr, dptr, 1);
}
}
break;
/* these are special outside of character
* classes but not in them */
case '^':
case '$':
if (incharclass) {
STORE(sptr, dptr, 1);
} else {
STORE("\\", dptr, 1);
STORE(sptr, dptr, 1);
}
break;
/*
* Not a subdomain regex, but needs to be
* escaped as it is a pcre metacharacter which
* we don't want to support. We always escape
* these, so no need to check bEscape
*/
case '.':
case '+':
case '|':
case '(':
case ')':
STORE("\\", dptr, 1);
// fall through to default
default:
STORE(sptr, dptr, 1);
break;
} /* switch (*sptr) */
bEscape = FALSE;
++sptr;
} /* while error == e_no_error && *sptr) */
if (ingrouping > 0 || incharclass) {
error = e_parse_error;
PERROR(_("%s: Regex grouping error: Unclosed grouping or character class, expecting close }\n"),
progname);
}
/* anchor end and terminate pattern string */
2007-02-27 02:29:16 +00:00
if ((error == e_no_error) && anchor) {
STORE("$" , dptr, 1);
}
if (error == e_no_error) {
2007-02-27 02:29:16 +00:00
STORE("", dptr, 1);
}
/* check error again, as above STORE may have set it */
if (error != e_no_error) {
if (error == e_buffer_overflow) {
PERROR(_("%s: Internal buffer overflow detected, %d characters exceeded\n"),
progname, PATH_MAX);
}
PERROR(_("%s: Unable to parse input line '%s'\n"),
2007-02-27 02:29:16 +00:00
progname, aare);
ret = FALSE;
goto out;
}
2007-02-27 02:29:16 +00:00
out:
if (ret == FALSE)
ptype = ePatternInvalid;
return ptype;
}
static int process_pcre_entry(struct cod_entry *entry)
{
char tbuf[PATH_MAX + 3]; /* +3 for ^, $ and \0 */
int ret = TRUE;
pattern_t ptype;
if (!entry) /* shouldn't happen */
return TRUE;
ptype = convert_aaregex_to_pcre(entry->name, 1, tbuf, PATH_MAX + 3);
if (ptype == ePatternInvalid)
return FALSE;
entry->pattern_type = ptype;
/*
* Only use buffer (tbuf) that we built above, if we
* identified a pattern requiring full regex support.
*/
if (ptype == ePatternRegex) {
int pattlen = strlen(tbuf);
if ((entry->pat.regex = malloc(pattlen + 1))) {
const char *errorreason;
int errpos;
strcpy(entry->pat.regex, tbuf);
if ((entry->pat.compiled =
pcre_compile(entry->pat.regex, 0,
&errorreason, &errpos,
NULL))) {
/* NULL out tables, kernel will use a
* private version
*/
entry->pat.compiled->tables = NULL;
} else {
int i;
PERROR(_("%s: Failed to compile regex '%s' [original: '%s']\n"),
progname, entry->pat.regex,
entry->name);
PERROR(_("%s: error near "),
progname);
for (i = 0; i < errpos; i++) {
fputc('.', stderr);
}
fputc('^', stderr);
fputc('\n', stderr);
PERROR(_("%s: error reason: '%s'\n"),
progname, errorreason);
free(entry->pat.regex);
entry->pat.regex = NULL;
ret = FALSE;
}
} else {
PERROR(_("%s: Failed to compile regex '%s' [original: '%s'] - malloc failed\n"),
progname, entry->pat.regex, entry->name);
ret = FALSE;
}
} else {
/* not a regex, scan input for any escape characters
* and remove, and reduce double \\ to a single */
filter_escapes(entry->name);
} /* ptype == ePatternRegex */
return ret;
}
2007-02-27 02:29:16 +00:00
static int process_dfa_entry(aare_ruleset_t *dfarules, struct cod_entry *entry)
{
char tbuf[PATH_MAX + 3]; /* +3 for ^, $ and \0 */
int ret = TRUE;
pattern_t ptype;
if (!entry) /* shouldn't happen */
return TRUE;
ptype = convert_aaregex_to_pcre(entry->name, 0, tbuf, PATH_MAX + 3);
if (ptype == ePatternInvalid)
return FALSE;
entry->pattern_type = ptype;
/* ix implies m but the apparmor module does not add m bit to
* dfa states like it does for pcre
*/
2007-11-16 09:35:57 +00:00
if (((entry->mode >> AA_OTHER_SHIFT) & AA_EXEC_MODIFIERS) ==
AA_EXEC_INHERIT)
entry->mode |= AA_EXEC_MMAP << AA_OTHER_SHIFT;
if (((entry->mode >> AA_GROUP_SHIFT) & AA_EXEC_MODIFIERS) ==
AA_EXEC_INHERIT)
entry->mode |= AA_EXEC_MMAP << AA_GROUP_SHIFT;
if (((entry->mode >> AA_USER_SHIFT) & AA_EXEC_MODIFIERS) ==
AA_EXEC_INHERIT)
entry->mode |= AA_EXEC_MMAP << AA_USER_SHIFT;
2007-02-27 02:29:16 +00:00
if (!aare_add_rule(dfarules, tbuf, entry->mode))
ret = FALSE;
if (entry->mode & AA_CHANGE_PROFILE) {
char lbuf[2*PATH_MAX + 8];
if (entry->namespace)
sprintf(lbuf, "%s//%s", entry->namespace, entry->name);
else
sprintf(lbuf, "%s", entry->namespace, entry->name);
ptype = convert_aaregex_to_pcre(lbuf, 0, tbuf, 2*PATH_MAX + 8);
if (ptype == ePatternInvalid)
return FALSE;
if (!aare_add_rule(dfarules, tbuf, AA_CHANGE_PROFILE))
return FALSE;
}
2007-02-27 02:29:16 +00:00
return ret;
}
int post_process_entries(struct codomain *cod)
{
int ret = TRUE, rc;
struct cod_entry *entry;
2007-02-27 02:29:16 +00:00
int count = 0;
2007-02-27 02:29:16 +00:00
list_for_each(cod->entries, entry) {
filter_slashes(entry->name);
2007-02-27 02:29:16 +00:00
if (regex_type == AARE_DFA)
rc = process_dfa_entry(cod->dfarules, entry);
else
rc = process_pcre_entry(entry);
if (!rc)
ret = FALSE;
2007-02-27 02:29:16 +00:00
count++;
}
2007-02-27 02:29:16 +00:00
cod->dfarule_count = count;
return ret;
}
int process_regex(struct codomain *cod)
{
2007-02-27 02:29:16 +00:00
int error = -1;
2007-02-27 02:29:16 +00:00
if (regex_type == AARE_DFA) {
cod->dfarules = aare_new_ruleset(0);
if (!cod->dfarules)
goto out;
}
2007-02-27 02:29:16 +00:00
if (!post_process_entries(cod))
goto out;
2007-02-27 02:29:16 +00:00
if (regex_type == AARE_DFA && cod->dfarule_count > 0) {
cod->dfa = aare_create_dfa(cod->dfarules, 0, &cod->dfa_size);
if (!cod->dfa)
goto out;
/*
if (cod->dfa_size == 0) {
PERROR(_("profile %s: has merged rules (%s) with "
"multiple x modifiers\n"),
cod->name, (char *) cod->dfa);
free(cod->dfa);
cod->dfa = NULL;
goto out;
}
*/
}
/*
* Post process subdomain(s):
*
* They are chained from the toplevel subdomain pointer
* thru each <codomain> next pointer.
* i.e first subdomain is list->subdomain
* second subdomain is list->subdomain->next
*
* N.B sub-subdomains are not valid so:
* if (list->subdomain) {
* assert(list->subdomain->subdomain == NULL)
* }
*/
if (process_hat_regex(cod) != 0)
2007-02-27 02:29:16 +00:00
goto out;
2007-02-01 20:18:50 +00:00
2007-02-27 02:29:16 +00:00
error = 0;
out:
return error;
}
#ifdef UNIT_TEST
#define MY_TEST(statement, error) \
if (!(statement)) { \
PERROR("FAIL: %s\n", error); \
rc = 1; \
}
/* Guh, fake routine */
void yyerror(char *msg, ...)
{
va_list arg;
char buf[PATH_MAX];
va_start(arg, msg);
vsnprintf(buf, sizeof(buf), msg, arg);
va_end(arg);
PERROR(_("AppArmor parser error: %s\n"), buf);
exit(1);
}
/* Guh, fake symbol */
char *progname;
static int test_filter_escapes(void)
{
int rc = 0;
char *test_string;
test_string = strdup("foo\\\\foo");
filter_escapes(test_string);
MY_TEST(strcmp(test_string, "foo\\foo") == 0, "simple filter for \\\\");
test_string = strdup("foo\\foo");
filter_escapes(test_string);
MY_TEST(strcmp(test_string, "foofoo") == 0, "simple filter for \\f");
return rc;
}
static int test_filter_slashes(void)
{
int rc = 0;
char *test_string;
test_string = strdup("///foo//////f//oo////////////////");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "/foo/f/oo") == 0, "simple tests");
test_string = strdup("/foo/f/oo");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "/foo/f/oo") == 0, "simple test for no changes");
test_string = strdup("/");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "/") == 0, "simple test for '/'");
test_string = strdup("");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "") == 0, "simple test for ''");
test_string = strdup("//usr");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "//usr") == 0, "simple test for // namespace");
test_string = strdup("//");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "//") == 0, "simple test 2 for // namespace");
test_string = strdup("///usr");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "/usr") == 0, "simple test for ///usr");
test_string = strdup("///");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "/") == 0, "simple test for ///");
test_string = strdup("/a/");
filter_slashes(test_string);
MY_TEST(strcmp(test_string, "/a") == 0, "simple test for /a/");
return rc;
}
int regex_type = AARE_PCRE;
int main(void)
{
int rc = 0;
int retval;
retval = test_filter_escapes();
if (retval != 0)
rc = retval;
retval = test_filter_slashes();
if (retval != 0)
rc = retval;
return rc;
}
#endif /* UNIT_TEST */