Add checks for the two ways of creating inodes that went past the path
hooks:
- linkable O_TMPFILE refused on the host directory and the host tmpfs
- O_TMPFILE with O_EXCL still allowed in the host directory
- linkable O_TMPFILE and linkat() allowed on the private and the
idmapped tmpfs
- O_TMPFILE refused through the cloned mounts and from the nested user
namespace
- RENAME_WHITEOUT refused in the host directory, allowed on the private
and the idmapped tmpfs
The positive checks make sure we don't refuse more than for named files.
renameat2() with RENAME_WHITEOUT leaves a whiteout in place of the
source. That's a character device owned by the caller's fsuid and fsgid
which the file system creates from its ->rename() method, so it never
passes the path_mknod hook. Since Linux 5.8 this doesn't require any
privileges either.
So a managed user namespace that can rename an entry on a file system
that outlives it, e.g., in a world-writable directory the user prepared
from the outside, can leave a whiteout owned by its transient range
behind.
The only hook that sees this is path_rename, which gets the flags.
Refuse RENAME_WHITEOUT there whenever validate_mount() would refuse a
mknod in the source directory.
The userns-restrict policy keeps a managed user namespace from creating
inodes owned by its transient range on file systems that outlive it.
It does so from the path_mknod, path_mkdir, path_symlink and path_link
hooks. But O_TMPFILE never reaches any of them. vfs_tmpfile() creates
the inode owned by the caller's fsuid and fsgid without calling a path
or inode creation hook at all.
That alone wouldn't be a problem since such an inode goes away with the
last reference to it. But without O_EXCL vfs_tmpfile() marks it
I_LINKABLE and whoever holds the fd can give it a name later. That
doesn't even need to happen inside the user namespace. The fd can be
handed to a process outside of it, e.g., the user's shell, which links
it via /proc/self/fd/<nr> with AT_SYMLINK_FOLLOW. The policy doesn't
look at that process at all so the file stays behind, owned by the
transient range. With mapForeign the user namespace can even do it
itself by switching its fsuid and fsgid to the foreign range before
calling linkat(), as path_link only checks the ids of the caller.
Once the range is recycled the file belongs to the next client. And
since chmod() isn't covered by the policy it may well be setuid.
So refuse a linkable O_TMPFILE from the file_open hook whenever
validate_mount() would refuse a named file in the same place. Note that
security_file_open() runs before do_dentry_open() strips O_EXCL from
f_flags so we can still see it. An O_TMPFILE with O_EXCL can never be
linked and is left alone, so tmpfile(3) keeps working.
Formatting a JSON variant and then writing it to a file via
write_string_file_at() is a common pattern. Let's add a helper that
combines the two, so that callers don't have to deal with the
intermediary string buffer anymore. The buffer is erased after use,
since JSON objects might carry sensitive data.
TPM2_PolicyOR() takes at most eight branch digests (the TPM's
TPML_DIGEST is fixed at eight elements). pcrlock hits this limit as soon
as a PCR has more than eight predicted values, for example when several
kernels or boot loader versions are covered by a single policy. It then
fails with E2BIG.
Lift the limit by nesting PolicyORs into a balanced tree with a fanout
of eight, as the TCG TSS JSON policy format prescribes for more than
eight branches: the leaves are the actual branches and every layer above
them contains only PolicyORs, so all leaves end up at the same depth.
Each level is partitioned evenly, which avoids pointless single-branch
ORs.
The tree is materialized by a helper shared between the calculating
(tpm2_calculate_policy_or()) and the submitting (tpm2_policy_or()) side,
so sealing and unsealing derive the exact same digests from the same
branch list. For eight or fewer branches the previous flat code path is
used unchanged, which keeps existing policies and sealed PINs working.
Every extra level costs one more TPM round trip during unseal, hence the
nesting depth is capped at TPM2_POLICY_OR_MAX_DEPTH (five, i.e. up to
8^5 = 32768 alternatives per PCR). If that's not enough for SUSE, we can
discuss it :-) cc @aplanas
This also drops the hard "more than 8 alternatives per PCR" rejection
from the super PCR policy calculation and submission.
I was not able to run the integration testing yet with mkosi due to some
NixOS stuff, sorry!
Let's make use of the new helper wherever we format a JSON variant and
write it to a file right away.
This also ports the DHCP server lease file writer, which so far
open-coded the temporary file + rename logic.
Formatting a JSON variant and then writing it to a file via
write_string_file_at() is a common pattern. Let's add a helper that
combines the two, so that callers don't have to deal with the
intermediary string buffer anymore. The buffer is erased after use,
since JSON objects might carry sensitive data.
This extends the selection logic of sysexts/confexts and any vpick
resource substantially:
* vpick will now look for user.uapi.match_xyz xattrs on the files inside
a .v/ directory. if they are defined they can contain a match against
the system. non-matching systems will skip over these entries. This is
useful to condition .v/ items against machines with specific
hostnames/machine-ids/machine-tags.
* sysext will also look for a set of similar MATCH_XYZ= fields in
extension-release and skip over non-matching sysexts.
(why check this twice, once one the xattr level, and once in
extension-release? xattrs can get lost, we consider them mostly an
optimization. Moreover they are not covered by the cryptographic
protection of the disk image, hence it makes sense to have a strcicter
rule embedded into the image)
pidfd_get_inode_id_self_cached() keys its cache on the PID only. This is
not sufficient to detect that we are in a different process: if we are
PID 1 of a PID namespace and fork off a child into a new PID namespace,
the child will be PID 1 too, and would then continue to use the parent's
cached pidfd ID. For example, a nested nspawn running as PID 1 would fill
the cache in sd_listen_fds() (which checks $LISTEN_PIDFDID), and then
pass its own ID to the payload, which would in turn drop the fds passed
to it.
Fix this by explicitly flushing the cache from reset_cached_pid(), i.e.
the pthread_atfork() child handler and from raw_clone(), the same way we
already flush the cached PID.
Also add a test that covers the PID 1 → PID 1 case.
Quite a few places read an integer sysctl and parse it manually, each in
its own way: some via read_one_line_file() on a hand-built /proc/sys/
path, some via sysctl_read() followed by safe_atoX(), parse_boolean() or
even a plain string comparison, and parsing into various integer types
(int, unsigned, unsigned long, uint64_t).
Add three helpers, sysctl_read_int(), sysctl_read_unsigned() and
sysctl_read_uint64(), plus a sysctl_read_uint32() alias for
sysctl_read_unsigned() for places where the value is stored in a
strictly 32-bit type (such as uid_t/gid_t), and a sysctl_read_lu()
wrapper that maps to sysctl_read_uint32() or sysctl_read_uint64(),
depending on the size of unsigned long. Then port all these users over:
- kernel.cap_last_cap (cap_last_cap() and test-capability-util)
- kernel.random.poolsize (random_pool_size())
- fs.nr_open (read_nr_open())
- kernel.pid_max and kernel.threads-max (procfs_get_pid_max(),
procfs_get_threads_max())
- net.unix.max_dgram_qlen (PID 1's bump_unix_max_dgram_qlen())
- kernel.overflowuid and kernel.overflowgid (PID 1's taint logic)
- fs.protected_hardlinks (tmpfiles)
- kernel.apparmor_restrict_unprivileged_userns (test-execute)
Note that fs.protected_hardlinks is read as integer instead of boolean
now, since the kernel exposes it as such (even if it only accepts 0 and
1 for now). This matters even more for its siblings fs.protected_regular
and fs.protected_fifos which accept 0, 1 and 2, and hence cannot be
parsed via parse_boolean().
PID 1 keeps tainting itself if kernel.overflowuid/kernel.overflowgid
cannot be parsed, as before, but not if they cannot be read at all.
sd_json_variant_dump() can flush the stream itself, and propagates the
error then. Let's make use of that where we so far called
fflush_and_check() right after dumping, and ignored the return value of
sd_json_variant_dump().
Up to now the policy reference for the signed PCR policy could only
be set with --tpm2-public-key-policyref= on the command line. An OS
that wants its root partition to only be unlockable from the initrd
hence had to override the systemd-repart.service command line with a
drop-in.
Add a TPM2PublicKeyPolicyRef= setting to partition definitions, which
the command line option overrides, as with TPM2PCRs= and --tpm2-pcrs=.
Like before, the policy reference is only used together with a PCR
public key.
---
repart,cryptenroll: only use policy reference when a public key exists
When no PCR public key is found, repart/cryptenroll proceed without
binding to a signed PCR policy, and they already leave out the policy
reference when calculating the sealing policy. They still passed it to
tpm2_make_luks2_json()/tpm2_unseal() which both assert that a policy
reference comes with a public key, causing a crash. Pass the policy
reference only when a public key is set, like for the sealing policy.
It was a thin wrapper around assert_se(), so its failures did not report
the value involved. Convert the remaining users to ASSERT_RETURN_EXPECTED()
around the typed macros and drop the wrapper.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The check that a relative timestamp round-trips within a month was
parsed as y > x ? (y - x) : (x - y <= bound) and so only applied the
bound in one direction; it now bounds both.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The last check of the strv_push_with_size test had
assert_se(n = strv_length(a)), an assignment that only required the
length to be non-zero; it now compares the length with the size counter
as intended.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Calls whose result was only checked for being non-zero are now checked
with ASSERT_OK_POSITIVE(), which additionally rejects negative errors.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Pairs of a failing call followed by an errno check are merged into
ASSERT_ERROR_ERRNO().
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The isempty() checks become ASSERT_STREQ(..., ""), which additionally
rejects NULL; the strings in question are never NULL at those points.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The check that a removed subdirectory is gone required access() to fail;
it now requires ENOENT.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The close_nointr() checks in the fd_move_above_stdio test compared the
negative errno return with a positive EBADF and could never fail; they
now require the calls to succeed.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The isempty() checks become ASSERT_STREQ(..., ""), which additionally
rejects NULL; the strings in question are never NULL at those points.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The check on copy_file_at()'s return value accepted any non-zero value,
errors included; it now requires success.
The copied data is compared with memcmp() instead of strneq(), since the
buffers hold raw file contents that are not NUL-terminated.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Calls whose result was only checked for being non-zero are now checked
with ASSERT_OK_POSITIVE(), which additionally rejects negative errors.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The asprintf() checks accepted -1 as success; they now require success.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Calls whose result was only checked for being non-zero are now checked
with ASSERT_OK_POSITIVE(), which additionally rejects negative errors.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Calls whose result was only checked for being non-zero are now checked
with ASSERT_OK_POSITIVE(), which additionally rejects negative errors.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Several checks had assert_se(id1 = id2), an assignment rather than the
comparison of the two mount IDs that the neighbouring checks make; they
now compare.
The NULL check after the second path_join() tested link1 instead of
link2; it now checks the right variable.
The checks on the descriptors returned by open() required fd > 0; they
now accept any valid descriptor, including 0.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
The checks that the overridden and masked scripts did not run required
access() to fail; they now require ENOENT.
Replace assert_se() with the typed ASSERT_* macros throughout, so that
a failing assertion logs the values involved.
Calls whose result was only checked for being non-zero are now checked
with ASSERT_OK_POSITIVE(), which additionally rejects negative errors.