2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-22 01:57:43 +00:00

Compare commits

...

71 Commits

Author SHA1 Message Date
Maxime Bélair
4a46925e9b Merge Initial profile for nginx
Initial profile for nginx, tested on Ubuntu 24.04 manually and with
nginx testsuite.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1538
Merged-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-18 23:09:21 +00:00
John Johansen
e7daccedc6 Merge regression: disconnected_mount_complain dangling fds and alloc fail handling
Signed-off-by: Ryan Lee <ryan.lee@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1767
Approved-by: Georgia Garcia <georgia.garcia@canonical.com>
Merged-by: John Johansen <john@jjmx.net>
2025-08-16 06:46:14 +00:00
Maxime Bélair
468f0096ee Merge aa-notify: Improve support for local profiles
This MR contains fixes and improvements for --local profiles in aa-notify

 - aa-notify: Make --local commandline option override use_local_profiles
 - utils: Move get_local_include to ProfileStorage
 - utils: Add tests for get_local_include
 - aa-notify gui: Fix undefined variable when ttkthemes is not installed

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1770
Approved-by: Maxime Bélair <maxime.belair@canonical.com>
Merged-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-15 11:52:26 +00:00
Maxime Bélair
d993dfbb02 aa-notify gui: Fix undefined variable when ttkthemes is not installed
When ttkthemes is not installed, bg_color is undefined. We don't use it
in that case.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-15 13:51:32 +02:00
Maxime Bélair
ba336533ac utils: Add tests for get_local_include
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-15 13:51:32 +02:00
Maxime Bélair
0d34f12d7e utils: Move get_local_include to ProfileStorage
Move get_local_include from aa.py to ProfileStorage, a more logical
location.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-15 13:51:26 +02:00
John Johansen
ebba635fa9 Merge profiles: Allow curl to read tmp, for scripts which might use config/etags/data...
Some system scripts, namely pollinate, pass temporary files as data.

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1769
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-08-14 17:24:56 +00:00
Christian Boltz
e477ccacfa Merge abstractions/gtk: allow writing vulcan cache
Reported by darix

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1766
Approved-by: Ryan Lee <rlee287@yahoo.com>
Merged-by: Christian Boltz <apparmor@cboltz.de>
2025-08-14 13:49:33 +00:00
Christian Boltz
9c5064529a Merge abstractions/libnuma: add rules for active usage
The current profile is for linking against libnuma. This
update adds the rules needed to get system information
when actually using libnuma functionality.

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1768
Approved-by: Georgia Garcia <georgia.garcia@canonical.com>
Approved-by: Christian Boltz <apparmor@cboltz.de>
Merged-by: Christian Boltz <apparmor@cboltz.de>
2025-08-14 13:06:22 +00:00
Christian Boltz
862d8ec9fc Merge aa-notify: Add --xauthority to set $XAUTHORITY under sudo
Fixes #449

Tkinter (used by aa-notify) needs the $XAUTHORITY envvar to start but on
some systems (e.g. OpenSuse), sudo clears it. This change add a
--xauthority command-line option to set it explicitly, so aa-notify works
under sudo.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>

Closes #449
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1771
Approved-by: Christian Boltz <apparmor@cboltz.de>
Merged-by: Christian Boltz <apparmor@cboltz.de>
2025-08-14 11:36:15 +00:00
Maxime Bélair
fbd266c63f aa-notify: Add --xauthority to set $XAUTHORITY under sudo
Fixes #449

Tkinter (used by aa-notify) needs the $XAUTHORITY envvar to start but on
some systems (e.g. OpenSuse), sudo clears it. This change add a
--xauthority command-line option to set it explicitly, so aa-notify works
under sudo.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-14 11:06:09 +02:00
Maxime Bélair
fcbf8e34ec aa-notify: Make --local commandline option override use_local_profiles
If both the --local commandline option and use_local_profiles
configuration are specified, the commandline now takes precedence.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-14 10:01:03 +02:00
Simon Poirier
01ab33202a profiles: Allow curl to read tmp, for scripts which might use config/etags/data...
Signed-off-by: Simon Poirier <simon.poirier@canonical.com>
2025-08-13 21:37:53 -04:00
Christian Ehrhardt
24216d79e9
abstractions/libnuma: add rules for active usage
The current profile is for linking against libnuma. This
update adds the rules needed to get system information
when actually using libnuma functionality.

Signed-off-by: Christian Ehrhardt <christian.ehrhardt@canonical.com>
2025-08-13 10:39:49 +02:00
Ryan Lee
bef673f3c6 regression: disconnected_mount_complain dangling fds and alloc fail handling
Signed-off-by: Ryan Lee <ryan.lee@canonical.com>
2025-08-12 15:00:20 -07:00
Christian Boltz
8210308508
abstractions/gtk: allow writing vulcan cache
Reported by darix
2025-08-12 22:08:16 +02:00
John Johansen
a8875460ed Merge utils: Allow writing to profile includes
Allow writing to local profiles

This notably allows aa-notify to write to local profiles instead of the main profile with the new `--local` option. This keeps the base profile clean, avoiding breakages when the system updates profiles.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1764
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-08-12 08:36:56 +00:00
Maxime Bélair
eae49bf8de test-aa-notify: Update help test
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-11 18:16:53 +02:00
Maxime Bélair
144d782ae8 aa-notify: Update config with use_local_profiles
aa-notify configuration now supports use_local_profiles, and this option
is documented in the manual.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-11 18:16:53 +02:00
Maxime Bélair
df1a4c8782 aa-notify: Allow writing to local profiles
The new option --local allows user to write new rules to local profiles
instead of system profiles, enabling cleaner profile deployment.

This option support the values (yes, no and auto)

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-11 18:16:53 +02:00
Maxime Bélair
4c30a0ac65 utils: Allow writing to profile includes
This patch allows writing write in include files and save them to disk.
This is particularly helpful for local includes (generally used in
profiles through `include if exists <local/foo>`), and keeps the base
profile clean, avoiding breakages when the system updates profiles.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-08-11 18:16:22 +02:00
Steve Beattie
60ca491f21 Merge fix more parser leaks
Closes #534
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1763
Approved-by: Steve Beattie <steve+gitlab@nxnw.org>
Merged-by: Steve Beattie <steve+gitlab@nxnw.org>
2025-08-06 19:12:04 -07:00
Georgia Garcia
43fa5f88a7 parser: fix cases leaks when new state creation fails
For every item in "cases", a new state is created, but if the creation
of one of them fails, the rest of the items in that list would not be
deleted and would leak. Fix it by continuing to iterate over the items
in the list and delete them, and then re-throw the exception.

$ /usr/bin/valgrind --leak-check=full --error-exitcode=151 ../apparmor_parser -Q -I simple_tests/ -M ./features_files/features.all simple_tests/xtrans/x-conflict.sd

==564911== 208 (48 direct, 160 indirect) bytes in 1 blocks are definitely lost in loss record 15 of 19
==564911==    at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==564911==    by 0x179E74: CharNode::follow(Cases&) (expr-tree.h:447)
==564911==    by 0x189F8B: DFA::update_state_transitions(optflags const&, State*) (hfa.cc:376)
==564911==    by 0x18A25B: DFA::process_work_queue(char const*, optflags const&) (hfa.cc:442)
==564911==    by 0x18CB65: DFA::DFA(Node*, optflags const&, bool) (hfa.cc:486)
==564911==    by 0x178263: aare_rules::create_chfa(int*, std::vector<aa_perms, std::allocator<aa_perms> >&, optflags const&, bool, bool) (aare_rules.cc:258)
==564911==    by 0x178A4F: aare_rules::create_dfablob(unsigned long*, int*, std::vector<aa_perms, std::allocator<aa_perms> >&, optflags const&, bool, bool) (aare_rules.cc:359)
==564911==    by 0x14E4E1: process_profile_regex(Profile*) (parser_regex.c:791)
==564911==    by 0x154CDF: process_profile_rules(Profile*) (parser_policy.c:194)
==564911==    by 0x154E0F: post_process_profile(Profile*, int) (parser_policy.c:240)
==564911==    by 0x154F7A: post_process_policy_list (parser_policy.c:257)
==564911==    by 0x154F7A: post_process_policy(int) (parser_policy.c:267)
==564911==    by 0x141B17: process_profile(int, aa_kernel_interface*, char const*, aa_policy_cache*) (parser_main.c:1227)

Fixes: https://gitlab.com/apparmor/apparmor/-/issues/534
Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-06 19:15:37 -03:00
Georgia Garcia
bb03d9ee08 parser: fix leak on conflicting x modifiers
When the "conflicting x modifiers" exception was thrown, the DFA
object creation would fail, therefore the destructor would not be
called and the states previously allocated would leak.

Unfortunately there's no way to call the destructor if the object was
not created, so I moved the contents of the destructor into a cleanup
helper function to be called in both instances.

$ /usr/bin/valgrind --leak-check=full --error-exitcode=151 ../apparmor_parser -Q -I simple_tests/ -M ./features_files/features.all simple_tests/xtrans/x-conflict.sd

==564911== 592 (112 direct, 480 indirect) bytes in 1 blocks are definitely lost in loss record 16 of 19
==564911==    at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==564911==    by 0x189C9A: DFA::add_new_state(optflags const&, std::set<ImportantNode*, std::less<ImportantNode*>, std::allocator<ImportantNode*> >*, std::set<ImportantNode*, std::less<ImportantNode*>, std::allocator<ImportantNode*> >*, State*) (hfa.cc:337)
==564911==    by 0x18CB22: add_new_state (hfa.cc:357)
==564911==    by 0x18CB22: DFA::DFA(Node*, optflags const&, bool) (hfa.cc:473)
==564911==    by 0x178263: aare_rules::create_chfa(int*, std::vector<aa_perms, std::allocator<aa_perms> >&, optflags const&, bool, bool) (aare_rules.cc:258)
==564911==    by 0x178A4F: aare_rules::create_dfablob(unsigned long*, int*, std::vector<aa_perms, std::allocator<aa_perms> >&, optflags const&, bool, bool) (aare_rules.cc:359)
==564911==    by 0x14E4E1: process_profile_regex(Profile*) (parser_regex.c:791)
==564911==    by 0x154CDF: process_profile_rules(Profile*) (parser_policy.c:194)
==564911==    by 0x154E0F: post_process_profile(Profile*, int) (parser_policy.c:240)
==564911==    by 0x154F7A: post_process_policy_list (parser_policy.c:257)
==564911==    by 0x154F7A: post_process_policy(int) (parser_policy.c:267)
==564911==    by 0x141B17: process_profile(int, aa_kernel_interface*, char const*, aa_policy_cache*) (parser_main.c:1227)
==564911==    by 0x135421: main (parser_main.c:1771)

Fixes: https://gitlab.com/apparmor/apparmor/-/issues/534
Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-06 19:15:37 -03:00
Georgia Garcia
d9866f0a24 parser: fix leaking disconnected paths when merging
Valgrind showed that the disconnected paths variables were leaking
during the merge. That happened because flagvals did not implement a
destructor freeing the variables, so they leaked. flagvals cannot
implement a destructor, because that would make it a non-trivial union
member and parser_yacc.y would not compile. This patch implements a
"clear" function that is supposed to act as the destructor.

$ /usr/bin/valgrind --leak-check=full --error-exitcode=151 ../apparmor_parser -Q -I simple_tests/ -M ./features_files/features.all flags_ok_disconnected_ipc15.sd
...
==3708747== 5 bytes in 1 blocks are definitely lost in loss record 1 of 11
==3708747==    at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==3708747==    by 0x492E35E: strdup (strdup.c:42)
==3708747==    by 0x14C74E: set_disconnected_path (profile.h:188)
==3708747==    by 0x14C74E: flagvals::init(char const*) (profile.h:223)
==3708747==    by 0x14859B: yyparse() (parser_yacc.y:592)
==3708747==    by 0x141A99: process_profile(int, aa_kernel_interface*, char const*, aa_policy_cache*) (parser_main.c:1187)
==3708747==    by 0x135421: main (parser_main.c:1771)
...

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-06 19:15:37 -03:00
John Johansen
fedcab2ad0 Merge nss-systemd: Grant access to the GDM user database
GDM 49~beta implements a userdb VarLink service for managing the unix users
running the greeter shell, as well as the gnome-initial-setup users.

```
gdm-launch-environment][1892]: Gdm: GdmSessionWorker: determining if authenticated user (password required:0) is authorized to session
unix_chkpwd[1897]: could not obtain user info (gdm-greeter)
kernel: audit: type=1400 audit(1754399331.488:211): apparmor="DENIED" operation="connect" class="file" profile="unix-chkpwd" name="/run/systemd/userdb/org.gnome.DisplayManager" pid=1897 comm="unix_chkpwd" requested_mask="wr" denied_mask="wr" fsuid=0 ouid=0
gdm-launch-environment][1892]: Gdm: GdmSessionWorker: user is not authorized to log in: Authentication failure
```

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1761
Approved-by: Ryan Lee <rlee287@yahoo.com>
Merged-by: John Johansen <john@jjmx.net>
2025-08-06 06:02:14 +00:00
Alessandro Astone
b6caed3b57 nss-systemd: Grant access to the GDM user database
GDM 49~beta implements a userdb VarLink service for managing the unix users
running the greeter shell, as well as the gnome-initial-setup users.

gdm-launch-environment][1892]: Gdm: GdmSessionWorker: determining if authenticated user (password required:0) is authorized to session
unix_chkpwd[1897]: could not obtain user info (gdm-greeter)
kernel: audit: type=1400 audit(1754399331.488:211): apparmor="DENIED" operation="connect" class="file" profile="unix-chkpwd" name="/run/systemd/userdb/org.gnome.DisplayManager" pid=1897 comm="unix_chkpwd" requested_mask="wr" denied_mask="wr" fsuid=0 ouid=0
gdm-launch-environment][1892]: Gdm: GdmSessionWorker: user is not authorized to log in: Authentication failure

