mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-22 18:17:09 +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:
commit
cfe20d2b63
@ -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
|
||||
|
@ -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
108
parser/apparmor_xattrs.pod
Normal 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
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
7
parser/tst/simple_tests/xattrs/bad_01.sd
Normal file
7
parser/tst/simple_tests/xattrs/bad_01.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description wrong conditional group
|
||||
#=EXRESULT FAIL
|
||||
#
|
||||
/usr/bin/xattrs-test peer=(myvalue=foo) {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/bad_02.sd
Normal file
7
parser/tst/simple_tests/xattrs/bad_02.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description no xattrs value
|
||||
#=EXRESULT FAIL
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(myvalue) {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/bad_03.sd
Normal file
7
parser/tst/simple_tests/xattrs/bad_03.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description flags before xattrs
|
||||
#=EXRESULT FAIL
|
||||
#
|
||||
/usr/bin/xattrs-test flags=(complain) xattrs=(myvalue=foo) {
|
||||
/foo r,
|
||||
}
|
10
parser/tst/simple_tests/xattrs/hats_01.sd
Normal file
10
parser/tst/simple_tests/xattrs/hats_01.sd
Normal file
@ -0,0 +1,10 @@
|
||||
#
|
||||
#=Description hat profile with xattrs
|
||||
#=EXRESULT FAIL
|
||||
#
|
||||
/usr/bin/xattrs-test {
|
||||
^hat xattrs=(myvalue=foo) {
|
||||
/foo r,
|
||||
}
|
||||
/foo w,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_01.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_01.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description basic xattr value
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(myvalue=foo) {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_02.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_02.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description xattrs with quoted value
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_03.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_03.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description match any value of an xattr
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(myvalue="*") {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_04.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_04.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description key with '.' character
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(hello.world=foo) {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_05.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_05.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description multiple xattrs
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(hello.world=foo goodbye.word=bar) {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_06.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_06.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description xattrs then flags
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(myvalue=foo) flags=(audit, mediate_deleted) {
|
||||
/foo r,
|
||||
}
|
8
parser/tst/simple_tests/xattrs/ok_07.sd
Normal file
8
parser/tst/simple_tests/xattrs/ok_07.sd
Normal file
@ -0,0 +1,8 @@
|
||||
#
|
||||
#=Description named profile
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
|
||||
profile xattrs-test /usr/bin/hi xattrs=(user.foo=* user.bar=*) {
|
||||
/foo r,
|
||||
}
|
8
parser/tst/simple_tests/xattrs/ok_08.sd
Normal file
8
parser/tst/simple_tests/xattrs/ok_08.sd
Normal file
@ -0,0 +1,8 @@
|
||||
#
|
||||
#=Description named profile without path
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
|
||||
profile xattrs-test xattrs=(user.foo="bar") {
|
||||
/foo r,
|
||||
}
|
7
parser/tst/simple_tests/xattrs/ok_09.sd
Normal file
7
parser/tst/simple_tests/xattrs/ok_09.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#
|
||||
#=Description profile with xattrs then flags
|
||||
#=EXRESULT PASS
|
||||
#
|
||||
/usr/bin/xattrs-test xattrs=(myvalue=foo) flags=(complain) {
|
||||
/foo r,
|
||||
}
|
@ -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
|
||||
|
@ -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'}}) {
|
||||
|
63
tests/regression/apparmor/xattrs_profile.c
Normal file
63
tests/regression/apparmor/xattrs_profile.c
Normal 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;
|
||||
}
|
159
tests/regression/apparmor/xattrs_profile.sh
Executable file
159
tests/regression/apparmor/xattrs_profile.sh
Executable 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
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user