diff --git a/parser/Makefile b/parser/Makefile index 9a18f4da0..2d40b06f5 100644 --- a/parser/Makefile +++ b/parser/Makefile @@ -30,7 +30,7 @@ SYSTEMD_UNIT_DIR=${DESTDIR}/usr/lib/systemd/system CONFDIR=/etc/apparmor INSTALL_CONFDIR=${DESTDIR}${CONFDIR} LOCALEDIR=/usr/share/locale -MANPAGES=apparmor.d.5 apparmor.7 apparmor_parser.8 aa-teardown.8 +MANPAGES=apparmor.d.5 apparmor.7 apparmor_parser.8 aa-teardown.8 apparmor_xattrs.7 YACC := bison YFLAGS := -d diff --git a/parser/apparmor.d.pod b/parser/apparmor.d.pod index c8b939557..cf1b44d1b 100644 --- a/parser/apparmor.d.pod +++ b/parser/apparmor.d.pod @@ -66,7 +66,7 @@ B = '#' I [ '\r' ] '\n' B = any characters -B = ( I ) [ I ] [ I ] '{' ( I )* '}' +B = ( I ) [ I ] [ I ] [ I ] '{' ( I )* '}' B = [ 'profile' ] I | 'profile' I @@ -78,6 +78,12 @@ B = (must start with alphanumeric character (after variab B = I +B = [ 'xattrs=' ] '(' comma or white space separated list of I ')' + +B = extended attribute name '=' I + +B = I + B = [ 'flags=' ] '(' comma or white space separated list of I ')' B = 'complain' | 'audit' | 'enforce' | 'mediate_deleted' | 'attach_disconnected' | 'chroot_relative' @@ -1371,6 +1377,18 @@ Directories anywhere underneath F. =back +=head2 Extended Attributes + +AppArmor profiles have the ability to target files based on their xattr(7) +values in addition to their path. For example, the following profile matches +files in /usr/bin with the attribute "security.apparmor" and value "trusted": + + /usr/bin/* xattrs(security.apparmor="trusted") { + # ... + } + +See apparmor_xattrs(7) for further details. + =head2 Rule Qualifiers There are several rule qualifiers that can be applied to permission rules. @@ -1609,7 +1627,7 @@ negative values match when specifying one or the other. Eg, 'rw' matches when =head1 SEE ALSO -apparmor(7), apparmor_parser(8), aa-complain(1), +apparmor(7), apparmor_parser(8), apprmor_xattrs(7), aa-complain(1), aa-enforce(1), aa_change_hat(2), mod_apparmor(5), and L. diff --git a/parser/apparmor_xattrs.pod b/parser/apparmor_xattrs.pod new file mode 100644 index 000000000..39c4fcad4 --- /dev/null +++ b/parser/apparmor_xattrs.pod @@ -0,0 +1,108 @@ +# ---------------------------------------------------------------------- +# Copyright (c) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, +# 2008, 2009 +# NOVELL (All rights reserved) +# +# Copyright (c) 2010 +# Canonical Ltd. (All rights reserved) +# +# Copyright (c) 2013 +# Christian Boltz (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. +# ---------------------------------------------------------------------- + + +=pod + +=head1 NAME + +AppArmor profile xattr(7) matching + +=head1 DESCRIPTION + +AppArmor profiles can conditionally match files based on the presence and value +of extended attributes in addition to file path. The following profile applies +to any file under "/usr/bin" where the "security.apparmor" extended attribute +has the value "trusted": + + profile trusted /usr/bin/* xattrs=(security.apparmor="trusted") { + # ... + } + +Note that "security.apparmor" and "trusted" are arbitrary, and profiles can +match based on the value of any attribute. + +The xattrs value may also contain a path regex: + + profile trusted /usr/bin/* xattrs=(user.trust="tier/*") { + + # ... + } + +The getfattr(1) and setfattr(1) tools can be used to view and manage xattr +values: + + $ setfattr -n 'security.apparmor' -v 'trusted' /usr/bin/example-tool + $ getfattr --absolute-names -d -m - /usr/bin/example-tool + # file: usr/bin/example-tool + security.apparmor="trusted" + +The priority of each profile is determined by the length of the path, then the +number of xattrs specified. A more specific path is preferred over xattr +matches: + + # Highest priority, longest path. + profile example1 /usr/bin/example-tool { + # ... + } + + # Lower priority than the longer path, but higher priority than a rule + # with fewer xattr matches. + profile example2 /usr/** xattrs=( + security.apparmor="trusted" + user.domain="**" + ) { + # ... + } + + # Lowest priority. Same path length as the second profile, but has + # fewer xattr matches. + profile example2 /usr/** { + # ... + } + +xattr matching requires the following kernel feature: + + /sys/kernel/security/apparmor/features/domain/attach_conditions/xattr + +=head1 KNOWN ISSUES + +AppArmor profiles currently can't reliably match extended attributes with +binary values such as security.evm and security.ima. In the future AppArmor may +gain the ability to match based on the presence of certain attributes while +ignoring their values. + +=head1 SEE ALSO + +apparmor(8), +apparmor_parser(8), +apparmor.d(5), +xattr(7), +aa-autodep(1), clean(1), +auditd(8), +getfattr(1), +setfattr(1), +and L. + +=cut diff --git a/parser/libapparmor_re/aare_rules.cc b/parser/libapparmor_re/aare_rules.cc index fd719db48..f5be7418f 100644 --- a/parser/libapparmor_re/aare_rules.cc +++ b/parser/libapparmor_re/aare_rules.cc @@ -124,6 +124,44 @@ bool aare_rules::add_rule_vec(int deny, uint32_t perms, uint32_t audit, return true; } +/* + * append_rule is like add_rule, but appends the rule to any existing rules + * with a null transition. The appended rule matches with the same permissions + * as the rule it's appended to. + * + * This is used by xattrs matching where, after matching the path, the DFA is + * advanced by a null character for each xattr. + */ +bool aare_rules::append_rule(const char *rule, dfaflags_t flags) +{ + Node *tree = NULL; + if (regex_parse(&tree, rule)) + return false; + + if (flags & DFA_DUMP_RULE_EXPR) { + cerr << "rule: "; + cerr << rule; + cerr << " -> "; + tree->dump(cerr); + cerr << "\n\n"; + } + + /* + * For each matching state, we want to create an optional path + * separated by a null character. + * + * When matching xattrs, the DFA must end up in an accepting state for + * the path, then each value of the xattrs. Using an optional node + * lets each rule end up in an accepting state. + */ + tree = new OptionalNode(new CatNode(new CharNode(0), tree)); + PermExprMap::iterator it; + for (it = expr_map.begin(); it != expr_map.end(); it++) { + expr_map[it->first] = new CatNode(it->second, tree); + } + return true; +} + /* create a dfa from the ruleset * returns: buffer contain dfa tables, @size set to the size of the tables * else NULL on failure, @min_match_len set to the shortest string diff --git a/parser/libapparmor_re/aare_rules.h b/parser/libapparmor_re/aare_rules.h index 3cdfa0963..ac0fba5d3 100644 --- a/parser/libapparmor_re/aare_rules.h +++ b/parser/libapparmor_re/aare_rules.h @@ -104,6 +104,7 @@ class aare_rules { uint32_t audit, dfaflags_t flags); bool add_rule_vec(int deny, uint32_t perms, uint32_t audit, int count, const char **rulev, dfaflags_t flags); + bool append_rule(const char *rule, dfaflags_t flags); void *create_dfa(size_t *size, int *min_match_len, dfaflags_t flags); }; diff --git a/parser/libapparmor_re/expr-tree.cc b/parser/libapparmor_re/expr-tree.cc index 69c24a072..94761703f 100644 --- a/parser/libapparmor_re/expr-tree.cc +++ b/parser/libapparmor_re/expr-tree.cc @@ -534,6 +534,9 @@ static void count_tree_nodes(Node *t, struct node_counts *counts) } else if (dynamic_cast(t)) { counts->star++; count_tree_nodes(t->child[0], counts); + } else if (dynamic_cast(t)) { + counts->optional++; + count_tree_nodes(t->child[0], counts); } else if (dynamic_cast(t)) { counts->charnode++; } else if (dynamic_cast(t)) { @@ -559,7 +562,7 @@ Node *simplify_tree(Node *t, dfaflags_t flags) int i; if (flags & DFA_DUMP_TREE_STATS) { - struct node_counts counts = { 0, 0, 0, 0, 0, 0, 0, 0 }; + struct node_counts counts = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; count_tree_nodes(t, &counts); fprintf(stderr, "expr tree: c %d, [] %d, [^] %d, | %d, + %d, * %d, . %d, cat %d\n", @@ -595,7 +598,7 @@ Node *simplify_tree(Node *t, dfaflags_t flags) } } if (flags & DFA_DUMP_TREE_STATS) { - struct node_counts counts = { 0, 0, 0, 0, 0, 0, 0, 0 }; + struct node_counts counts = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; count_tree_nodes(t, &counts); fprintf(stderr, "simplified expr tree: c %d, [] %d, [^] %d, | %d, + %d, * %d, . %d, cat %d\n", diff --git a/parser/libapparmor_re/expr-tree.h b/parser/libapparmor_re/expr-tree.h index 6990cdef6..21a3b07a2 100644 --- a/parser/libapparmor_re/expr-tree.h +++ b/parser/libapparmor_re/expr-tree.h @@ -482,6 +482,26 @@ public: bool contains_null() { return child[0]->contains_null(); } }; +/* Match a node zero or one times. */ +class OptionalNode: public OneChildNode { +public: + OptionalNode(Node *left): OneChildNode(left) { nullable = true; } + void compute_firstpos() { firstpos = child[0]->firstpos; } + void compute_lastpos() { lastpos = child[0]->lastpos; } + int eq(Node *other) + { + if (dynamic_cast(other)) + return child[0]->eq(other->child[0]); + return 0; + } + ostream &dump(ostream &os) + { + os << '('; + child[0]->dump(os); + return os << ")?"; + } +}; + /* Match a node one or more times. (This is a unary operator.) */ class PlusNode: public OneChildNode { public: @@ -713,6 +733,7 @@ struct node_counts { int alt; int plus; int star; + int optional; int any; int cat; }; diff --git a/parser/parser_interface.c b/parser/parser_interface.c index 07ea0f6db..32f5279e2 100644 --- a/parser/parser_interface.c +++ b/parser/parser_interface.c @@ -371,6 +371,28 @@ void sd_serialize_xtable(std::ostringstream &buf, char **table) sd_write_structend(buf); } +void sd_serialize_xattrs(std::ostringstream &buf, struct cond_entry_list xattrs) +{ + int count; + struct cond_entry *entry; + + if (!(xattrs.list)) + return; + + count = 0; + for (entry = xattrs.list; entry; entry = entry->next) { + count++; + } + + sd_write_struct(buf, "xattrs"); + sd_write_array(buf, NULL, count); + for (entry = xattrs.list; entry; entry = entry->next) { + sd_write_string(buf, entry->name, NULL); + } + sd_write_arrayend(buf); + sd_write_structend(buf); +} + void sd_serialize_profile(std::ostringstream &buf, Profile *profile, int flattened) { @@ -432,6 +454,8 @@ void sd_serialize_profile(std::ostringstream &buf, Profile *profile, sd_write_uint32(buf, 0); sd_write_structend(buf); + sd_serialize_xattrs(buf, profile->xattrs); + sd_serialize_rlimits(buf, &profile->rlimits); if (profile->net.allow && kernel_supports_network) { diff --git a/parser/parser_lex.l b/parser/parser_lex.l index 20c845693..52a6e6656 100644 --- a/parser/parser_lex.l +++ b/parser/parser_lex.l @@ -308,7 +308,7 @@ GT > } { - peer/{WS}*={WS}*\( { + (peer|xattrs)/{WS}*={WS}*\( { /* we match to the = in the lexer so that we can switch scanner * state. By the time the parser see the = it may be too late * as bison may have requested the next token from the scanner diff --git a/parser/parser_regex.c b/parser/parser_regex.c index 72df37aa8..1b3c2b659 100644 --- a/parser/parser_regex.c +++ b/parser/parser_regex.c @@ -432,12 +432,29 @@ static const char *local_name(const char *name) return name; } +/* + * get_xattr_value returns the value of an xattr expression, performing NULL + * checks along the way. The method returns NULL if the xattr match doesn't + * have an xattrs (though this case currently isn't permitted by the parser). + */ +char *get_xattr_value(struct cond_entry *entry) +{ + if (!entry->eq) + return NULL; + if (!entry->vals) + return NULL; + return entry->vals->value; +} + static int process_profile_name_xmatch(Profile *prof) { std::string tbuf; pattern_t ptype; const char *name; + struct cond_entry *entry; + const char *xattr_value; + /* don't filter_slashes for profile names */ if (prof->attachment) name = prof->attachment; @@ -451,7 +468,7 @@ static int process_profile_name_xmatch(Profile *prof) if (ptype == ePatternInvalid) { PERROR(_("%s: Invalid profile name '%s' - bad regular expression\n"), progname, name); return FALSE; - } else if (ptype == ePatternBasic && !(prof->altnames || prof->attachment)) { + } else if (ptype == ePatternBasic && !(prof->altnames || prof->attachment || prof->xattrs.list)) { /* no regex so do not set xmatch */ prof->xmatch = NULL; prof->xmatch_len = 0; @@ -479,6 +496,28 @@ static int process_profile_name_xmatch(Profile *prof) } } } + if (prof->xattrs.list) { + for (entry = prof->xattrs.list; entry; entry = entry->next) { + xattr_value = get_xattr_value(entry); + if (!xattr_value) + xattr_value = "**"; // Default to allowing any value. + /* len is measured because it's required to + * convert the regex to pcre, but doesn't impact + * xmatch_len. The kernel uses the number of + * xattrs matched to prioritized in addition to + * xmatch_len. + */ + int len; + tbuf.clear(); + convert_aaregex_to_pcre(xattr_value, 0, + glob_default, tbuf, + &len); + if (!rules->append_rule(tbuf.c_str(), dfaflags)) { + delete rules; + return FALSE; + } + } + } prof->xmatch = rules->create_dfa(&prof->xmatch_size, &prof->xmatch_len, dfaflags); delete rules; if (!prof->xmatch) diff --git a/parser/parser_yacc.y b/parser/parser_yacc.y index ab3c9fc59..3c4e69ee6 100644 --- a/parser/parser_yacc.y +++ b/parser/parser_yacc.y @@ -306,9 +306,9 @@ opt_id: { /* nothing */ $$ = NULL; } opt_id_or_var: { /* nothing */ $$ = NULL; } | id_or_var { $$ = $1; } -profile_base: TOK_ID opt_id_or_var flags TOK_OPEN rules TOK_CLOSE +profile_base: TOK_ID opt_id_or_var opt_cond_list flags TOK_OPEN rules TOK_CLOSE { - Profile *prof = $5; + Profile *prof = $6; bool self_stack = false; if (!prof) { @@ -342,7 +342,13 @@ profile_base: TOK_ID opt_id_or_var flags TOK_OPEN rules TOK_CLOSE prof->attachment = $2; if ($2 && !($2[0] == '/' || strncmp($2, "@{", 2) == 0)) yyerror(_("Profile attachment must begin with a '/' or variable.")); - prof->flags = $3; + if ($3.name) { + if (strcmp($3.name, "xattrs") != 0) + yyerror(_("profile id: invalid conditional group %s=()"), $3.name); + free ($3.name); + prof->xattrs = $3; + } + prof->flags = $4; if (force_complain && kernel_abi_version == 5) /* newer abis encode force complain as part of the * header @@ -393,6 +399,12 @@ hat: hat_start profile_base Profile *prof = $2; if ($2) PDEBUG("Matched: hat %s { ... }\n", prof->name); + /* + * It isn't clear what a xattrs match on a hat profile + * should do, disallow it for now. + */ + if ($2->xattrs.list) + yyerror("hat profiles can't use xattrs matches"); prof->flags.hat = 1; $$ = prof; diff --git a/parser/profile.h b/parser/profile.h index 7121c0a75..048a7cf9c 100644 --- a/parser/profile.h +++ b/parser/profile.h @@ -120,6 +120,8 @@ public: size_t xmatch_size; int xmatch_len; + struct cond_entry_list xattrs; + /* char *sub_name; */ /* subdomain name or NULL */ /* int default_deny; */ /* TRUE or FALSE */ int local; @@ -151,6 +153,8 @@ public: xmatch_size = 0; xmatch_len = 0; + xattrs.list = NULL; + local = local_mode = local_audit = 0; parent = NULL; diff --git a/parser/tst/simple_tests/xattrs/bad_01.sd b/parser/tst/simple_tests/xattrs/bad_01.sd new file mode 100644 index 000000000..241dd1308 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/bad_01.sd @@ -0,0 +1,7 @@ +# +#=Description wrong conditional group +#=EXRESULT FAIL +# +/usr/bin/xattrs-test peer=(myvalue=foo) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/bad_02.sd b/parser/tst/simple_tests/xattrs/bad_02.sd new file mode 100644 index 000000000..206a34865 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/bad_02.sd @@ -0,0 +1,7 @@ +# +#=Description no xattrs value +#=EXRESULT FAIL +# +/usr/bin/xattrs-test xattrs=(myvalue) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/bad_03.sd b/parser/tst/simple_tests/xattrs/bad_03.sd new file mode 100644 index 000000000..3e5f09a3a --- /dev/null +++ b/parser/tst/simple_tests/xattrs/bad_03.sd @@ -0,0 +1,7 @@ +# +#=Description flags before xattrs +#=EXRESULT FAIL +# +/usr/bin/xattrs-test flags=(complain) xattrs=(myvalue=foo) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/hats_01.sd b/parser/tst/simple_tests/xattrs/hats_01.sd new file mode 100644 index 000000000..42b8a0cbe --- /dev/null +++ b/parser/tst/simple_tests/xattrs/hats_01.sd @@ -0,0 +1,10 @@ +# +#=Description hat profile with xattrs +#=EXRESULT FAIL +# +/usr/bin/xattrs-test { + ^hat xattrs=(myvalue=foo) { + /foo r, + } + /foo w, +} diff --git a/parser/tst/simple_tests/xattrs/ok_01.sd b/parser/tst/simple_tests/xattrs/ok_01.sd new file mode 100644 index 000000000..53bdfb312 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_01.sd @@ -0,0 +1,7 @@ +# +#=Description basic xattr value +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(myvalue=foo) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_02.sd b/parser/tst/simple_tests/xattrs/ok_02.sd new file mode 100644 index 000000000..eb10c17f6 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_02.sd @@ -0,0 +1,7 @@ +# +#=Description xattrs with quoted value +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_03.sd b/parser/tst/simple_tests/xattrs/ok_03.sd new file mode 100644 index 000000000..2cc5a44ab --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_03.sd @@ -0,0 +1,7 @@ +# +#=Description match any value of an xattr +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(myvalue="*") { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_04.sd b/parser/tst/simple_tests/xattrs/ok_04.sd new file mode 100644 index 000000000..401510510 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_04.sd @@ -0,0 +1,7 @@ +# +#=Description key with '.' character +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(hello.world=foo) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_05.sd b/parser/tst/simple_tests/xattrs/ok_05.sd new file mode 100644 index 000000000..798e81f08 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_05.sd @@ -0,0 +1,7 @@ +# +#=Description multiple xattrs +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(hello.world=foo goodbye.word=bar) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_06.sd b/parser/tst/simple_tests/xattrs/ok_06.sd new file mode 100644 index 000000000..c61b1063f --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_06.sd @@ -0,0 +1,7 @@ +# +#=Description xattrs then flags +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(myvalue=foo) flags=(audit, mediate_deleted) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_07.sd b/parser/tst/simple_tests/xattrs/ok_07.sd new file mode 100644 index 000000000..7ef7aab40 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_07.sd @@ -0,0 +1,8 @@ +# +#=Description named profile +#=EXRESULT PASS +# + +profile xattrs-test /usr/bin/hi xattrs=(user.foo=* user.bar=*) { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_08.sd b/parser/tst/simple_tests/xattrs/ok_08.sd new file mode 100644 index 000000000..94fa6c52d --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_08.sd @@ -0,0 +1,8 @@ +# +#=Description named profile without path +#=EXRESULT PASS +# + +profile xattrs-test xattrs=(user.foo="bar") { + /foo r, +} diff --git a/parser/tst/simple_tests/xattrs/ok_09.sd b/parser/tst/simple_tests/xattrs/ok_09.sd new file mode 100644 index 000000000..c60feb491 --- /dev/null +++ b/parser/tst/simple_tests/xattrs/ok_09.sd @@ -0,0 +1,7 @@ +# +#=Description profile with xattrs then flags +#=EXRESULT PASS +# +/usr/bin/xattrs-test xattrs=(myvalue=foo) flags=(complain) { + /foo r, +} diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile index 198ca4215..1dabb9853 100644 --- a/tests/regression/apparmor/Makefile +++ b/tests/regression/apparmor/Makefile @@ -139,7 +139,8 @@ SRC=access.c \ unix_socket.c \ unix_socket_client.c \ unlink.c \ - xattrs.c + xattrs.c \ + xattrs_profile.c #only do the ioperm/iopl tests for x86 derived architectures ifneq (,$(findstring $(shell uname -i),i386 i486 i586 i686 x86 x86_64)) @@ -239,6 +240,7 @@ TESTS=aa_exec \ unix_socket_unnamed \ unlink\ xattrs\ + xattrs_profile\ longpath #only do dbus if proper libs are installl diff --git a/tests/regression/apparmor/mkprofile.pl b/tests/regression/apparmor/mkprofile.pl index 6b192406f..8803fce41 100755 --- a/tests/regression/apparmor/mkprofile.pl +++ b/tests/regression/apparmor/mkprofile.pl @@ -20,6 +20,8 @@ my $usestdin = ''; my %output_rules; my $hat = "__no_hat"; my %flags; +my %xattrs; +my $path = ''; GetOptions( 'escape|E' => \$escape, @@ -374,6 +376,26 @@ sub gen_addimage($) { } } +sub gen_xattr($) { + my $rule = shift; + my @rules = split (/:/, $rule); + if (@rules != 3) { + (!$nowarn) && print STDERR "Warning: invalid xattr description '$rule', ignored\n"; + } else { + $xattrs{$rules[1]} = $rules[2]; + } +} + +sub gen_path($) { + my $rule = shift; + my @rules = split (/:/, $rule); + if (@rules != 2) { + (!$nowarn) && print STDERR "Warning: invalid path description '$rule', ignored\n"; + } else { + $path = $rules[1]; + } +} + sub emit_flags($) { my $hat = shift; @@ -429,6 +451,10 @@ sub gen_from_args() { } elsif ($rule =~ /^addimage:/) { gen_addimage($rule); $addimage = 1; + } elsif ($rule =~ /^xattr:/) { + gen_xattr($rule); + } elsif ($rule =~ /^path:/) { + gen_path($rule); } else { gen_file($rule); } @@ -438,9 +464,25 @@ sub gen_from_args() { print STDOUT "# Profile autogenerated by $__VERSION__\n"; if (not substr($bin, 0, 1) eq "/") { - print STDOUT "profile " + print STDOUT "profile " } print STDOUT "$bin "; + if (not $path eq "") { + print STDOUT "$path " + } + if (%xattrs) { + print STDOUT "xattrs=("; + my $firstloop = 1; + foreach my $xattr (keys %xattrs) { + if ($firstloop) { + $firstloop = 0; + } else { + print STDOUT " "; + } + print STDOUT "$xattr=$xattrs{$xattr}"; + } + print STDOUT ") "; + } emit_flags('__no_hat'); print STDOUT "{\n"; foreach my $outrule (@{$output_rules{'__no_hat'}}) { diff --git a/tests/regression/apparmor/xattrs_profile.c b/tests/regression/apparmor/xattrs_profile.c new file mode 100644 index 000000000..e364ae9b1 --- /dev/null +++ b/tests/regression/apparmor/xattrs_profile.c @@ -0,0 +1,63 @@ +#include + +/* + * Copyright (C) 2018 Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, version 2 of the + * License. + */ +#include +#include +#include +#include +/* + * NAME xattr_profile + * DESCRIPTION this test asserts that it's running under a specific apparmor + * profile + */ +int main(int argc, char *argv[]) +{ + FILE *fd; + ssize_t n; + size_t len = 0; + char *line; + char *token; + const char *path = "/proc/self/attr/current"; + + if (argc != 2) { + fprintf(stderr, "usage: %s apparmor-profile\n", argv[0]); + return 1; + } + + fd = fopen(path, "r"); + if (fd == NULL) { + fprintf(stderr, "failed to open %s: %s", path, strerror(errno)); + return 1; + } + + if ((n = getline(&line, &len, fd)) == -1) { + fprintf(stderr, "failed to read %s: %s", path, strerror(errno)); + fclose(fd); + return 1; + } + fclose(fd); + if ((token = strsep(&line, "\n")) != NULL) { + line = token; + } + + // Get name of profile without "(complain)" or similar suffix + if ((token = strsep(&line, " ")) != NULL) { + line = token; + } + + if (strcmp(line, argv[1])) { + printf("FAILED: run as profile %s, expected %s\n", + line, argv[1]); + return 1; + } + + printf("PASS\n"); + return 0; +} diff --git a/tests/regression/apparmor/xattrs_profile.sh b/tests/regression/apparmor/xattrs_profile.sh new file mode 100755 index 000000000..9ce79b5fc --- /dev/null +++ b/tests/regression/apparmor/xattrs_profile.sh @@ -0,0 +1,159 @@ +#! /bin/bash +# Copyright (C) 2018 Canonical, Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, version 2 of the +# License. + +#=NAME xattrs_profile +#=DESCRIPTION +# This test verifies that profiles using xattr matching match correctly. +#=END + +pwd=`dirname $0` +pwd=`cd $pwd ; /bin/pwd` + +bin=$pwd + +. $bin/prologue.inc + +file="$bin/xattrs_profile" + +requires_kernel_features domain/attach_conditions/xattr + + +# Clean up existing xattrs +clean_xattr() +{ + setfattr --remove=user.foo $file 2> /dev/null || true + setfattr --remove=user.bar $file 2> /dev/null || true + setfattr --remove=user.spam $file 2> /dev/null || true +} + +set_xattr() +{ + setfattr --name="$1" --value="$2" $file +} + +clean_xattr + +# Test basic basic xattr + +genprofile "image=profile_1" \ + "addimage:$file" \ + "path:$file" \ + "/proc/*/attr/current:r" \ + "xattr:user.foo:hello" \ + "xattr:user.bar:bye" \ + --nowarn + +runchecktest "Path with no xattrs" pass unconfined +set_xattr "user.foo" "hello" +runchecktest "Path only matching one xattr" pass unconfined +set_xattr "user.bar" "hello" +runchecktest "Path not matching xattr value" pass unconfined +set_xattr "user.bar" "bye" +runchecktest "Path matching xattrs value" pass profile_1 +set_xattr "user.spam" "hello" +runchecktest "Path matching xattrs value with additional xattr" pass profile_1 + +clean_xattr + +# Test basic xattrs with wildcards + +genprofile "image=profile_1" \ + "addimage:$file" \ + "path:$bin/xattrs_profile" \ + "/proc/*/attr/current:r" \ + "xattr:user.foo:hello/*" \ + "xattr:user.bar:*" + +runchecktest "Path with no xattrs" pass unconfined +set_xattr "user.foo" "hello" +runchecktest "Path not matching xattr regexs" pass unconfined +set_xattr "user.bar" "hello" +runchecktest "Path matching one xattr regex" pass unconfined +set_xattr "user.foo" "hello/foo" +runchecktest "Path matching xattrs regex" pass profile_1 +set_xattr "user.spam" "bye" +runchecktest "Path matching xattrs regex with additional xattr" pass profile_1 + +clean_xattr + +# Test that longer paths have higher priority than xattrs + +genprofile "image=profile_1" \ + "addimage:$file" \ + "path:$bin/*" \ + "/proc/*/attr/current:r" \ + "xattr:user.foo:hello" \ + -- \ + "image=profile_2" \ + "addimage:$file" \ + "path:$bin/xattrs_profile" \ + "/proc/*/attr/current:r" + +runchecktest "Path with no xattrs" pass profile_2 +set_xattr "user.foo" "hello" +runchecktest "Path more specific than xattr profile" pass profile_2 + +clean_xattr + +# Test that longer paths with xattrs have higher priority than shorter paths + +genprofile "image=profile_1" \ + "addimage:$file" \ + "path:$file" \ + "/proc/*/attr/current:r" \ + "xattr:user.foo:hello" \ + -- \ + "image=profile_2" \ + "addimage:$file" \ + "path:$bin/xattrs_*" \ + "/proc/*/attr/current:r" + +runchecktest "Path with no xattrs" pass profile_2 +set_xattr "user.foo" "hello" +runchecktest "Path with xattrs longer" pass profile_1 + +clean_xattr + +# Test that xattrs break path length ties + +genprofile "image=profile_1" \ + "addimage:$file" \ + "path:$file" \ + "/proc/*/attr/current:r" \ + "xattr:user.foo:hello" \ + -- \ + "image=profile_2" \ + "addimage:$file" \ + "path:$file" \ + "/proc/*/attr/current:r" + +runchecktest "Path with no xattrs" pass profile_2 +set_xattr "user.foo" "hello" +runchecktest "Profiles with xattrs and same path length" pass profile_1 + +clean_xattr + +# xattr matching doesn't work if the xattr value has a null character. This +# impacts matching security.ima and security.evm values. +# +# A kernel patch has been proposed to fix this: +# https://lists.ubuntu.com/archives/apparmor/2018-December/011882.html + +genprofile "image=profile_1" \ + "addimage:$file" \ + "path:$file" \ + "/proc/*/attr/current:r" \ + "xattr:user.foo:**" \ + +runchecktest "Path with no xattrs" pass unconfined +set_xattr "user.foo" "ab" +runchecktest "matches value" pass profile_1 +set_xattr "user.foo" "0x610062" # "a\0b" +runchecktest "xattr values with null characters don't work" pass unconfined + +clean_xattr diff --git a/utils/apparmor/regex.py b/utils/apparmor/regex.py index 4d16fb8f9..6ec966483 100644 --- a/utils/apparmor/regex.py +++ b/utils/apparmor/regex.py @@ -45,7 +45,7 @@ RE_PROFILE_CONDITIONAL_VARIABLE = re.compile('^\s*if\s+(not\s+)?defined\s+(@\{?\ RE_PROFILE_CONDITIONAL_BOOLEAN = re.compile('^\s*if\s+(not\s+)?defined\s+(\$\{?\w+\}?)\s*\{\s*(#.*)?$') RE_PROFILE_NETWORK = re.compile(RE_AUDIT_DENY + 'network(?P
\s+.*)?' + RE_COMMA_EOL) RE_PROFILE_CHANGE_HAT = re.compile('^\s*\^(\"??.+?\"??)' + RE_COMMA_EOL) -RE_PROFILE_HAT_DEF = re.compile('^(?P\s*)(?P\^|hat\s+)(?P\"??[^)]+?\"??)' + RE_XATTRS + RE_FLAGS + '\s*\{' + RE_EOL) +RE_PROFILE_HAT_DEF = re.compile('^(?P\s*)(?P\^|hat\s+)(?P\"??[^)]+?\"??)' + RE_FLAGS + '\s*\{' + RE_EOL) RE_PROFILE_DBUS = re.compile(RE_AUDIT_DENY + '(dbus\s*,|dbus(?P
\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_MOUNT = re.compile(RE_AUDIT_DENY + '((mount|remount|umount|unmount)(\s+[^#]*)?\s*,)' + RE_EOL) RE_PROFILE_SIGNAL = re.compile(RE_AUDIT_DENY + '(signal\s*,|signal(?P
\s+[^#]*)\s*,)' + RE_EOL)