apparmor 'l' denial when using linkat() with attach_disconnected

Bug #1772097 reported by Jamie Strandboge
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
AppArmor
Confirmed
Medium
Unassigned

Bug Description

Qt 5.10 recently (https://github.com/qt/qtbase/commit/189e9c93d7ed42202ad51507c8944d64e9a7888d) started using something akin to the following for creating temp files:

fd = open("/tmp", O_RDWR | O_DIRECTORY | O_CLOEXEC | O_TMPFILE)
write(fd, "...")
linkat(AT_FDCWD, "/proc/self/fd/<fd for O_TMPFILE>", AT_FDCWD, "/tmp/...", AT_SYMLINK_FOLLOW)

A diagnosis was provided here: https://forum.snapcraft.io/t/qt-5-10-linkat-denials-broken-kde-snaps/5484/10

Attached is a simple reproducer that demonstrates that apparmor works fine with this for unconfined and non-attach_disconnected profiles, but using attach_disconnected breaks the above with denials that cannot be allowed with policy. Eg:

$ tar -zxvf ./linkat.tar.gz
linkat/
linkat/main.c
linkat/test.sh
linkat/profile
linkat/profile_attach_disconnected
linkat/test-no-snap.sh
linkat/test-tmp-linkat_1.0_amd64.snap

$ cd ./linkat
$ ./test-no-snap.sh
= Test unconfined =
Running: aa-exec -p unconfined -- ./tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

Running: aa-exec -p unconfined -- ./tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

= Test confined =
Loading apparmor profile for 'test'

Policy for 'test'
#include <tunables/global>

profile test {
  #include <abstractions/base>

  /tmp/{#,okular}* rwl,
  @{HOME}/snap/test-tmp-linkat/common/{#,okular}* rwl,
}

Running: aa-exec -p test -- ./tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

Running: aa-exec -p test -- ./tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

= Test confined with attach_disconnected =
Loading apparmor profile for 'test_atch_disconnected'

Policy for 'test_attach_disconnected'
#include <tunables/global>

profile test_attach_disconnected (attach_disconnected) {
  #include <abstractions/base>

  /tmp/{#,okular}* rwl,
  @{HOME}/snap/test-tmp-linkat/common/{#,okular}* rwl,
}

Running: aa-exec -p test_attach_disconnected -- ./tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

Running: aa-exec -p test_attach_disconnected -- ./tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

FAIL: some tests failed
[1]

The failing test shows this in the logs:
May 18 13:53:29 localhost audit[20542]: AVC apparmor="DENIED" operation="link" info="Failed name lookup - deleted entry" error=-2 profile="test_attach_disconnected" name="/tmp/#132105" pid=20542 comm="tmp-linkat" requested_mask="l" denied_mask="l" fsuid=1000 ouid=1000
May 18 13:53:29 localhost audit[20542]: AVC apparmor="DENIED" operation="link" profile="test_attach_disconnected" name="/tmp/okular_DHqFKd.ps" pid=20542 comm="tmp-linkat" requested_mask="l" denied_mask="l" fsuid=1000 ouid=1000 target="/tmp/#132105"
May 18 13:53:29 localhost audit[20543]: AVC apparmor="DENIED" operation="link" info="Failed name lookup - deleted entry" error=-2 profile="test_attach_disconnected" name="/home/jamie/snap/test-tmp-linkat/common/#12871268" pid=20543 comm="tmp-linkat" requested_mask="l" denied_mask="l" fsuid=1000 ouid=1000
May 18 13:53:29 localhost audit[20543]: AVC apparmor="DENIED" operation="link" profile="test_attach_disconnected" name="/home/jamie/snap/test-tmp-linkat/common/okular_DHqFKd.ps" pid=20543 comm="tmp-linkat" requested_mask="l" denied_mask="l" fsuid=1000 ouid=1000 target="/home/jamie/snap/test-tmp-linkat/common/#12871268"

Unfortunately, this breaks snaps using Qt 5.10 as there is no workaround (attach_disconnected is required due to how the snap's mountspace is setup). Eg:

$ ./test.sh
= Test strict mode snap (per-snap mount namespace with strict) =
test-tmp-linkat 1.0 installed

Grepping for relevant policy:
@{SNAP_NAME}="test-tmp-linkat"
profile "snap.test-tmp-linkat.test-tmp-linkat" (attach_disconnected) {
  owner @{HOME}/snap/@{SNAP_NAME}/common/** wl,
  /var/snap/@{SNAP_NAME}/common/** wl,
  /tmp/** mrwlkix,

Running: test-tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

Running: test-tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

= Test devmode mode snap (per-snap mount namespace with complain) =
test-tmp-linkat 1.0 installed

Grepping for relevant policy:
@{SNAP_NAME}="test-tmp-linkat"
profile "snap.test-tmp-linkat.test-tmp-linkat" (attach_disconnected,complain) {
  owner @{HOME}/snap/@{SNAP_NAME}/common/** wl,
  /var/snap/@{SNAP_NAME}/common/** wl,
  /tmp/** mrwlkix,

Running: test-tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

Running: test-tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

= Test class mode snap (global mount namespace loose with complain) =
test-tmp-linkat 1.0 installed

Grepping for relevant policy:
@{SNAP_NAME}="test-tmp-linkat"
profile "snap.test-tmp-linkat.test-tmp-linkat" (attach_disconnected,complain) {
  /** rwlkm,

Running: test-tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

Running: test-tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

= Test unconfined =
Running: aa-exec -p unconfined -- /snap/test-tmp-linkat/current/bin/tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

Running: aa-exec -p unconfined -- /snap/test-tmp-linkat/current/bin/tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

= Test confined outside of snap =
Loading apparmor profile for 'test'

Policy for 'test'
#include <tunables/global>

profile test {
  #include <abstractions/base>

  /tmp/{#,okular}* rwl,
  @{HOME}/snap/test-tmp-linkat/common/{#,okular}* rwl,
}

Running: aa-exec -p test -- /snap/test-tmp-linkat/current/bin/tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

Running: aa-exec -p test -- /snap/test-tmp-linkat/current/bin/tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat success

= Test confined outside of snap with attach_disconnected =
Loading apparmor profile for 'test_atch_disconnected'

Policy for 'test_attach_disconnected'
#include <tunables/global>

profile test_attach_disconnected (attach_disconnected) {
  #include <abstractions/base>

  /tmp/{#,okular}* rwl,
  @{HOME}/snap/test-tmp-linkat/common/{#,okular}* rwl,
}

Running: aa-exec -p test_attach_disconnected -- /snap/test-tmp-linkat/current/bin/tmp-linkat /tmp
prefix: /tmp
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

Running: aa-exec -p test_attach_disconnected -- /snap/test-tmp-linkat/current/bin/tmp-linkat /home/jamie/snap/test-tmp-linkat/common/
prefix: /home/jamie/snap/test-tmp-linkat/common/
opened fd: 3 [/proc/self/fd/3]
writing works
linkat failed: No such file or directory
FAIL

FAIL: some tests failed
[1]

The reduced test case (ie, test-no-snap.sh) was confirmed to have this bug on:
* Linux version 4.16.0-1-amd64 (<email address hidden>) (gcc version 7.3.0 (Debian 7.3.0-17)) #1 SMP Debian 4.16.5-1 (2018-04-29)
* Ubuntu 4.15.0-20.21-generic 4.15.17
* Ubuntu 4.13.0-41.46-generic 4.13.16
* Ubuntu 4.4.0-121.145-generic 4.4.117
* Ubuntu 3.13.0-147.196-generic 3.13.11-ckt39

Ubuntu 12.04 kernel does not support O_TMPFILE (it is 3.2 and O_TMPFILE was added in 3.11).

Revision history for this message
Jamie Strandboge (jdstrand) wrote :
description: updated
Revision history for this message
John Johansen (jjohansen) wrote :

So it looks like apparmor is detecting one of the entries as deleted, which it will allow for fds being passed around but not to base lookups off of.

You should be able to work around this by adding the mediate_deleted flag to the profile. Which will allow path lookup from the fd that it is identifying as deleted.

So your example profile would change to

profile test_attach_disconnected (attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  /tmp/{#,okular}* rwl,
  @{HOME}/snap/test-tmp-linkat/common/{#,okular}* rwl,
}

Revision history for this message
Jamie Strandboge (jdstrand) wrote :

I can confirm that adding mediate_deleted to the snaps and test profiles allows the access with no other denials or log entries. Using mediate_deleted means that while the standard open/unlink methodology for creating a tmp file and then passing the fd to the processes that need it continues to work, other processes will be able to poke around in /proc/pid/fd and obtain access to the deleted file (apparmor will require a ptrace rule for this access though, so in the case of snaps, they would only be able to access their own deleted files via /prof/pid/fd and continue not to have access to other snap's deleted files).

Changed in apparmor:
status: New → Confirmed
importance: High → Medium
Revision history for this message
Jamie Strandboge (jdstrand) wrote :

Since there is a known workaround (though it is undecided if snapd will use it), reducing the priority to 'medium' for now.

Revision history for this message
Jamie Strandboge (jdstrand) wrote :

For snapd, as much as I don’t want to use mediate_deleted, I feel we need to considering that the open/unlink/linkat method is documented in ‘man 2 open’ and other snaps (eg, vlc, Qt 5.10, etc) are using it (at least until this bug is fixed and used everywhere). While this makes this open/unlink/linkat technique work, it does mean that a process from the snap will be able to open deleted files with open fds via the /proc interface. This is however no worse than when running unconfined since applications can always use /proc/pid/fd/... to access deleted files. Using apparmor with mediate_deleted is an improvement over unconfined in this area since ptrace mediates access to other task's processes.

To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.