LP: #2119541
2025-08-05 15:51:25 +02:00
John Johansen
ae70dc38f8 Merge parser: drop dead code in mount.cc
perms = 0, therefore perms & something is always false.

Fixes: coverity#320937 and coverity#320937

Also remove nop code from mnt_rule::post_parse_profile(Profile &prof) as discussed in this MR.

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1759
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-08-05 08:31:10 +00:00
Steve Beattie
51bdbec119 Merge parser misc fixes (memory leaks, restoring ostream format)
Closes #533
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1760
Approved-by: Steve Beattie <steve+gitlab@nxnw.org>
Merged-by: Steve Beattie <steve+gitlab@nxnw.org>
2025-08-04 22:34:01 -07:00
Georgia Garcia
b8dee97ed3 parser: fix leaking name in variable expansion
Fixes: https://gitlab.com/apparmor/apparmor/-/issues/533
Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-04 18:55:58 -03:00
Georgia Garcia
8b2e2c3358 parser: free leaking cod_entry in case of failure in do_alias
Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-04 18:55:58 -03:00
Georgia Garcia
3faddfcf46 parser: fix coverity's "not restoring ostream format"
Save the ostream flags and restore them after the std::hex
modification.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-04 18:55:58 -03:00
Georgia Garcia
05458768cf parser: constify and pass by reference unchanged value
Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-04 18:55:58 -03:00
Georgia Garcia
cb0d66d55a parser: fix leaks in deleted variables
Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-04 18:55:58 -03:00
Christian Boltz
0de9678d4f
mount.cc: remove nop code from mnt_rule::post_parse_profile(Profile &prof)
... as discussed in https://gitlab.com/apparmor/apparmor/-/merge_requests/1759#note_2665952086
2025-08-04 19:35:26 +02:00
Christian Boltz
617d3021e8
parser: drop dead code in mount.cc
perms = 0, therefore perms & something is always false.

