2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-22 10:07:12 +00:00

Add support for profiles with xattrs matching

Add userland support for matching based on extended file attributes. This
leverages DFA based matching already in the kernel:

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=8e51f908
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=73f488cd

Matching is exposed via flags on the profile:

    /usr/bin/* xattrs=(user.foo=bar user.bar=foo) {
        # ...
    }

xattr values are appended to the existing xmatch via a null transition.

    $ echo '/usr/bin/* xattrs=(user.foo=foo user.bar=bar) {}' | \
        ./parser/apparmor_parser -QT -D expr-tree
    DFA: Expression Tree
    /usr/bin/[^\0000/]([^\0000/])*(\0000bar)?(\0000foo)?< 0x1>
    DFA: Expression Tree
    (\a|(\n|(\0002|\t)))< 0x4>

Tested manually on a 4.19 kernel via QEMU+KVM.

TODO:

  * ~~Add regression tests~~ (EDIT: done)
  * ~~EDIT: add support in the tools~~ (EDIT: done)

Questions for reviewers:

  * ~~parser/libapparmor: regex construction probably needs cleaning up~~ (EDIT: done)
  * ~~parser/parser_regex.c: confused what xmatch length is for~~ (EDIT: done)

/cc @mjg59

PR: https://gitlab.com/apparmor/apparmor/merge_requests/270
Signed-off-by: John Johansen <john.johansen@canonical.com>
This commit is contained in:
John Johansen 2019-03-21 08:12:07 +00:00
commit cfe20d2b63
30 changed files with 643 additions and 13 deletions

View File

@ -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

View File

@ -66,7 +66,7 @@ B<COMMENT> = '#' I<TEXT> [ '\r' ] '\n'
B<TEXT> = any characters
B<PROFILE> = ( I<PROFILE HEAD> ) [ I<ATTACHMENT SPECIFICATION> ] [ I<PROFILE FLAG CONDS> ] '{' ( I<RULES> )* '}'
B<PROFILE> = ( I<PROFILE HEAD> ) [ I<ATTACHMENT SPECIFICATION> ] [ I<PROFILE XATTR CONDS> ] [ I<PROFILE FLAG CONDS> ] '{' ( I<RULES> )* '}'
B<PROFILE HEAD> = [ 'profile' ] I<FILEGLOB> | 'profile' I<PROFILE NAME>
@ -78,6 +78,12 @@ B<UNQUOTED PROFILE NAME> = (must start with alphanumeric character (after variab
B<ATTACHMENT SPECIFICATION> = I<FILEGLOB>
B<PROFILE XATTR CONDS> = [ 'xattrs=' ] '(' comma or white space separated list of I<PROFILE XATTR> ')'
B<PROFILE XATTR> = extended attribute name '=' I<XATTR VALUE FILEGLOB>
B<XATTR VALUE FILEGLOB> = I<FILEGLOB>
B<PROFILE FLAG CONDS> = [ 'flags=' ] '(' comma or white space separated list of I<PROFILE FLAGS> ')'
B<PROFILE FLAGS> = 'complain' | 'audit' | 'enforce' | 'mediate_deleted' | 'attach_disconnected' | 'chroot_relative'
@ -1371,6 +1377,18 @@ Directories anywhere underneath F</tmp>.
=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<https://wiki.apparmor.net>.

108
parser/apparmor_xattrs.pod Normal file
View File

@ -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<https://wiki.apparmor.net>.
=cut

View File

@ -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

View File

@ -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);
};

View File

@ -534,6 +534,9 @@ static void count_tree_nodes(Node *t, struct node_counts *counts)
} else if (dynamic_cast<StarNode *>(t)) {
counts->star++;
count_tree_nodes(t->child[0], counts);
} else if (dynamic_cast<OptionalNode *>(t)) {
counts->optional++;
count_tree_nodes(t->child[0], counts);
} else if (dynamic_cast<CharNode *>(t)) {
counts->charnode++;
} else if (dynamic_cast<AnyCharNode *>(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",

View File

@ -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<OptionalNode *>(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;
};

View File

@ -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) {

View File

@ -308,7 +308,7 @@ GT >
}
<INITIAL,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE>{
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

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,7 @@
#
#=Description wrong conditional group
#=EXRESULT FAIL
#
/usr/bin/xattrs-test peer=(myvalue=foo) {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description no xattrs value
#=EXRESULT FAIL
#
/usr/bin/xattrs-test xattrs=(myvalue) {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description flags before xattrs
#=EXRESULT FAIL
#
/usr/bin/xattrs-test flags=(complain) xattrs=(myvalue=foo) {
/foo r,
}

View File

@ -0,0 +1,10 @@
#
#=Description hat profile with xattrs
#=EXRESULT FAIL
#
/usr/bin/xattrs-test {
^hat xattrs=(myvalue=foo) {
/foo r,
}
/foo w,
}

View File

@ -0,0 +1,7 @@
#
#=Description basic xattr value
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(myvalue=foo) {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description xattrs with quoted value
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description match any value of an xattr
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(myvalue="*") {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description key with '.' character
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(hello.world=foo) {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description multiple xattrs
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(hello.world=foo goodbye.word=bar) {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description xattrs then flags
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(myvalue=foo) flags=(audit, mediate_deleted) {
/foo r,
}

View File

@ -0,0 +1,8 @@
#
#=Description named profile
#=EXRESULT PASS
#
profile xattrs-test /usr/bin/hi xattrs=(user.foo=* user.bar=*) {
/foo r,
}

View File

@ -0,0 +1,8 @@
#
#=Description named profile without path
#=EXRESULT PASS
#
profile xattrs-test xattrs=(user.foo="bar") {
/foo r,
}

View File

@ -0,0 +1,7 @@
#
#=Description profile with xattrs then flags
#=EXRESULT PASS
#
/usr/bin/xattrs-test xattrs=(myvalue=foo) flags=(complain) {
/foo r,
}

View File

@ -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

View File

@ -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'}}) {

View File

@ -0,0 +1,63 @@
#include <sys/types.h>
/*
* 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 <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* 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;
}

View File

@ -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

View File

@ -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<details>\s+.*)?' + RE_COMMA_EOL)
RE_PROFILE_CHANGE_HAT = re.compile('^\s*\^(\"??.+?\"??)' + RE_COMMA_EOL)
RE_PROFILE_HAT_DEF = re.compile('^(?P<leadingspace>\s*)(?P<hat_keyword>\^|hat\s+)(?P<hat>\"??[^)]+?\"??)' + RE_XATTRS + RE_FLAGS + '\s*\{' + RE_EOL)
RE_PROFILE_HAT_DEF = re.compile('^(?P<leadingspace>\s*)(?P<hat_keyword>\^|hat\s+)(?P<hat>\"??[^)]+?\"??)' + RE_FLAGS + '\s*\{' + RE_EOL)
RE_PROFILE_DBUS = re.compile(RE_AUDIT_DENY + '(dbus\s*,|dbus(?P<details>\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<details>\s+[^#]*)\s*,)' + RE_EOL)