Fixes: coverity#320937 and coverity#320937
2025-08-04 00:08:26 +02:00
Christian Boltz
63b46dd3d7 Merge utils: fix typo in aa-show-usage man page
Signed-off-by: Ryan Lee <ryan.lee@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1757
Approved-by: Christian Boltz <apparmor@cboltz.de>
Merged-by: Christian Boltz <apparmor@cboltz.de>
2025-08-01 19:56:38 +00:00
Ryan Lee
67382dcf15 utils: fix typo in aa-show-usage man page
Signed-off-by: Ryan Lee <ryan.lee@canonical.com>
2025-08-01 12:20:18 -07:00
Steve Beattie
d61295a249 Merge parser: fix variable expansion
When the variable was being expanded, it needed to be reevaluated to
check if there was still unresolved variables. That allowed for a
weird bug to happen: If the string contained a variable preceded by @,
like in "user@@{uid}" and the variable was resolved to a case where {
is used, like in @{uid}={[0-9],[1-9][0-9]}, then on the second pass,
the parser would try to resolve the following variable
@{[0-9],[1-9][0-9]}, which is incorrect behavior. Fix it by not
including part of the string that was already resolved on the
subsequent passes.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1756
Approved-by: Steve Beattie <steve+gitlab@nxnw.org>
Merged-by: Steve Beattie <steve+gitlab@nxnw.org>
2025-07-31 22:55:32 -07:00
Georgia Garcia
a2f2ca6119 parser: fix variable expansion
When the variable was being expanded, it needed to be reevaluated to
check if there was still unresolved variables. That allowed for a
weird bug to happen: If the string contained a variable preceded by @,
like in "user@@{uid}" and the variable was resolved to a case where {
is used, like in @{uid}={[0-9],[1-9][0-9]}, then on the second pass,
the parser would try to resolve the following variable
@{[0-9],[1-9][0-9]}, which is incorrect behavior. Fix it by not
including part of the string that was already resolved on the
subsequent passes.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-07-31 18:04:16 -03:00
John Johansen
61e09c6ffa Merge Fix whitespace in hfa.h
This got broken in 0f36070a54764deb6e5186443d81ba05ea17216a / https://gitlab.com/apparmor/apparmor/-/merge_requests/1750

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1755
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-07-31 19:48:00 +00:00
Christian Boltz
45a7cc1ed0
Fix whitespace in hfa.h
This got broken in 0f36070a54764deb6e5186443d81ba05ea17216a / https://gitlab.com/apparmor/apparmor/-/merge_requests/1750
2025-07-31 21:28:03 +02:00
John Johansen
dc78be4db6 Prepare for 5.0.0~alpha1 release
- bump version

Signed-off-by: John Johansen <john.johansen@canonical.com>
2025-07-31 11:41:53 -07:00
John Johansen
ea97cbedef Merge fix xtable generation and drop unusd perm32 v1 support
The xtable on perms32 capable systems is being padded to the size of
the accept state tables. This was a hack to get around issue in a buggy
perms32 v1. We do not support any system using perms 32 v1 so we can
drop the hack.

Similarly since we don't support perms32v1 we don't support prompt
compat dev or perms32v1, so drop them as well

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1750
Approved-by: Ryan Lee <rlee287@yahoo.com>
Merged-by: John Johansen <john@jjmx.net>
2025-07-31 18:08:06 +00:00
John Johansen
514bf114b2 parser: drop unused map_xbits()
The map_xbits() is currently unused so drop it.

Signed-off-by: John Johansen <john.johansen@canonical.com>
2025-07-31 10:23:22 -07:00
John Johansen
0430080a16 parser: drop unused create_welded_dfablob and related code
Their is no reason for the parse to stitch 2 dfas together this way.
In the future there will be better ways to do this using unconpressed
dfas.

Dropping this also allows for some simplification, in other parts of
the code.

Drop the dead/unused code

Signed-off-by: John Johansen <john.johansen@canonical.com>
2025-07-31 10:23:22 -07:00
John Johansen
0f36070a54 parser: drop support for prompt_compat_permsv1, and prompt_compat_dev
prompt_compat_permsv1 and prompt_compat_dev were used to support
prompt during early dev. We do not support any kernel using these
so drop them.

This also allows us to drop the propogation of prompt as a parameter
through several functions.

Signed-off-by: John Johansen <john.johansen@canonical.com>
2025-07-31 10:23:22 -07:00
John Johansen
392849e518 parser: fix xtable generation
The xtable on perms32 capable systems is being padded to the size of
the accept state tables. This was a hack to get around issue in a buggy
perms32 v1. We do not support any system using perms 32 v1 so we can
drop the hack.

Signed-off-by: John Johansen <john.johansen@canonical.com>
2025-07-31 10:23:22 -07:00
John Johansen
e8cd6e704a Merge coverity: remove log retrieving step temporarily
Right now coverity is running in two steps, one to collect logs in
case of failures, and a different one to actually send the data to
coverity. The log collection step is failing because when collecting
data for python with the new version of coverity, build-log.txt is not
generated.

The whole way we build with coverity might need changing, but
currently this patch is removing the log collection so the pipeline
passes.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1754
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-07-31 17:21:02 +00:00
Georgia Garcia
95d7f37520 coverity: remove log retrieving step temporarily
Right now coverity is running in two steps, one to collect logs in
case of failures, and a different one to actually send the data to
coverity. The log collection step is failing because when collecting
data for python with the new version of coverity, build-log.txt is not
generated.

The whole way we build with coverity might need changing, but
currently this patch is removing the log collection so the pipeline
passes.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-07-31 13:02:07 -03:00
John Johansen
c54c4a7e01 Merge coverity: fix deprecated uses of --no-command and --fs-capture-search
According to the coverity documentation [1], filesystem capture is no
longer supported, favoring the use of the "coverity capture" tool.
This fixes the coverity pipeline which is broken due to flags
--no-command and --fs-capture-search no longer working.

[1] https://documentation.blackduck.com/bundle/coverity-docs-2024.3/page/coverity-analysis/topics/moving_from_filesystem_capture_to_the_coverity_cli.html

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1751
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-07-31 02:05:06 +00:00
John Johansen
375470144f Merge regression: fix usage statement for linkat_tmpfile
See https://gitlab.com/apparmor/apparmor/-/merge_requests/1743#note_2658749912 for context.

Signed-off-by: Ryan Lee <ryan.lee@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1752
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-07-31 01:17:17 +00:00
Ryan Lee
73bcf488b2 regression: fix usage statement for linkat_tmpfile
Signed-off-by: Ryan Lee <ryan.lee@canonical.com>
2025-07-30 16:35:21 -07:00
Georgia Garcia
117df51e4a coverity: fix deprecated uses of --no-command and --fs-capture-search
According to the coverity documentation [1], filesystem capture is no
longer supported, favoring the use of the "coverity capture" tool.
This fixes the coverity pipeline which is broken due to flags
--no-command and --fs-capture-search no longer working.

[1] https://documentation.blackduck.com/bundle/coverity-docs-2024.3/page/coverity-analysis/topics/moving_from_filesystem_capture_to_the_coverity_cli.html

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-07-30 19:35:25 -03:00
John Johansen
37185f50a4 Merge regression: add test for making O_TMPFILE followed by linkat
The unnamed nature of an O_TMPFILE, combined with the delayed linkage of
linkat(2), creates a potential for a filesystem mediation bypass or other
unexpected file mediation behavior. Thus, add a test to verify whether or
not such a bypass occurs.

Signed-off-by: Ryan Lee <ryan.lee@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1743
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-07-30 11:09:11 +00:00
John Johansen
b40ac50f49 Merge profiles: add QtWebEngineProcess path used by Arch Linux and other distros
Arch Linux qt6-webengine has `/usr/lib/qt6/QtWebEngineProcess` and
qt5-webengine has `/usr/lib/qt/libexec/QtWebEngineProcess`.

Fedora has `/usr/lib64/qt6/libexec/QtWebEngineProcess`.

openSUSE Tumbleweed has `/usr/libexec/qt5/QtWebEngineProcess` and
`/usr/libexec/qt6/QtWebEngineProcess`.

Co-authored-by: Maxime Bélair <maxime.belair@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1726
Approved-by: Maxime Bélair <maxime.belair@canonical.com>
Merged-by: John Johansen <john@jjmx.net>
2025-07-30 08:37:10 +00:00
John Johansen
87e0151c7c Merge added systemd-creds to list of wg-quick binaries
I'd like to store my wg creds in my TPM module using `systemd-creds`:

```bash
PostUp = systemd-creds --name wg0 decrypt /etc/wireguard/secrets/wg0.cred | wg set wg0 private-key /dev/stdin
```

Currently I use `local/wg-quick` as work-around.
The `Ux` permission is may be a little too open, but 2 problems remain:

- the profile maintainer can't know which creds file need to be accessible
- different TMP module implementations / drivers may require different permissions

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1644
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-07-30 08:34:49 +00:00
Robert Stiller
b9ed931c90 added systemd-creds to list of wg-quick binaries 2025-07-30 08:34:49 +00:00
Maxime Bélair
63ce02c01d Merge logparser: add support for change_onexec logs
Add support for change_onexec logs by converting them to change_profile.
Fix associated test.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1745
Approved-by: Christian Boltz <apparmor@cboltz.de>
Merged-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-30 08:27:43 +00:00
Maxime Bélair
e82ee9f4f4 Merge aa-notify: reduce the likelihood of misuses
This MR removes some footguns in aa-notify

- Prevents the modification of special profiles
- Improve the clarity of messages
- Add support for regexes in userns_special_profiles
- Refactor get_event_type.
- Add support for regexes for special profiles
- Optimize aa-notify performances
- Minor bugfixes

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1732
Approved-by: Christian Boltz <apparmor@cboltz.de>
Merged-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-30 08:26:50 +00:00
Maxime Bélair
9ac6047f6c aa-notify: Explicitly import tkinter.font
import tkinter does not automatically import tkinter.font so calls to
the latter fail if the execution environment does not already contains
it.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-29 13:14:18 -07:00
Maxime Bélair
73f4f650e7 aa-notify: Reduce profiles updates to reduce overhead.
Profiles are now updated only at initialization and when aa-notify
itself updates a profile.

A future MR will come to read profiles individually only when an event
for this profile comes to reduce overhead, as more and more profiles are
created.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-29 13:14:18 -07:00
Maxime Bélair
12e3557896 aa-notify: Support regexes in userns_special_profiles
It is now possible to use regexes to define special profiles. unpriv_.*
is used by default.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-29 13:14:18 -07:00
Maxime Bélair
d8c57da6ba Allow aa-notify to use the priority mechanism
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-29 13:14:18 -07:00
Maxime Bélair
4de3b64e52 Add tests for get_event_type
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-29 13:14:18 -07:00
Maxime Bélair
71a71e0fa7 Create get_event_type instead of customized_message['userns']['cond']
This improves the code readability

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-29 13:14:18 -07:00
Ryan Lee
d3a49ff566 regression: add linkat_tmpfile test to task.yaml
Signed-off-by: Ryan Lee <ryan.lee@canonical.com>
2025-07-24 08:45:16 -07:00
Ryan Lee
3e7ddc1ce5 regression: add test for making O_TMPFILE followed by linkat
The unnamed nature of an O_TMPFILE, combined with the delayed linkage of
linkat(2), creates a potential for a filesystem mediation bypass. Thus,
add a test to verify whether or not such a bypass occurs.

Signed-off-by: Ryan Lee <ryan.lee@canonical.com>
2025-07-24 08:45:16 -07:00
Maxime Bélair
2448655188 logparser: add support for change_onexec logs
Add support for change_onexec logs by converting it to change_profile.
Fix associated test.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-24 13:32:13 +02:00
nl6720
f1773f4083
profiles: add QtWebEngineProcess path used by Arch Linux and other distros
Arch Linux qt6-webengine has `/usr/lib/qt6/QtWebEngineProcess` and
qt5-webengine has `/usr/lib/qt/libexec/QtWebEngineProcess`.

Fedora has `/usr/lib64/qt6/libexec/QtWebEngineProcess`.

openSUSE Tumbleweed has `/usr/libexec/qt5/QtWebEngineProcess` and
`/usr/libexec/qt6/QtWebEngineProcess`.

Co-authored-by: Maxime Bélair <maxime.belair@canonical.com>
2025-07-23 09:31:02 +03:00
Maxime Bélair
766cd2d8a5 Initial profile for nginx
Initial profile for nginx, tested on Ubuntu 24.04 manually and with
nginx testsuite.

Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2025-05-22 13:24:31 +02:00
52 changed files with 655 additions and 504 deletions

1
.gitignore vendored
View File

@ -255,6 +255,7 @@ tests/regression/apparmor/introspect
tests/regression/apparmor/io_uring
tests/regression/apparmor/link
tests/regression/apparmor/link_subset
tests/regression/apparmor/linkat_tmpfile
tests/regression/apparmor/mkdir
tests/regression/apparmor/mmap
tests/regression/apparmor/mount

View File

@ -54,9 +54,6 @@ snapshot: clean
.PHONY: coverity
coverity: snapshot
cd $(SNAPSHOT_NAME)/libraries/libapparmor && ./configure --with-python
$(foreach dir, libraries/libapparmor utils, \
cov-build --dir $(COVERITY_DIR) --no-command --fs-capture-search $(SNAPSHOT_NAME)/$(dir); \
mv $(COVERITY_DIR)/build-log.txt $(COVERITY_DIR)/build-log-python-$(subst /,.,$(dir)).txt ;)
cov-build --dir $(COVERITY_DIR) -- sh -c \
"$(foreach dir, $(filter-out utils profiles tests, $(DIRS)), \
$(MAKE) -j $$(nproc) -C $(SNAPSHOT_NAME)/$(dir);) "

View File

@ -1 +1 @@
4.1.0~beta1
5.0.0~alpha1

View File

@ -1,2 +1,4 @@
profile unconfined {
change_profile -> system_tor,
}

View File

@ -203,7 +203,7 @@ bool aare_rules::append_rule(const char *rule, bool oob, bool with_perm,
CHFA *aare_rules::create_chfa(int *min_match_len,
vector <aa_perms> &perms_table,
optflags const &opts, bool filedfa,
bool extended_perms, bool prompt)
bool extended_perms)
{
/* finish constructing the expr tree from the different permission
* set nodes */
@ -315,7 +315,7 @@ CHFA *aare_rules::create_chfa(int *min_match_len,
//cerr << "Checking extended perms " << extended_perms << "\n";
if (extended_perms) {
//cerr << "creating permstable\n";
dfa.compute_perms_table(perms_table, prompt);
dfa.compute_perms_table(perms_table);
// TODO: move perms table to a class
if (opts.dump & DUMP_DFA_TRANS_TABLE && perms_table.size()) {
cerr << "Perms Table size: " << perms_table.size() << "\n";
@ -329,7 +329,7 @@ CHFA *aare_rules::create_chfa(int *min_match_len,
cerr << "\n";
}
}
chfa = new CHFA(dfa, eq, opts, extended_perms, prompt);
chfa = new CHFA(dfa, eq, opts, extended_perms);
if (opts.dump & DUMP_DFA_TRANS_TABLE)
chfa->dump(cerr);
if (opts.dump & DUMP_DFA_COMPTRESSED_STATES)
@ -350,15 +350,14 @@ CHFA *aare_rules::create_chfa(int *min_match_len,
void *aare_rules::create_dfablob(size_t *size, int *min_match_len,
vector <aa_perms> &perms_table,
optflags const &opts, bool filedfa,
bool extended_perms, bool prompt)
bool extended_perms)
{
char *buffer = NULL;
stringstream stream;
try {
CHFA *chfa = create_chfa(min_match_len, perms_table,
opts, filedfa, extended_perms,
prompt);
opts, filedfa, extended_perms);
if (!chfa) {
*size = 0;
return NULL;
@ -383,82 +382,3 @@ void *aare_rules::create_dfablob(size_t *size, int *min_match_len,
return buffer;
}
/* 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
* that can match the dfa for determining xmatch priority.
*/
void *aare_rules::create_welded_dfablob(aare_rules *file_rules,
size_t *size, int *min_match_len,
size_t *new_start,
vector <aa_perms> &perms_table,
optflags const &opts,
bool extended_perms, bool prompt)
{
int file_min_len;
vector <aa_perms> file_perms;
CHFA *file_chfa;
try {
file_chfa = file_rules->create_chfa(&file_min_len,
file_perms, opts,
true, extended_perms, prompt);
if (!file_chfa) {
*size = 0;
return NULL;
}
}
catch(int error) {
*size = 0;
return NULL;
}
CHFA *policy_chfa;
try {
policy_chfa = create_chfa(min_match_len,
perms_table, opts,
false, extended_perms, prompt);
if (!policy_chfa) {
delete file_chfa;
*size = 0;
return NULL;
}
}
catch(int error) {
delete file_chfa;
*size = 0;
return NULL;
}
stringstream stream;
try {
policy_chfa->weld_file_to_policy(*file_chfa, *new_start,
extended_perms, prompt,
perms_table, file_perms);
policy_chfa->flex_table(stream, opts);
}
catch(int error) {
delete (file_chfa);
delete (policy_chfa);
*size = 0;
return NULL;
}
delete file_chfa;
delete policy_chfa;
/* write blob to buffer */
stringbuf *buf = stream.rdbuf();
buf->pubseekpos(0);
*size = buf->in_avail();
if (file_min_len < *min_match_len)
*min_match_len = file_min_len;
char *buffer = (char *)malloc(*size);
if (!buffer)
return NULL;
buf->sgetn(buffer, *size);
return buffer;
}

View File

@ -123,17 +123,11 @@ class aare_rules {
CHFA *create_chfa(int *min_match_len,
std::vector <aa_perms> &perms_table,
optflags const &opts, bool filedfa,
bool extended_perms, bool prompt);
bool extended_perms);
void *create_dfablob(size_t *size, int *min_match_len,
std::vector <aa_perms> &perms_table,
optflags const &opts,
bool filedfa, bool extended_perms, bool prompt);
void *create_welded_dfablob(aare_rules *file_rules,
size_t *size, int *min_match_len,
size_t *new_start,
std::vector <aa_perms> &perms_table,
optflags const &opts,
bool extended_perms, bool prompt);
bool filedfa, bool extended_perms);
};
#endif /* __LIBAA_RE_RULES_H */

View File

@ -59,7 +59,7 @@ void CHFA::init_free_list(vector<pair<size_t, size_t> > &free_list,
* permtable index flag
*/
CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
bool permindex, bool prompt): eq(eq)
bool permindex): eq(eq)
{
if (opts.dump & DUMP_DFA_TRANS_PROGRESS)
fprintf(stderr, "Compressing HFA:\r");
@ -118,12 +118,10 @@ CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
accept2.resize(max(dfa.states.size(), (size_t) 2));
dfa.nonmatching->map_perms_to_accept(accept[0],
accept2[0],
accept3,
prompt);
accept3);
dfa.start->map_perms_to_accept(accept[1],
accept2[1],
accept3,
prompt);
accept3);
}
next_check.resize(max(optimal, (size_t) dfa.max_range));
free_list.resize(next_check.size());
@ -147,8 +145,7 @@ CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
else
(*i)->map_perms_to_accept(accept[num.size()],
accept2[num.size()],
accept3,
prompt);
accept3);
num.insert(make_pair(*i, num.size()));
}
if (opts.dump & (DUMP_DFA_TRANS_PROGRESS)) {
@ -170,8 +167,7 @@ CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
else
i->second->map_perms_to_accept(accept[num.size()],
accept2[num.size()],
accept3,
prompt);
accept3);
num.insert(make_pair(i->second, num.size()));
}
if (opts.dump & (DUMP_DFA_TRANS_PROGRESS)) {
@ -519,116 +515,3 @@ void CHFA::flex_table(ostream &os, optflags const &opts) {
flex_table_serialize<uint16_t>(*this, os, (1 << 16) - 1);
}
}
/*
* @file_chfa: chfa to add on to the policy chfa
* @new_start: new start state for where the @file_dfa is in the new chfa
*
* Make a new chfa that is a combination of policy and file chfas. It
* assumes policy is built with AA_CLASS_FILE support transition. The
* resultant chfa will have file states and indexes offset except for
* start and null states.
*
* NOTE:
* - modifies chfa
* requires:
* - no ec
* - policy chfa has transitions state[start].next[AA_CLASS_FILE]
* - policy perms table is build if using permstable
*/
void CHFA::weld_file_to_policy(CHFA &file_chfa, size_t &new_start,
bool accept_idx, bool prompt,
vector <aa_perms> &policy_perms,
vector <aa_perms> &file_perms)
{
// doesn't support remapping eq classes yet
if (eq.size() > 0 || file_chfa.eq.size() > 0)
throw 1;
size_t old_base_size = default_base.size();
size_t old_next_size = next_check.size();
const State *nonmatching = default_base[0].first;
//const State *start = default_base[1].first;
const State *file_nonmatching = file_chfa.default_base[0].first;
// renumber states from file_dfa by appending to policy dfa
num.insert(make_pair(file_nonmatching, 0)); // remap to policy nonmatching
for (map<const State *, size_t>::iterator i = file_chfa.num.begin(); i != file_chfa.num.end() ; i++) {
if (i->first == file_nonmatching)
continue;
num.insert(make_pair(i->first, i->second + old_base_size));
}
// handle default and base table expansion, and setup renumbering
// while we remap file_nonmatch within the table, we still keep its
// slot.
bool first = true;
for (DefaultBase::iterator i = file_chfa.default_base.begin(); i != file_chfa.default_base.end(); i++) {
const State *def;
size_t base;
if (first) {
first = false;
// remap file_nonmatch to nonmatch
def = nonmatching;
base = 0;
} else {
def = i->first;
base = i->second + old_next_size;
}
default_base.push_back(make_pair(def, base));
}
// mapping for these are handled by num[]
for (NextCheck::iterator i = file_chfa.next_check.begin(); i != file_chfa.next_check.end(); i++) {
next_check.push_back(*i);
}
// append file perms to policy perms, and rework permsidx if needed
if (accept_idx) {
// policy idx double
// file + doubled offset
// Requires: policy perms table, so we can double and
// update indexes
// * file perm idx to start on even idx
// * policy perms table size to double and entries
// to repeat
assert(accept.size() == old_base_size);
accept.resize(accept.size() + file_chfa.accept.size());
assert(policy_perms.size() < std::numeric_limits<ssize_t>::max());
ssize_t size = (ssize_t) policy_perms.size();
policy_perms.resize(size*2 + file_perms.size());
// shift and double the policy perms
for (ssize_t i = size - 1; i >= 0; i--) {
policy_perms[i*2] = policy_perms[i];
policy_perms[i*2 + 1] = policy_perms[i];
}
// update policy accept idx for the new shifted perms table
for (size_t i = 0; i < old_base_size; i++) {
accept[i] = accept[i]*2;
}
// copy over file perms
for (size_t i = 0; i < file_perms.size(); i++) {
policy_perms[size*2 + i] = file_perms[i];
}
// shift file accept indexs
for (size_t i = 0; i < file_chfa.accept.size(); i++) {
accept[old_base_size + i] = file_chfa.accept[i] + size*2;
}
} else {
// perms are stored in accept just append the perms
size_t size = accept.size();
accept.resize(size + file_chfa.accept.size());
accept2.resize(size + file_chfa.accept.size());
for (size_t i = 0; i < file_chfa.accept.size(); i++) {
accept[size + i] = file_chfa.accept[i];
accept2[size + i] = file_chfa.accept2[i];
}
}
// Rework transition state[start].next[AA_CLASS_FILE]
next_check[default_base[1].second + AA_CLASS_FILE].first = file_chfa.start;
new_start = num[file_chfa.start];
}

View File

@ -39,7 +39,7 @@ class CHFA {
public:
CHFA(void);
CHFA(DFA &dfa, std::map<transchar, transchar> &eq, optflags const &opts,
bool permindex, bool prompt);
bool permindex);
void dump(ostream & os);
void flex_table(ostream &os, optflags const &opts);
void init_free_list(std::vector<std::pair<size_t, size_t> > &free_list,
@ -48,10 +48,6 @@ class CHFA {
StateTrans &cases);
void insert_state(std::vector<std::pair<size_t, size_t> > &free_list,
State *state, DFA &dfa);
void weld_file_to_policy(CHFA &file_chfa, size_t &new_start,
bool accept_idx, bool prompt,
std::vector <aa_perms> &policy_perms,
std::vector <aa_perms> &file_perms);
// private:
// sigh templates suck, friend declaration does not work so for now

View File

@ -334,7 +334,16 @@ State *DFA::add_new_state(optflags const &opts, NodeSet *anodes,
ProtoState proto;
proto.init(nnodev, anodev);
State *state = new State(opts, node_map.size(), proto, other, filedfa);
State *state;
try {
state = new State(opts, node_map.size(), proto, other, filedfa);
} catch(int error) {
/* this function is called in the DFA object creation,
* and the exception prevents the destructor from
* being called, so call the helper here */
cleanup();
throw error;
}
pair<NodeMap::iterator,bool> x = node_map.insert(proto, state);
if (x.second == false) {
delete state;
@ -392,7 +401,17 @@ void DFA::update_state_transitions(optflags const &opts, State *state)
*/
for (Cases::iterator j = cases.begin(); j != cases.end(); j++) {
State *target;
target = add_new_state(opts, j->second, nonmatching);
try {
target = add_new_state(opts, j->second, nonmatching);
} catch (int error) {
/* when add_new_state fails, there could still
* be NodeSets in the rest of cases, so clean
* them up before re-throwing the exception */
for (Cases::iterator k = ++j; k != cases.end(); k++) {
delete k->second;
}
throw error;
}
/* Don't insert transition that the otherwise transition
* already covers
@ -522,11 +541,7 @@ DFA::DFA(Node *root, optflags const &opts, bool buildfiledfa): root(root), filed
DFA::~DFA()
{
anodes_cache.clear();
nnodes_cache.clear();
for (Partition::iterator i = states.begin(); i != states.end(); i++)
delete *i;
cleanup();
}
State *DFA::match_len(State *state, const char *str, size_t len)
@ -1367,13 +1382,12 @@ void DFA::apply_equivalence_classes(map<transchar, transchar> &eq)
}
void DFA::compute_perms_table_ent(State *state, size_t pos,
vector <aa_perms> &perms_table,
bool prompt)
vector <aa_perms> &perms_table)
{
uint32_t accept1, accept2, accept3;
// until front end doesn't map the way it does
state->map_perms_to_accept(accept1, accept2, accept3, prompt);
state->map_perms_to_accept(accept1, accept2, accept3);
if (filedfa) {
state->idx = pos * 2;
perms_table[pos*2] = compute_fperms_user(accept1, accept2, accept3);
@ -1384,7 +1398,7 @@ void DFA::compute_perms_table_ent(State *state, size_t pos,
}
}
void DFA::compute_perms_table(vector <aa_perms> &perms_table, bool prompt)
void DFA::compute_perms_table(vector <aa_perms> &perms_table)
{
size_t mult = filedfa ? 2 : 1;
size_t pos = 2;
@ -1393,13 +1407,13 @@ void DFA::compute_perms_table(vector <aa_perms> &perms_table, bool prompt)
perms_table.resize(states.size() * mult);
// nonmatching and start need to be 0 and 1 so handle outside of loop
compute_perms_table_ent(nonmatching, 0, perms_table, prompt);
compute_perms_table_ent(start, 1, perms_table, prompt);
compute_perms_table_ent(nonmatching, 0, perms_table);
compute_perms_table_ent(start, 1, perms_table);
for (Partition::iterator i = states.begin(); i != states.end(); i++) {
if (*i == nonmatching || *i == start)
continue;
compute_perms_table_ent(*i, pos, perms_table, prompt);
compute_perms_table_ent(*i, pos, perms_table);
pos++;
}
}

View File

@ -289,13 +289,10 @@ public:
int apply_and_clear_deny(void) { return perms.apply_and_clear_deny(); }
void map_perms_to_accept(perm32_t &accept1, perm32_t &accept2,
perm32_t &accept3, bool prompt)
perm32_t &accept3)
{
accept1 = perms.allow;
if (prompt && prompt_compat_mode == PROMPT_COMPAT_DEV)
accept2 = PACK_AUDIT_CTL(perms.prompt, perms.quiet);
else
accept2 = PACK_AUDIT_CTL(perms.audit, perms.quiet);
accept2 = PACK_AUDIT_CTL(perms.audit, perms.quiet);
accept3 = perms.prompt;
}
@ -371,6 +368,15 @@ class DFA {
NodeMap node_map;
std::list<State *> work_queue;
void cleanup(void) {
anodes_cache.clear();
nnodes_cache.clear();
for (Partition::iterator i = states.begin(); i != states.end(); i++) {
delete *i;
}
states.clear();
}
public:
DFA(Node *root, optflags const &flags, bool filedfa);
virtual ~DFA();
@ -399,10 +405,8 @@ public:
void apply_equivalence_classes(std::map<transchar, transchar> &eq);
void compute_perms_table_ent(State *state, size_t pos,
std::vector <aa_perms> &perms_table,
bool prompt);
void compute_perms_table(std::vector <aa_perms> &perms_table,
bool prompt);
std::vector <aa_perms> &perms_table);
void compute_perms_table(std::vector <aa_perms> &perms_table);
unsigned int diffcount;
int oob_range;

View File

@ -133,8 +133,7 @@ struct aa_perms compute_fperms_user(uint32_t accept1, uint32_t accept2,
perms.prompt = map_old_perms(dfa_user_allow(accept3));
perms.audit = map_old_perms(dfa_user_audit(accept1, accept2));
perms.quiet = map_old_perms(dfa_user_quiet(accept1, accept2));
if (prompt_compat_mode != PROMPT_COMPAT_PERMSV1)
perms.xindex = dfa_user_xindex(accept1);
perms.xindex = dfa_user_xindex(accept1);
compute_fperms_allow(&perms, accept1);
perms.prompt &= ~(perms.allow | perms.deny);
@ -150,8 +149,7 @@ struct aa_perms compute_fperms_other(uint32_t accept1, uint32_t accept2,
perms.prompt = map_old_perms(dfa_other_allow(accept3));
perms.audit = map_old_perms(dfa_other_audit(accept1, accept2));
perms.quiet = map_old_perms(dfa_other_quiet(accept1, accept2));
if (prompt_compat_mode != PROMPT_COMPAT_PERMSV1)
perms.xindex = dfa_other_xindex(accept1);
perms.xindex = dfa_other_xindex(accept1);
compute_fperms_allow(&perms, accept1);
perms.prompt &= ~(perms.allow | perms.deny);
@ -165,12 +163,6 @@ static uint32_t map_other(uint32_t x)
((x & 0x60) << 19); /* SETOPT/GETOPT */
}
static uint32_t map_xbits(uint32_t x)
{
return ((x & 0x1) << 7) |
((x & 0x7e) << 9);
}
struct aa_perms compute_perms_entry(uint32_t accept1, uint32_t accept2,
uint32_t accept3)
// don't need to worry about version internally within the parser

View File

@ -570,6 +570,8 @@ ostream &mnt_rule::dump(ostream &os)
{
prefix_rule_t::dump(os);
std::ios::fmtflags fmt(os.flags());
if (perms & AA_MAY_MOUNT)
os << "mount";
else if (perms & AA_MAY_UMOUNT)
@ -603,6 +605,7 @@ ostream &mnt_rule::dump(ostream &os)
os << " " << "(0x" << hex << perms << "/0x" << (audit != AUDIT_UNSPECIFIED ? perms : 0) << ")";
os << ",\n";
os.flags(fmt);
return os;
}
@ -1163,21 +1166,7 @@ fail:
void mnt_rule::post_parse_profile(Profile &prof)
{
if (trans) {
perm32_t perms = 0;
int n = add_entry_to_x_table(&prof, trans);
if (!n) {
PERROR("Profile %s has too many specified profile transitions.\n", prof.name);
exit(1);
}
if (perms & AA_USER_EXEC)
perms |= SHIFT_PERMS(n << 10, AA_USER_SHIFT);
if (perms & AA_OTHER_EXEC)
perms |= SHIFT_PERMS(n << 10, AA_OTHER_SHIFT);
perms = ((perms & ~AA_ALL_EXEC_MODIFIERS) |
(perms & AA_ALL_EXEC_MODIFIERS));
trans = NULL;
/* TODO: pivot_root profile transition */
}
}

View File

@ -133,8 +133,10 @@ static void process_entries(const void *nodep, VISIT value, int level unused)
if (entry->link_name &&
strncmp((*t)->from, entry->link_name, len) == 0) {
char *n = do_alias(*t, entry->link_name);
if (!n)
if (!n) {
free_cod_entries(dup);
return;
}
if (!dup)
dup = copy_cod_entry(entry);
free(dup->link_name);

View File

@ -185,19 +185,9 @@ bool prompt_compat_mode_supported(int mode)
if (mode == PROMPT_COMPAT_PERMSV2 &&
(kernel_supports_permstable32 && !kernel_supports_permstable32_v1))
return true;
/*
else if (mode == PROMPT_COMPAT_DEV &&
kernel_supports_promptdev)
return true;
*/
else if (mode == PROMPT_COMPAT_FLAG &&
kernel_supports_permstable32)
return true;
/*
else if (mode == PROMPT_COMPAT_PERMSV1 &&
(kernel_supports_permstable32_v1))
return true;
*/
else if (mode == PROMPT_COMPAT_IGNORE)
return true;
@ -208,12 +198,8 @@ int default_prompt_compat_mode()
{
if (prompt_compat_mode_supported(PROMPT_COMPAT_PERMSV2))
return PROMPT_COMPAT_PERMSV2;
if (prompt_compat_mode_supported(PROMPT_COMPAT_DEV))
return PROMPT_COMPAT_DEV;
if (prompt_compat_mode_supported(PROMPT_COMPAT_FLAG))
return PROMPT_COMPAT_FLAG;
if (prompt_compat_mode_supported(PROMPT_COMPAT_PERMSV1))
return PROMPT_COMPAT_PERMSV1;
if (prompt_compat_mode_supported(PROMPT_COMPAT_IGNORE))
return PROMPT_COMPAT_IGNORE;
return PROMPT_COMPAT_IGNORE;
@ -231,12 +217,6 @@ void print_prompt_compat_mode(FILE *f)
case PROMPT_COMPAT_PERMSV2:
fprintf(f, "permsv2");
break;
case PROMPT_COMPAT_PERMSV1:
fprintf(f, "permsv1");
break;
case PROMPT_COMPAT_DEV:
fprintf(stderr, "dev");
break;
default:
fprintf(f, "Unknown prompt compat mode '%d'", prompt_compat_mode);
}

View File

@ -384,13 +384,11 @@ void sd_serialize_rlimits(std::ostringstream &buf, struct aa_rlimits *limits)
sd_write_structend(buf);
}
void sd_serialize_xtable(std::ostringstream &buf, char **table,
size_t min_size)
void sd_serialize_xtable(std::ostringstream &buf, char **table)
{
size_t count;
size_t size;
if (!table[4] && min_size == 0)
if (!table[4])
return;
sd_write_struct(buf, "xtable");
count = 0;
@ -399,9 +397,7 @@ void sd_serialize_xtable(std::ostringstream &buf, char **table,
count++;
}
size = max(min_size, count);
sd_write_array(buf, NULL, size);
sd_write_array(buf, NULL, count);
for (size_t i = 4; i < count + 4; i++) {
size_t len = strlen(table[i]) + 1;
@ -414,13 +410,6 @@ void sd_serialize_xtable(std::ostringstream &buf, char **table,
}
sd_write_strn(buf, table[i], len, NULL);
}
if (min_size > count) {
//fprintf(stderr, "Adding padding to xtable count %lu, min %lu\n", count, min_size);
for (; count < min_size; count++) {
/* fill with null strings */
sd_write_strn(buf, "\000", 1, NULL);
}
}
sd_write_arrayend(buf);
sd_write_structend(buf);
@ -554,38 +543,17 @@ void sd_serialize_profile(std::ostringstream &buf, Profile *profile,
sd_serialize_dfa(buf, profile->policy.dfa, profile->policy.size,
profile->policy.perms_table);
if (kernel_supports_permstable32) {
sd_serialize_xtable(buf, profile->exec_table,
profile->uses_prompt_rules &&
prompt_compat_mode == PROMPT_COMPAT_PERMSV1 ?
profile->policy.perms_table.size() : 0);
sd_serialize_xtable(buf, profile->exec_table);
}
sd_write_structend(buf);
}
/* either have a single dfa or lists of different entry types */
if (profile->uses_prompt_rules && prompt_compat_mode == PROMPT_COMPAT_PERMSV1) {
/* special compat mode to work around verification problem */
sd_serialize_dfa(buf, profile->policy.dfa, profile->policy.size,
profile->policy.perms_table);
sd_write_name(buf, "dfa_start");
sd_write_uint32(buf, profile->policy.file_start);
if (profile->policy.dfa) {
// fprintf(stderr, "profile %s: policy xtable\n", profile->name);
// TODO: this is dummy exec make dependent on V1
sd_serialize_xtable(buf, profile->exec_table,
//permstable32_v1 workaround
profile->policy.perms_table.size());
}
} else {
sd_serialize_dfa(buf, profile->dfa.dfa, profile->dfa.size,
profile->dfa.perms_table);
if (profile->dfa.dfa) {
// fprintf(stderr, "profile %s: dfa xtable\n", profile->name);
sd_serialize_xtable(buf, profile->exec_table,
//??? work around
profile->dfa.perms_table.size());
}
sd_serialize_dfa(buf, profile->dfa.dfa, profile->dfa.size,
profile->dfa.perms_table);
if (profile->dfa.dfa) {
// fprintf(stderr, "profile %s: dfa xtable\n", profile->name);
sd_serialize_xtable(buf, profile->exec_table);
}
sd_write_structend(buf);
}

View File

@ -797,12 +797,8 @@ static int process_arg(int c, char *optarg)
case ARG_PROMPT_COMPAT:
if (strcmp(optarg, "permsv2") == 0) {
prompt_compat_mode = PROMPT_COMPAT_PERMSV2;
} else if (strcmp(optarg, "permsv1") == 0) {
prompt_compat_mode = PROMPT_COMPAT_PERMSV1;
} else if (strcmp(optarg, "default") == 0) {
prompt_compat_mode = default_prompt_compat_mode();
} else if (strcmp(optarg, "dev") == 0) {
prompt_compat_mode = PROMPT_COMPAT_DEV;
} else if (strcmp(optarg, "ignore") == 0) {
prompt_compat_mode = PROMPT_COMPAT_IGNORE;
} else if (strcmp(optarg, "flag") == 0) {

View File

@ -244,10 +244,7 @@ int post_process_profile(Profile *profile, int debug_only)
error = post_process_policy_list(profile->hat_table, debug_only);
if (prompt_compat_mode == PROMPT_COMPAT_DEV && profile->uses_prompt_rules)
profile->flags.flags |= FLAG_PROMPT_COMPAT;
else if (prompt_compat_mode == PROMPT_COMPAT_FLAG && profile->uses_prompt_rules)
if (prompt_compat_mode == PROMPT_COMPAT_FLAG && profile->uses_prompt_rules)
profile->flags.mode = MODE_PROMPT;
return error;

View File

@ -578,7 +578,7 @@ build:
*
* we don't need to build xmatch for permstable32, so don't
*/
prof->xmatch = rules->create_dfablob(&prof->xmatch_size, &prof->xmatch_len, prof->xmatch_perms_table, parseopts, false, false, false);
prof->xmatch = rules->create_dfablob(&prof->xmatch_size, &prof->xmatch_len, prof->xmatch_perms_table, parseopts, false, false);
delete rules;
if (!prof->xmatch)
return false;
@ -785,28 +785,17 @@ int process_profile_regex(Profile *prof)
/* under permstable32_v1 we weld file and policydb together, so
* don't create the file blob here
*/
if (prof->dfa.rules->rule_count > 0 && prompt_compat_mode != PROMPT_COMPAT_PERMSV1) {
if (prof->dfa.rules->rule_count > 0) {
int xmatch_len = 0;
//fprintf(stderr, "Creating file DFA %d\n", kernel_supports_permstable32);
prof->dfa.dfa = prof->dfa.rules->create_dfablob(&prof->dfa.size,
&xmatch_len, prof->dfa.perms_table,
parseopts, true,
kernel_supports_permstable32,
prof->uses_prompt_rules);
kernel_supports_permstable32);
delete prof->dfa.rules;
prof->dfa.rules = NULL;
if (!prof->dfa.dfa)
goto out;
/*
if (prof->dfa_size == 0) {
PERROR(_("profile %s: has merged rules (%s) with "
"multiple x modifiers\n"),
prof->name, (char *) prof->dfa);
free(prof->dfa);
prof->dfa = NULL;
goto out;
}
*/
}
error = 0;
@ -1081,7 +1070,6 @@ static const char *mediates_ns = CLASS_STR(AA_CLASS_NS);
static const char *mediates_posix_mqueue = CLASS_STR(AA_CLASS_POSIX_MQUEUE);
static const char *mediates_sysv_mqueue = CLASS_STR(AA_CLASS_SYSV_MQUEUE);
static const char *mediates_io_uring = CLASS_STR(AA_CLASS_IO_URING);
static const char *deny_file = ".*";
/* Set the mediates priority to the maximum possible. This is to help
* ensure that the mediates information is not wiped out by a rule
@ -1164,44 +1152,13 @@ int process_profile_policydb(Profile *prof)
goto out;
}
if (prompt_compat_mode == PROMPT_COMPAT_PERMSV1) {
// MUST have file and policy
// This requires file rule processing happen first
if (!prof->dfa.rules->rule_count) {
// add null dfa
if (!prof->dfa.rules->add_rule(deny_file, 0, RULE_DENY, AA_MAY_READ, 0, parseopts))
goto out;
}
if (!prof->policy.rules->rule_count) {
if (!prof->policy.rules->add_rule(mediates_file, 0, RULE_DENY, AA_MAY_READ, 0, parseopts))
goto out;
}
int xmatch_len = 0;
prof->policy.dfa = prof->policy.rules->create_welded_dfablob(
prof->dfa.rules,
&prof->policy.size,
&xmatch_len,
&prof->policy.file_start,
prof->policy.perms_table, parseopts,
kernel_supports_permstable32_v1,
prof->uses_prompt_rules);
delete prof->policy.rules;
delete prof->dfa.rules;
prof->policy.rules = NULL;
prof->dfa.rules = NULL;
if (!prof->policy.dfa)
goto out;
} else if (prof->policy.rules->rule_count > 0 &&
// yes not needed as covered above, just making sure
// this doesn't get messed up in the future
prompt_compat_mode != PROMPT_COMPAT_PERMSV1) {
if (prof->policy.rules->rule_count > 0) {
int xmatch_len = 0;
prof->policy.dfa = prof->policy.rules->create_dfablob(&prof->policy.size,
&xmatch_len,
prof->policy.perms_table,
parseopts, false,
kernel_supports_permstable32,
prof->uses_prompt_rules);
kernel_supports_permstable32);
delete prof->policy.rules;
prof->policy.rules = NULL;

View File

@ -188,24 +188,21 @@ cleanup:
if (prof->attachment) {
tmp = symtab::delete_var(PROFILE_EXEC_VAR);
delete tmp;
if (saved_exec_path) {
if (saved_exec_path)
symtab::add_var(*saved_exec_path);
delete saved_exec_path;
}
}
cleanup_attach:
if (prof->attachment) {
tmp = symtab::delete_var(PROFILE_ATTACH_VAR);
delete tmp;
if (saved_attach_path) {
if (saved_attach_path)
symtab::add_var(*saved_attach_path);
delete saved_attach_path;
}
}
cleanup_name:
tmp = symtab::delete_var(PROFILE_NAME_VARIABLE);
delete tmp;
delete saved_exec_path;
delete saved_attach_path;
out:
return error;
}

View File

@ -577,6 +577,7 @@ flags: opt_flags TOK_OPENPAREN flagvals TOK_CLOSEPAREN
flagvals: flagvals flagval
{
$1.merge($2);
$2.clear();
$$ = $1;
};

View File

@ -78,6 +78,7 @@ void ProfileList::dump_profile_names(bool children)
Profile::~Profile()
{
hat_table.clear();
flags.clear();
free_cod_entries(entries);
free_cond_entry_list(xattrs);
@ -97,10 +98,6 @@ Profile::~Profile()
free(name);
if (attachment)
free(attachment);
if (flags.disconnected_path)
free(flags.disconnected_path);
if (flags.disconnected_ipc)
free(flags.disconnected_ipc);
if (ns)
free(ns);
for (int i = (AA_EXEC_LOCAL >> 10) + 1; i < AA_EXEC_COUNT; i++)

View File

@ -175,6 +175,12 @@ public:
signal = 0;
error = 0;
}
void clear(void) {
free(disconnected_path);
free(disconnected_ipc);
}
void init(const char *str)
{
init();
@ -301,7 +307,7 @@ public:
}
// same ignore rhs.disconnect_path
} else {
disconnected_path = rhs.disconnected_path;
disconnected_path = strdup(rhs.disconnected_path);
}
}
if (rhs.disconnected_ipc) {
@ -311,7 +317,7 @@ public:
}
// same so do nothing
} else {
disconnected_ipc = rhs.disconnected_ipc;
disconnected_ipc = strdup(rhs.disconnected_ipc);
}
}
if (rhs.signal) {

View File

@ -28,9 +28,7 @@
#define PROMPT_COMPAT_UNKNOWN 0
#define PROMPT_COMPAT_IGNORE 1
#define PROMPT_COMPAT_PERMSV2 2
#define PROMPT_COMPAT_DEV 3
#define PROMPT_COMPAT_FLAG 4
#define PROMPT_COMPAT_PERMSV1 5
class Profile;
@ -433,11 +431,14 @@ public:
ostream &dump(ostream &os) override {
class_rule_t::dump(os);
std::ios::fmtflags fmt(os.flags());
if (saved)
os << "(0x" << std::hex << perms << "/orig " << saved << ") ";
else
os << "(0x" << std::hex << perms << ") ";
os.flags(fmt);
return os;
}
@ -462,7 +463,11 @@ public:
ostream &dump(ostream &os) override {
class_rule_t::dump(os);
std::ios::fmtflags fmt(os.flags());
os << "(0x" << std::hex << perms << ") ";
os.flags(fmt);
return os;
}

View File

@ -0,0 +1,7 @@
#=DESCRIPTION expansion of alternation after extra, unescaped @
#=EXRESULT PASS
@{uid} = {[0-9],[1-9][0-9]}
/usr/bin/foo {
/sys/fs/cgroup/user.slice/user-@{uid}.slice/user@@{uid}.service/cpu.max r,
}

View File

@ -189,11 +189,23 @@ static void trim_trailing_slash(std::string& str)
str.clear(); // str is all '/'
}
int copy_value_to_name(const std::string& value, char **name)
{
free(*name);
*name = strdup(value.c_str());
if (!*name) {
errno = ENOMEM;
return -1;
}
return 0;
}
int variable::expand_by_alternation(char **name)
{
std::string expanded_name = "";
bool filter_leading_slash = false;
bool filter_trailing_slash = false;
int ret = 0;
if (!name) {
PERROR("ASSERT: name to be expanded cannot be NULL\n");
@ -226,8 +238,6 @@ int variable::expand_by_alternation(char **name)
return 1;
}
free(*name);
size_t setsize = ref->expanded.size();
auto i = ref->expanded.begin();
@ -252,15 +262,19 @@ int variable::expand_by_alternation(char **name)
if (setsize > 1) {
expanded_name += "}";
}
expanded_name = prefix + expanded_name + suffix;
*name = strdup(expanded_name.c_str());
if (!*name) {
errno = ENOMEM;
return -1;
}
/* don't include prefix */
expanded_name = expanded_name + suffix;
ret = copy_value_to_name(expanded_name, name);
if (ret)
return ret;
/* recursive until no variables are found in *name */
return expand_by_alternation(name);
ret = expand_by_alternation(name);
if (ret == 0) {
/* return prefix to name */
expanded_name = prefix + std::string(*name);
ret = copy_value_to_name(expanded_name, name);
}
return ret;
}
int variable::expand_variable()
@ -293,6 +307,7 @@ int variable::expand_variable()
}
name = variable::process_var(var.c_str());
variable *ref = symtab::lookup_existing_symbol(name);
free(name);
if (!ref) {
PERROR("Failed to find declaration for: %s\n", var.c_str());
rc = 1;
@ -322,7 +337,6 @@ int variable::expand_variable()
}
out:
free(name);
expanding = false;
return rc;
}

View File

@ -4,7 +4,7 @@
abi <abi/4.0>,
include <tunables/global>
profile QtWebEngineProcess /usr/lib/@{multiarch}/qt{5,6}/libexec/QtWebEngineProcess flags=(unconfined) {
profile QtWebEngineProcess /usr/lib{,64,exec}/{,@{multiarch}/}qt{,5,6}/{,libexec/}QtWebEngineProcess flags=(unconfined) {
userns,
@{exec_path} mr,

View File

@ -35,6 +35,10 @@
owner @{HOME}/.gtkrc r,
owner @{HOME}/.gtkrc-2.0 r,
owner @{HOME}/.gtk-bookmarks r,
owner @{HOME}/.cache/gtk-4.0/ rw,
owner @{HOME}/.cache/gtk-4.0/vulkan-pipeline-cache/{,*} rw,
owner @{HOME}/.config/gtkrc r,
owner @{HOME}/.config/gtkrc-2.0 r,
owner @{HOME}/.config/gtk-{3,4}.0/ rw,

View File

@ -11,12 +11,20 @@
abi <abi/4.0>,
# this abstract profile can be included by applications that are
# dynamically linked to libnuma
# This abstract profile can be included by applications that are
# dynamically linked to libnuma.
# libnuma defines the function num_init() as the .init function
# to be called by the runtime linker (ld) when libnuma is loaded
# even if not any active usage of libnuma takes place
@{sys}/devices/system/cpu/node/ r,
# Actually using libnuma functionality will need a few more
# sysfs entries to gather information about the system
@{sys}/devices/system/cpu/ r,
@{sys}/devices/system/node/node[0-9]*/meminfo r,
@{sys}/devices/system/node/*/cpumap r,
# Include additions to the abstraction
include if exists <abstractions/libnuma.d>

View File

@ -25,6 +25,7 @@
@{run}/systemd/userdb/io.systemd.Home rw, # systemd-home dirs
@{run}/systemd/userdb/io.systemd.NameServiceSwitch rw, # UNIX/glibc NSS
@{run}/systemd/userdb/io.systemd.Machine rw, # systemd-machined
@{run}/systemd/userdb/org.gnome.DisplayManager rw, # GDM implements a user database for its greeter
@{PROC}/sys/kernel/random/boot_id r,

View File

@ -27,6 +27,9 @@ profile curl /usr/bin/curl {
# (see --config, --cacert options)
file r @{HOME}/**,
# allow reading data/config from tmp
owner file r /tmp/**,
# allow writing output to $HOME, /tmp (see -o option)
file w @{HOME}/**,
file w /tmp/**,

67
profiles/apparmor.d/nginx Normal file
View File

@ -0,0 +1,67 @@
#------------------------------------------------------------------
# Copyright (C) 2025 Canonical Ltd.
#
# Author: Maxime Bélair <maxime.belair@canonical.com>
#
# 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.
#------------------------------------------------------------------
# vim: ft=apparmor
abi <abi/4.0>,
include <tunables/global>
# Standard config for webservers. This assumes that the server uses one of these directories.
# If it has been modified, change accordingly.
# Web directory location is available at /etc/nginx/sites-available/default
@{srv}=/var/www/html/** /srv/**
profile nginx /usr/{s,}bin/nginx {
include <abstractions/base>
include <abstractions/nameservice-strict>
include <abstractions/openssl>
include <abstractions/ssl_keys>
capability dac_override,
capability dac_read_search,
capability net_bind_service,
capability setgid,
capability setuid,
network inet stream,
network inet6 stream,
network inet dgram,
network inet6 dgram,
network netlink raw,
# Configuration
file /etc/nginx/** r,
# Server directory
file @{srv} r,
# Support for modules, perl and lua
file /usr/share/nginx/** r,
file /usr/share/perl/*/**.pm r,
file /usr/share/lua/*/**.lua r,
# Temporary files
owner file /tmp/** rw,
# nginx libs
owner file /var/lib/nginx/** rw,
# logs
file /var/log/nginx/* w,
# Binaries
file @{exec_path} mr,
owner file /run/nginx.pid rw,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/nginx>
}

View File

@ -18,9 +18,7 @@ profile plasmashell /usr/bin/plasmashell {
ptrace,
# allow executing QtWebEngineProcess with full permissions including userns (using profile stacking to avoid no_new_privs issues)
/usr/lib/x86_64-linux-gnu/qt[56]/libexec/QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
/usr/libexec/qt[56]/QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
/usr/lib/qt6/libexec/QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
priority=1 /usr/lib{,64,exec}/{,@{multiarch}/}qt{,5,6}/{,libexec/}QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
# allow to execute all other programs under their own profile, or to run unconfined
/** pux,

View File

@ -35,6 +35,7 @@ profile wg-quick /usr/bin/wg-quick flags=(attach_disconnected) {
file mrix /usr/sbin/xtables-nft-multi,
file mrix /usr/bin/resolvectl,
file mrix /usr/sbin/resolvconf,
file PUx /usr/bin/systemd-creds,
# dbus access
file rw @{run}/dbus/system_bus_socket,

View File

@ -144,6 +144,7 @@ SRC=access.c \
getcon_verify.c \
link.c \
link_subset.c \
linkat_tmpfile.c \
mmap.c \
mkdir.c \
mount.c \
@ -311,6 +312,7 @@ TESTS=aa_exec \
i18n \
link \
link_subset \
linkat_tmpfile \
mkdir \
mmap \
mount \

View File

@ -85,6 +85,7 @@ int test_with_old_style_mount() {
perror("FAIL: could not open executable file in shadowed dir");
close(shadowed_file_fd);
close(shadowing_dirfd);
close(shadowed_dirfd);
return 1;
}
@ -137,6 +138,11 @@ int test_with_old_style_mount() {
DEBUG_PRINTF("Read from disconnected file\n");
char *file_contents_buf = calloc(shadowed_file_size+1, sizeof(char));
if (file_contents_buf == NULL) {
perror("FAIL: could not allocate memory to read file");
rc |= 1;
goto cleanup_mount;
}
if (read(shadowed_file_fd, file_contents_buf, shadowed_file_size) == -1) {
perror("FAIL: could not read from file after mount");
rc |= 1;
@ -164,6 +170,7 @@ int test_with_old_style_mount() {
rc |= 1;
}
cleanup_fds:
close(shadowed_exec_fd);
close(shadowed_file_fd);
close(shadowing_dirfd);
close(shadowed_exec_fd);

View File

@ -0,0 +1,29 @@
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv) {
if (argc != 2 && argc != 3) {
fprintf(stderr, "FAIL: Usage: linkat_tmpfile tmpdir [final_location]\n");
return 1;
}
int tmpfile_fd = open(argv[1], O_TMPFILE | O_WRONLY, S_IRUSR | S_IWUSR);
if (tmpfile_fd == -1) {
perror("FAIL: could not open tmpfile");
return 1;
}
if (argc == 3) {
int linkat_result = linkat(tmpfile_fd, "", AT_FDCWD, argv[2], AT_EMPTY_PATH);
if (linkat_result == -1) {
perror("FAIL: could not link tmpfile into final location");
close(tmpfile_fd);
return 1;
}
}
close(tmpfile_fd);
fprintf(stderr, "PASS\n");
return 0;
}

View File

@ -0,0 +1,52 @@
#! /bin/bash
# Copyright (C) 2025 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 linkat
#=DESCRIPTION
# Verifies that file creation with O_TMPFILE and linkat(2) is mediated correctly
#=END
pwd=`dirname $0`
pwd=`cd $pwd ; /bin/pwd`
bin=$pwd
. "$bin/prologue.inc"
tmpdir_nested=$tmpdir/nested
tmpdir_nested_file=$tmpdir_nested/file
tmpfile=$tmpdir/file
mkdir $tmpdir_nested
genprofile cap:dac_read_search
runchecktest "linkat O_TMPFILE noperms" fail $tmpdir_nested
runchecktest "linkat O_TMPFILE noperms, link" fail $tmpdir_nested $tmpfile
# Denial log entry for tmpfile is /path/#[6digits]
# Don't assume because O_TMPFILE fds should lack a name entirely
genprofile cap:dac_read_search "${tmpdir_nested}/:w" "${tmpdir_nested}/*:w"
runchecktest "linkat O_TMPFILE tmpdir only" pass $tmpdir_nested
runchecktest "linkat O_TMPFILE tmpdir only, link" fail $tmpdir_nested $tmpfile
genprofile cap:dac_read_search "${tmpfile}:w"
runchecktest "linkat O_TMPFILE tmpfile only" fail $tmpdir_nested
runchecktest "linkat O_TMPFILE tmpfile only, link" fail $tmpdir_nested $tmpfile
genprofile cap:dac_read_search "${tmpdir_nested}/:w" "${tmpdir_nested}/*:w" "${tmpfile}:w"
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (w)" pass $tmpdir_nested
# Even if semantically a (w)rite it gets logged as the (l)ink that it actually is
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (w), link" xpass $tmpdir_nested $tmpfile
genprofile cap:dac_read_search "${tmpdir_nested}/:w" "${tmpdir_nested}/*:w" "${tmpfile}:l"
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (l)" pass $tmpdir_nested
# Even if semantically a (w)rite we want to test backwards compatibility with (l)ink as it is currently seen
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (l), link" pass $tmpdir_nested $tmpfile
rm $tmpfile
rmdir $tmpdir_nested

View File

@ -36,6 +36,7 @@ environment:
TEST/io_uring: 1
TEST/link: 1
TEST/link_subset: 1
TEST/linkat_tmpfile: 1
TEST/longpath: 1
TEST/mkdir: 1
TEST/mmap: 1

View File

@ -53,7 +53,7 @@ import apparmor.update_profile as update_profile
import LibAppArmor # C-library to parse one log line
from apparmor.common import DebugLogger, open_file_read
from apparmor.fail import enable_aa_exception_handler
from apparmor.notify import get_last_login_timestamp
from apparmor.notify import get_last_login_timestamp, get_event_special_type, set_userns_special_profile
from apparmor.translations import init_translation
from apparmor.logparser import ReadLog
from apparmor.gui import UsernsGUI, ErrorGUI, ShowMoreGUI, ShowMoreGUIAggregated, set_interface_theme, ProfileRules
@ -66,6 +66,9 @@ import threading
gi.require_version('GLib', '2.0')
# setup module translations
_ = init_translation()
def get_user_login():
"""Portable function to get username.
@ -449,22 +452,15 @@ def compile_filter_regex(filters):
def can_allow_rule(ev, special_profiles):
if customized_message['userns']['cond'](ev, special_profiles):
ev_type = get_event_special_type(ev, special_profiles)
if ev_type != 'normal':
if ev['execpath'] is None:
return False
return not aa.get_profile_filename_from_profile_name(ev['comm'])
else:
return aa.get_profile_filename_from_profile_name(ev['profile']) is not None
def is_special_profile_userns(ev, special_profiles):
if not special_profiles or ev['profile'] not in special_profiles:
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
if 'execpath' not in ev or not ev['execpath']:
ev['execpath'] = aa.find_executable(ev['comm'])
return True
def create_userns_profile(name, path, ans):
update_profile_path = update_profile.__file__
@ -490,6 +486,8 @@ def create_userns_profile(name, path, ans):
except subprocess.CalledProcessError as e:
if e.returncode != 126: # return code 126 means the user cancelled the request
UsernsGUI.show_error_cannot_reload_profile(profile_path, e.returncode)
else:
aa.update_profiles()
def ask_for_user_ns_denied(path, name, interactive=True):
@ -508,8 +506,6 @@ def can_leverage_userns_event(ev):
if ev['execpath'] is None:
return 'error_cannot_find_path'
aa.update_profiles()
if aa.get_profile_filename_from_profile_name(ev['comm']):
return 'error_userns_profile_exists'
return 'ok'
@ -544,25 +540,35 @@ def get_more_info_about_event(rl, ev, special_profiles, profile_path, header='')
if value:
out += '\t{} = {}\n'.format(_(key), value)
out += _('\nThe software that declined this operation is {}\n').format(ev['profile'])
if ev['aamode'] == 'REJECTING':
out += _('\nThe profile that denied this operation is {}\n').format(ev['profile'])
else:
out += _('\nThe profile that triggered this alert is {}\n').format(ev['profile'])
rule = rl.create_rule_from_ev(ev)
if rule:
if type(rule) is FileRule and rule.exec_perms == FileRule.ANY_EXEC:
rule.exec_perms = 'Pix'
aa.update_profiles()
if customized_message['userns']['cond'](ev, special_profiles):
out += _('You may allow it through a dedicated unconfined profile for {}.').format(ev['comm'])
if get_event_special_type(ev, special_profiles) != 'normal':
userns_event_usable = can_leverage_userns_event(ev)
if userns_event_usable == 'error_cannot_find_path':
raw_rule = _('# You may allow it through a dedicated unconfined profile for {0}. However, apparmor cannot find {0}. If you want to allow it, please create a profile for it manually.').format(ev['comm'])
raw_rule = _('# You may allow it through a dedicated unconfined profile for {0}. If you want to allow it, please create a profile for it manually.').format(ev['comm'])
elif userns_event_usable == 'error_userns_profile_exists':
raw_rule = _('# You may allow it through a dedicated unconfined profile for {} ({}). However, a profile already exists with this name. If you want to allow it, please create a profile for it manually.').format(ev['comm'], ev['execpath'])
elif userns_event_usable == 'ok':
raw_rule = _('# You may allow it through a dedicated unconfined profile for {} ({})').format(ev['comm'], ev['execpath'])
out += raw_rule[1:]
else:
raw_rule = rule.get_clean()
# TODO: This is brittle. Priority>1 might be needed. Also do we need to make the message show that we force allow?
if ev['profile'] in aa.active_profiles.profiles and aa.is_known_rule(aa.active_profiles.profiles[ev['profile']], rule.rule_name, rule):
rule.priority = 1
raw_rule = "priority=1 " + raw_rule
if aa.is_known_rule(aa.active_profiles.profiles[ev['profile']], rule.rule_name, rule):
# TODO: Handle this edge case more gracefully
raw_rule = _('# aa-notify tried to add rule {}. However aa-notify is not allowed to override priority>0 rules. Please fix your profile manually.\n').format(raw_rule)
if profile_path:
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
out += raw_rule
@ -570,7 +576,6 @@ def get_more_info_about_event(rl, ev, special_profiles, profile_path, header='')
out += _('However {profile} is not in {profile_dir}\nIt is likely that the profile was not stored in {profile_dir} or was removed.\n').format(profile=ev['profile'], profile_dir=aa.profile_dir)
else: # Should not happen
out += _('ERROR: Could not create rule from event.')
return out, raw_rule
@ -589,7 +594,6 @@ def cb_more_info(notification, action, _args):
if ans == 'add_rule':
add_to_profile(raw_rule, ev['profile'])
elif ans in {'allow', 'deny'}:
customized_message['userns']['cond'](ev, special_profiles)
create_userns_profile(ev['comm'], ev['execpath'], ans)
@ -608,12 +612,15 @@ def add_to_profile(rule, profile_name):
return
update_profile_path = update_profile.__file__
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', rule, profile_name]
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', args.local, rule, profile_name]
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
if e.returncode != 126: # return code 126 means the user cancelled the request
ErrorGUI(_('Failed to add rule {rule} to {profile}\nError code = {retcode}').format(rule=rule, profile=profile_name, retcode=e.returncode), False).show()
else:
aa.update_profiles()
def create_from_file(file_path):
@ -624,6 +631,8 @@ def create_from_file(file_path):
except subprocess.CalledProcessError as e:
if e.returncode != 126: # return code 126 means the user cancelled the request
ErrorGUI(_('Failed to add some rules'), False).show()
else:
aa.update_profiles()
def allow_rules(clean_rules, allow_all=False):
@ -639,7 +648,7 @@ def allow_rules(clean_rules, allow_all=False):
with open(tmp.name, mode='w') as f:
for profile_name, profile_rules in clean_rules.items():
written += f.write(profile_rules.get_writable_rules(template_path))
written += f.write(profile_rules.get_writable_rules(template_path, args.local))
if written > 0:
create_from_file(tmp.name)
@ -669,26 +678,29 @@ def cb_add_to_profile(notification, action, _args):
ErrorGUI(_('ERROR: Could not create rule from event.'), False).show()
return
aa.update_profiles()
if customized_message['userns']['cond'](ev, special_profiles):
if get_event_special_type(ev, special_profiles) != 'normal':
ask_for_user_ns_denied(ev['execpath'], ev['comm'], False)
else:
add_to_profile(rule.get_clean(), ev['profile'])
customized_message = {
'userns': {
'cond': lambda ev, special_profiles: (ev['operation'] == 'userns_create' or ev['operation'] == 'capable') and is_special_profile_userns(ev, special_profiles),
'msg': 'Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?'
'userns_change_profile': {
'msg': _('Application {0} is transited to special profile. Capabilities could be denied')
},
'userns_denied': {
'msg': _('Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?')
},
'userns_capable': {
'msg': _('Application {0} in special profile wanted to add a capability: ok?')
}
}
def customize_notification_message(ev, msg, special_profiles):
if customized_message['userns']['cond'](ev, special_profiles):
msg = _(customized_message['userns']['msg']).format(ev['comm'])
msg_type = get_event_special_type(ev, special_profiles)
if msg_type in customized_message:
msg = customized_message[msg_type]['msg'].format(ev['comm'])
return msg
@ -712,7 +724,6 @@ def aggregate_event(agg, ev, keys_to_aggregate):
def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles):
notification = ''
summary = ''
more_info = ''
clean_rules = dict()
summary = _('Notifications were raised for profiles: {}\n').format(', '.join(list(agg.keys())))
@ -740,10 +751,11 @@ def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles
ev = data['events'][0]
profile_name = ev['profile']
profile_path = aa.get_profile_filename_from_profile_name(profile_name)
is_userns_profile = customized_message['userns']['cond'](ev, special_profiles)
is_userns_profile = get_event_special_type(ev, special_profiles) != 'normal'
if is_userns_profile:
bin_name = ev['comm']
if 'execpath' not in ev:
ev['execpath'] = None
bin_path = ev['execpath']
actionable = can_leverage_userns_event(ev) == 'ok'
else:
@ -761,7 +773,7 @@ def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles
rules_for_profiles.add(raw_rule)
if rules_for_profiles != set():
if profile not in special_profiles:
if not is_userns_profile:
if profile_path is not None:
clean_rules_name = _('profile {}:').format(profile)
elif re_snap.match(profile):
@ -819,9 +831,6 @@ def main():
# setup exception handling
enable_aa_exception_handler()
# setup module translations
_ = init_translation()
# Register the on_exit method with atexit
# Takes care of closing the debug log etc
atexit.register(aa.on_exit)
@ -833,6 +842,7 @@ def main():
parser = argparse.ArgumentParser(description=_('Display AppArmor notifications or messages for DENIED entries.'))
parser.add_argument('-p', '--poll', action='store_true', help=_('poll AppArmor logs and display notifications'))
parser.add_argument('--display', type=str, help=_('set the DISPLAY environment variable (might be needed if sudo resets $DISPLAY)'))
parser.add_argument('--xauthority', type=str, help=_('set the XAUTHORITY environment variable (might be needed if sudo resets XAUTHORITY)'))
parser.add_argument('-f', '--file', type=str, help=_('search FILE for AppArmor messages'))
parser.add_argument('-l', '--since-last', action='store_true', help=_('display stats since last login'))
parser.add_argument('-s', '--since-days', type=int, metavar=('NUM'), help=_('show stats for last NUM days (can be used alone or with -p)'))
@ -841,6 +851,7 @@ def main():
parser.add_argument('-w', '--wait', type=int, metavar=('NUM'), help=_('wait NUM seconds before displaying notifications (with -p)'))
parser.add_argument('-m', '--merge-notifications', action='store_true', help=_('Merge notification for improved readability (with -p)'))
parser.add_argument('-F', '--foreground', action='store_true', help=_('Do not fork to the background'))
parser.add_argument('-L', '--local', nargs='?', const='yes', choices=['yes', 'no', 'auto'], help=_('Add to local profile'))
parser.add_argument('--prompt-filter', type=str, metavar=('PF'), help=_('kind of operations which display a popup prompt'))
parser.add_argument('--debug', action='store_true', help=_('debug mode'))
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
@ -930,6 +941,7 @@ def main():
- prompt_filter
- maximum_number_notification_profiles
- keys_to_aggregate
- use_local_profiles
- filter.profile,
- filter.operation,
- filter.name,
@ -952,6 +964,7 @@ def main():
'message_footer',
'maximum_number_notification_profiles',
'keys_to_aggregate',
'use_local_profiles',
'filter.profile',
'filter.operation',
'filter.name',
@ -1027,7 +1040,9 @@ def main():
userns_special_profiles = config['']['userns_special_profiles'].strip().split(',')
else:
# By default, unconfined and unprivileged_userns are the special profiles
userns_special_profiles = ['unconfined', 'unprivileged_userns']
userns_special_profiles = ['unconfined', 'unprivileged_userns', 'unpriv_.*']
# To support regexes
userns_special_profiles = set_userns_special_profile(userns_special_profiles)
if 'ignore_denied_capability' in config['']:
ignore_denied_capability = config['']['ignore_denied_capability'].strip().split(',')
@ -1059,6 +1074,19 @@ def main():
else:
keys_to_aggregate = {'operation', 'class', 'name', 'denied', 'target'}
if not args.local:
if 'use_local_profiles' in config['']:
if config['']['use_local_profiles'] in {'auto', 'yes', 'no'}:
args.local = config['']['use_local_profiles']
elif config['']['use_local_profiles'] is None:
args.local = 'yes'
else:
sys.exit(_('ERROR: using an invalid value for use_local_profiles in config {}\nSupported values: {}').format(
config['']['use_local_profiles'], ', '.join({'yes', 'auto', 'no'})
))
else:
args.local = 'auto'
if args.file:
logfile = args.file
elif os.path.isfile('/var/run/auditd.pid') and os.path.isfile('/var/log/audit/audit.log'):
@ -1076,6 +1104,8 @@ def main():
if args.display:
os.environ['DISPLAY'] = args.display
if args.xauthority:
os.environ['XAUTHORITY'] = args.xauthority
if args.poll:
# Exit immediately if show_notifications is no or any of the options below
@ -1156,10 +1186,14 @@ def main():
if ev['operation'] == 'capable' and ev['comm'] in ignore_denied_capability:
continue
# Special behaivor for userns:
if args.prompt_filter and 'userns' in args.prompt_filter and customized_message['userns']['cond'](ev, userns_special_profiles):
prompt_userns(ev)
continue # Notification already displayed for this event, we go to the next one.
# Special behavior for userns:
if get_event_special_type(ev, userns_special_profiles) != 'normal':
if 'execpath' not in ev:
ev['execpath'] = None
if args.prompt_filter and 'userns' in args.prompt_filter:
prompt_userns(ev)
continue # Notification already displayed for this event, we go to the next one.
# Notifications should not be run as root, since root probably is
# the wrong desktop user and not the one getting the notifications.

View File

@ -71,6 +71,14 @@ This has no effect when running under sudo.
wait NUM seconds before displaying notifications (for use with -p)
=item -L, --local [{yes,no,auto}]
add rules to a local profiles instead of the real profiles.
This simplify profiles' deployment by keeping local modifications self-contained.
- B<yes>: always use a local profile
- B<no>: never use a local profile
- B<auto>: use a local profile if the main profile already relies on a local profile
=item -v, --verbose
show messages with summaries.
@ -89,8 +97,8 @@ System-wide configuration for B<aa-notify> is done via
# Set to 'no' to disable AppArmor notifications globally
show_notifications="yes"
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
userns_special_profiles="unconfined,unprivileged_userns"
# Special profiles used to remove privileges for unconfined binaries using user namespaces. Special profiles use Python's regular expression syntax. If unsure, leave as is.
userns_special_profiles="unconfined,unprivileged_userns,unpriv_.*"
# Theme for aa-notify GUI. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
interface_theme="ubuntu"
@ -98,6 +106,9 @@ System-wide configuration for B<aa-notify> is done via
# Binaries for which we ignore userns-related capability denials
ignore_denied_capability="sudo,su"
# Write change to local profiles if enabled to preserve regular profiles and simplify upgrades (yes, no, auto)
use_local_profiles="yes"
# OPTIONAL - kind of operations which display a popup prompt.
prompt_filter="userns"

View File

@ -72,7 +72,7 @@ Filter by profile name
=item --filter.profile_attach PROFILE_ATTACH
Filter by profile attachement (i.e. by path of the executable to which this profile applies)
Filter by profile attachment (i.e. by path of the executable to which this profile applies)
=item --filter.profile_path PROFILE_PATH

View File

@ -1703,6 +1703,71 @@ def read_profile(file, is_active_profile, read_error_fatal=False):
extra_profiles.add_profile(filename, profile, attachment, profile_data[profile])
# TODO: Split profiles' creating and saving.
def create_local_profile_if_needed(profile_name):
base_profile = profile_name
while True:
parent = active_profiles[base_profile].data.get('parent')
if parent == '':
break
base_profile = parent
local_include = active_profiles[profile_name].get_local_include()
# Not found: we add a mention of the local profile in the main profile
if not local_include:
local_include = "local/" + profile_name.replace('/', '.')
active_profiles[profile_name]['inc_ie'].add(IncludeRule(local_include, True, True))
write_profile_ui_feedback(base_profile)
inc_file = profile_dir + '/' + local_include
# Create the include if needed
if not include.get(inc_file, {}).get(inc_file, False):
include[inc_file] = dict()
include[inc_file][inc_file] = ProfileStorage(inc_file, inc_file, "create_local_profile_if_needed")
return inc_file
def serialize_include(prof_storage, include_metadata=True):
lines = []
if include_metadata:
lines.append('# Last Modified: %s' % time.asctime())
if prof_storage.get('initial_comment'):
lines.append(prof_storage['initial_comment'].rstrip())
lines.extend(prof_storage.get_rules_clean(0))
return '\n'.join(lines) + '\n'
def write_include_ui_feedback(include_data, incfile, out_dir=None, include_metadata=True):
aaui.UI_Info(_('Writing updated include file %s') % incfile)
write_include(include_data, incfile, out_dir, include_metadata)
def write_include(include_data, incfile, out_dir=None, include_metadata=True):
target_file = incfile if incfile.startswith('/') else os.path.join(profile_dir, incfile)
if out_dir:
target_file = os.path.join(out_dir, os.path.basename(target_file))
include_string = serialize_include(include_data, include_metadata=include_metadata)
with NamedTemporaryFile('w', suffix='~', delete=False, dir=profile_dir + "/local") as tmp:
if os.path.exists(target_file):
shutil.copymode(target_file, tmp.name)
else:
pass # 0o600 (NamedTemporaryFile default)
tmp.write(include_string)
try:
shutil.move(tmp.name, target_file)
except PermissionError:
aaui.UI_Important(_('WARNING: Can\'t write to %s. Please run this script with elevated privileges') % target_file)
def attach_profile_data(profiles, profile_data):
profile_data = merged_to_split(profile_data)
# Make deep copy of data to avoid changes to

View File

@ -1,6 +1,7 @@
import os
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font
import subprocess
import apparmor.aa as aa
@ -99,12 +100,12 @@ class ProfileRules:
for raw_rule in raw_rules:
self.rules.append(SelectableRule(raw_rule, self.selectable))
def get_writable_rules(self, template_path, allow_all=False):
def get_writable_rules(self, template_path, local='yes', allow_all=False):
out = ''
for rule in self.rules:
if allow_all or rule.selected.get():
if not self.is_userns_profile:
out += 'add_rule\t{}\t{}\n'.format(rule.rule, self.profile_name)
out += 'add_rule\t{}\t{}\t{}\n'.format(local, rule.rule, self.profile_name)
else:
out += 'create_userns\t{}\t{}\t{}\t{}\t{}\n'.format(template_path, self.profile_name, self.bin_path, self.profile_path, 'allow')
return out
@ -157,17 +158,18 @@ class ShowMoreGUIAggregated(GUI):
self.text_display = tk.Text(self.label_frame, wrap='word', height=40, width=100, yscrollcommand=self.scrollbar.set)
kwargs = {
"height": self.text_display.winfo_reqheight() - 4, # The border are *inside* the canvas but *outside* the textbox. I need to remove 4px (2*the size of the borders) to get the same size
"width": self.text_display.winfo_reqwidth() - 4,
"borderwidth": self.text_display['borderwidth'],
"relief": self.text_display['relief'],
"yscrollcommand": self.scrollbar.set,
}
if ttkthemes:
self.text_display.configure(background=self.bg_color, foreground=self.fg_color)
self.canvas = tk.Canvas(
self.label_frame,
background=self.bg_color,
height=self.text_display.winfo_reqheight() - 4, # The border are *inside* the canvas but *outside* the textbox. I need to remove 4px (2*the size of the borders) to get the same size
width=self.text_display.winfo_reqwidth() - 4,
borderwidth=self.text_display['borderwidth'],
relief=self.text_display['relief'],
yscrollcommand=self.scrollbar.set
)
kwargs['background'] = self.bg_color
self.canvas = tk.Canvas(self.label_frame, **kwargs)
self.inner_frame = ttk.Frame(self.canvas)
self.canvas.create_window((2, 2), window=self.inner_frame, anchor='nw')
@ -205,7 +207,7 @@ class ShowMoreGUIAggregated(GUI):
def create_profile_rules_frame(self, parent, clean_rules):
for profile_name, profile_rules in clean_rules.items():
label = ttk.Label(parent, text=profile_name, font=tk.font.BOLD)
label = ttk.Label(parent, text=profile_name, font=tkinter.font.BOLD)
label.pack(anchor='w', pady=(5, 0))
label.bind("<Button-1>", lambda event, rules=profile_rules: self.toggle_profile_rules(rules))

View File

@ -113,7 +113,6 @@ class ReadLog:
return log_entry
def get_event_type(self, e):
if e['operation'] == 'exec':
return 'file'
elif e['class'] and e['class'] == 'namespace':
@ -131,6 +130,8 @@ class ReadLog:
return 'pivot_root'
elif e['class'] and e['class'] == 'net' and e['family'] and e['family'] == 'unix':
return 'unix'
elif e['operation'] == 'change_onexec':
return 'change_profile'
elif e['class'] == 'file' or self.op_type(e) == 'file':
return 'file'
elif e['operation'] == 'capable':
@ -160,6 +161,8 @@ class ReadLog:
return None
def create_rule_from_ev(self, ev):
if not ev:
return None
event_type = self.get_event_type(ev)
if not event_type:
return None
@ -244,7 +247,7 @@ class ReadLog:
elif event_type == 'io_uring':
ev['peer_profile'] = event.peer_profile
elif event_type == 'capability':
elif event_type == 'capability' or ev['operation'] == 'change_onexec':
ev['comm'] = event.comm
if not ev['time']:
@ -359,7 +362,7 @@ class ReadLog:
self.hashlog[aamode][full_profile]['change_hat'][e['name2']] = True
return
elif e['operation'] == 'change_profile':
elif e['operation'] == 'change_profile' or e['operation'] == 'change_onexec':
ChangeProfileRule.hashlog_from_event(self.hashlog[aamode][full_profile]['change_profile'], e)
return

View File

@ -16,6 +16,7 @@
import os
import struct
import sqlite3
import re
from apparmor.common import AppArmorBug, DebugLogger
@ -129,3 +130,33 @@ def get_last_login_timestamp_wtmp(username, filename='/var/log/wtmp'):
# When loop is done, last value should be the latest login timestamp
return last_login
def is_special_profile_userns(ev, special_profiles):
if 'comm' not in ev:
return False # special profiles have a 'comm' entry
if not special_profiles or not special_profiles.match(ev['profile']):
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
return True
def get_event_special_type(ev, special_profiles):
if is_special_profile_userns(ev, special_profiles):
if ev['operation'] == 'userns_create':
if ev['aamode'] == 'REJECTING':
return 'userns_denied'
else:
return 'userns_change_profile'
elif ev['operation'] == 'change_onexec':
return 'userns_change_profile'
elif ev['operation'] == 'capable':
return 'userns_capable'
else:
raise AppArmorBug('unexpected operation: %s' % ev['operation'])
return 'normal'
def set_userns_special_profile(special_profiles):
return re.compile('^({})$'.format('|'.join(special_profiles)))

View File

@ -199,6 +199,21 @@ class ProfileStorage:
return data
def get_local_include(self):
inc = None
preferred_inc = self.data['name']
if preferred_inc.startswith('/'):
preferred_inc = preferred_inc[1:]
preferred_inc = 'local/' + preferred_inc.replace('/', '.')
# If a local profile already exists, we use it.
for rule in self.data['inc_ie'].rules:
if rule.path.startswith("local/"):
inc = rule.path
if rule.path == preferred_inc: # Prefer includes that matches the profile name.
break
return inc
@classmethod
def parse(cls, line, file, lineno, profile, hat):
"""parse a profile start line (using parse_profile_startline()) and convert it to an instance of this class"""

View File

@ -8,8 +8,19 @@ from apparmor import aa
from apparmor.logparser import ReadLog
from apparmor.translations import init_translation
_ = init_translation()
is_aa_inited = False
def init_if_needed():
global is_aa_inited
if not is_aa_inited:
aa.init_aa()
aa.read_profiles()
is_aa_inited = True
def create_userns(template_path, name, bin_path, profile_path, decision):
with open(template_path, 'r') as f:
@ -27,27 +38,48 @@ def create_userns(template_path, name, bin_path, profile_path, decision):
exit(_('Cannot reload updated profile'))
def add_to_profile(rule, profile_name):
aa.init_aa()
aa.update_profiles()
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
rule_obj = rule_class.create_instance(rule)
if not aa.active_profiles.profile_exists(profile_name):
exit(_('Cannot find {} in profiles').format(profile_name))
aa.active_profiles[profile_name][rule_type].add(rule_obj, cleanup=True)
def add_to_profile(rule_obj, profile_name):
aa.active_profiles[profile_name][rule_obj.rule_name].add(rule_obj, cleanup=True)
# Save changes
aa.write_profile_ui_feedback(profile_name)
def add_to_local_profile(rule_obj, profile_name):
inc_file = aa.create_local_profile_if_needed(profile_name)
aa.include[inc_file][inc_file].data[rule_obj.rule_name].add(rule_obj, cleanup=True)
aa.write_include_ui_feedback(aa.include[inc_file][inc_file], inc_file)
def add_rule(mode, rule, profile_name):
init_if_needed()
if not aa.active_profiles.profile_exists(profile_name):
exit(_('Cannot find {} in profiles').format(profile_name))
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
rule_obj = rule_class.create_instance(rule)
if mode == 'yes':
add_to_local_profile(rule_obj, profile_name)
elif mode == 'no':
add_to_profile(rule_obj, profile_name)
elif mode == 'auto':
if aa.active_profiles[profile_name].get_local_include():
add_to_local_profile(rule_obj, profile_name)
else:
add_to_profile(rule_obj, profile_name)
else:
usage(False)
aa.reload_base(profile_name)
def usage(is_help):
print('This tool is a low level tool - do not use it directly')
print('{} create_userns <template_path> <name> <bin_path> <profile_path> <decision>'.format(sys.argv[0]))
print('{} add_rule <rule> <profile_name>'.format(sys.argv[0]))
print('{} add_rule <mode=yes|no|auto> <rule> <profile_name>'.format(sys.argv[0]))
print('{} from_file <file>'.format(sys.argv[0]))
if is_help:
exit(0)
@ -76,9 +108,9 @@ def do_command(command, args):
usage(False)
create_userns(args[1], args[2], args[3], args[4], args[5])
elif command == 'add_rule':
if not len(args) == 3:
if not len(args) == 4:
usage(False)
add_to_profile(args[1], args[2])
add_rule(args[1], args[2], args[3])
elif command == 'help':
usage(True)
else:

View File

@ -11,8 +11,8 @@
# Set to 'no' to disable AppArmor notifications globally
show_notifications="yes"
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
userns_special_profiles="unconfined,unprivileged_userns"
# Special profiles used to remove privileges for unconfined binaries using user namespaces. Special profiles use Python's regular expression syntax. If unsure, leave as is.
userns_special_profiles="unconfined,unprivileged_userns,unpriv_.*"
# Theme to use for aa-notify GUI themes. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
interface_theme="ubuntu"
@ -20,6 +20,9 @@ interface_theme="ubuntu"
# Binaries for which we ignore userns-related capability denials
ignore_denied_capability="sudo,su"
# OPTIONAL - Write changes to local profiles to preserve regular profiles and simplify upgrades (yes, no, auto)
# use_local_profiles="yes"
# OPTIONAL - kind of operations which display a popup prompt.
# prompt_filter="userns"

View File

@ -167,8 +167,9 @@ class AANotifyTest(AANotifyBase):
expected_return_code = 0
expected_output_1 = \
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [-f FILE] [-l] [-s NUM] [-v]
[-u USER] [-w NUM] [-m] [-F] [--prompt-filter PF] [--debug]
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [--xauthority XAUTHORITY]
[-f FILE] [-l] [-s NUM] [-v] [-u USER] [-w NUM] [-m] [-F]
[-L [{yes,no,auto}]] [--prompt-filter PF] [--debug]
[--filter.profile PROFILE] [--filter.operation OPERATION]
[--filter.name NAME] [--filter.denied DENIED]
[--filter.family FAMILY] [--filter.socket SOCKET]
@ -182,6 +183,9 @@ Display AppArmor notifications or messages for DENIED entries.
-p, --poll poll AppArmor logs and display notifications
--display DISPLAY set the DISPLAY environment variable (might be needed if
sudo resets $DISPLAY)
--xauthority XAUTHORITY
set the XAUTHORITY environment variable (might be needed
if sudo resets XAUTHORITY)
-f, --file FILE search FILE for AppArmor messages
-l, --since-last display stats since last login
-s, --since-days NUM show stats for last NUM days (can be used alone or with
@ -193,6 +197,8 @@ Display AppArmor notifications or messages for DENIED entries.
-m, --merge-notifications
Merge notification for improved readability (with -p)
-F, --foreground Do not fork to the background
-L, --local [{yes,no,auto}]
Add to local profile
--prompt-filter PF kind of operations which display a popup prompt
--debug debug mode
@ -231,6 +237,11 @@ Filtering options:
), (
', --wait NUM ',
' NUM, --wait NUM',
), (
' -L, --local [{yes,no,auto}]\n'
+ ' Add to local profile',
' -L [{yes,no,auto}], --local [{yes,no,auto}]\n'
+ ' Add to local profile'
)]
for patch in patches:
expected_output_2 = expected_output_2.replace(patch[0], patch[1])

View File

@ -166,7 +166,6 @@ log_to_profile_skip = [
# tests that cause an empty log
log_to_profile_known_empty_log = [
'change_onexec_lp1648143', # change_onexec not supported in logparser.py yet (and the log is about "no new privs" error)
'ptrace_garbage_lp1689667_1', # no denied= in log
'ptrace_no_denied_mask', # no denied= in log
'unconfined-change_hat', # unconfined trying to change_hat, which isn't allowed

View File

@ -12,7 +12,8 @@
import unittest
from apparmor.common import AppArmorBug
from apparmor.notify import get_last_login_timestamp, get_last_login_timestamp_wtmp, sane_timestamp
from apparmor.notify import get_last_login_timestamp, get_last_login_timestamp_wtmp, sane_timestamp, get_event_special_type, set_userns_special_profile
from apparmor.logparser import ReadLog
from common_test import AATest, setup_all_loops
@ -87,6 +88,36 @@ class TestGet_last_login_timestamp_wtmp(AATest):
get_last_login_timestamp_wtmp('root', 'wtmp-examples/wtmp-x86_64-past')
class TestEventSpecialType(AATest):
userns_special_profiles = set_userns_special_profile(['unconfined', 'unprivileged_userns', 'unpriv_.*'])
parser = ReadLog('', '', '')
tests = (
('[ 176.385388] audit: type=1400 audit(1666891380.570:78): apparmor="DENIED" operation="userns_create" class="namespace" profile="/usr/bin/bwrap-userns-restrict" pid=1785 comm="userns_child_ex" requested="userns_create" denied="userns_create"', 'normal'),
('[ 839.488169] audit: type=1400 audit(1752065668.819:208): apparmor="DENIED" operation="userns_create" class="namespace" info="Userns create restricted - failed to find unprivileged_userns profile" error=-13 profile="unconfined" pid=12124 comm="unshare" requested="userns_create" denied="userns_create" target="unprivileged_userns"', 'userns_denied'),
('[ 429.272003] audit: type=1400 audit(1720613712.153:168): apparmor="AUDIT" operation="userns_create" class="namespace" info="Userns create - transitioning profile" profile="unconfined" pid=5630 comm="unshare" requested="userns_create" target="unprivileged_userns" execpath="/usr/bin/unshare"', 'userns_change_profile'),
('[ 52.901383] audit: type=1400 audit(1752064882.228:82): apparmor="DENIED" operation="capable" class="cap" profile="unprivileged_userns" pid=6700 comm="electron" capability=21 capname="sys_admin"', 'userns_capable'),
('Jul 31 17:11:16 dbusdev-saucy-amd64 dbus[1692]: apparmor="DENIED" operation="dbus_bind" bus="session" name="com.apparmor.Test" mask="bind" pid=2940 profile="/tmp/apparmor-2.8.0/tests/regression/apparmor/dbus_service"', 'normal'),
('[103975.623545] audit: type=1400 audit(1481284511.494:2807): apparmor="DENIED" operation="change_onexec" info="no new privs" error=-1 namespace="root//lxd-tor_<var-lib-lxd>" profile="unconfined" name="system_tor" pid=18593 comm="(tor)" target="system_tor"', 'userns_change_profile'),
('[78661.551820] audit: type=1400 audit(1752661047.170:350): apparmor="DENIED" operation="capable" class="cap" profile="unpriv_bwrap" pid=1412550 comm="node" capability=21 capname="sys_admin"', 'userns_capable'),
)
def _run_test(self, ev, expected):
parsed_event = self.parser.parse_event(ev)
r = self.parser.create_rule_from_ev(parsed_event)
self.assertIsNotNone(r)
real_type = get_event_special_type(parsed_event, self.userns_special_profiles)
self.assertEqual(expected, real_type,
"ev {}: {} != {}".format(ev, expected, real_type))
def test_invalid(self):
ev = 'type=AVC msg=audit(1333698107.128:273917): apparmor="DENIED" operation="recvmsg" parent=1596 profile="unprivileged_userns" pid=1875 comm="nc" laddr=::ffff:127.0.0.1 lport=2048 faddr=::ffff:127.0.0.1 fport=59180 family="inet6" sock_type="stream" protocol=6'
parsed_event = self.parser.parse_event(ev)
parsed_event['comm'] = 'something' # Artificially crafted invalid event
with self.assertRaises(AppArmorBug):
get_event_special_type(parsed_event, self.userns_special_profiles)
setup_all_loops(__name__)
if __name__ == '__main__':
unittest.main(verbosity=1)

View File

@ -14,6 +14,7 @@ import unittest
from apparmor.common import AppArmorBug, AppArmorException
from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, split_flags, var_transform
from apparmor.rule.capability import CapabilityRule
from apparmor.rule.include import IncludeRule
from common_test import AATest, setup_all_loops
@ -313,6 +314,27 @@ class AaTest_var_transform(AATest):
self.assertEqual(var_transform(params), expected)
class AaTest_include(AATest):
tests = (
(('profile foo /foo {', []), None), # No include
(('profile foo /foo {', ['elsewhere/foo']), None), # No include in local/
(('profile foo /foo {', ['local/foo']), "local/foo"), # Single include, we pick it
(('profile foo /foo {', ['local/bar']), "local/bar"), # Single include, we pick it
(('profile x//y /y {', ['local/x..y', 'local/y']), "local/x..y"), # Pick the include that matches the profile nam
(('profile foo /foo {', ['local/bar', 'local/foo', 'local/baz']), "local/foo"), # Pick the include that matches the profile name
(('/usr/bin/xx {', ['local/usr.bin.xx', 'local/xx']), "local/usr.bin.xx"), # Pick the include that matches the profile name
(('profile foo /foo {', ['local/bar', 'local/baz', 'local/qux']), "local/qux"), # No match, pick the last one
)
def _run_test(self, params, expected):
(profile, hat, prof_storage) = ProfileStorage.parse(params[0], 'somefile', 1, None, None)
for inc in params[1]:
prof_storage.data['inc_ie'].add(IncludeRule(inc, True, True))
self.assertEqual(prof_storage.get_local_include(), expected)
setup_all_loops(__name__)
if __name__ == '__main__':
unittest.main(verbosity=1)