homed/fscrypt: default new homes to v2 policies

fscrypt v1 policies bind master keys to the calling process's keyring,
which means files in a homed-managed directory aren't readable when
accessed through a container bind mount, a different mount namespace,
or by any process other than the one that first unlocked the home.
Reading a file from outside such a context first warms the buffer cache
and papers over the symptom (#18280), but the underlying problem (the
key not flowing across keyrings) remains.

v2 policies (Linux 5.4+) route master keys through the filesystem
keyring via FS_IOC_ADD_ENCRYPTION_KEY / FS_IOC_REMOVE_ENCRYPTION_KEY,
so the key is visible to every process accessing the filesystem.

Switch homed to v2:

- Read the existing policy via FS_IOC_GET_ENCRYPTION_POLICY_EX, which
  reports both v1 and v2 policies. The ioctl is available since Linux
  5.4, i.e. on every kernel we support (our baseline is 5.10), so no
  fallback to the v1-only FS_IOC_GET_ENCRYPTION_POLICY is needed.
- Drive slot matching off a full fscrypt_key_specifier (HomeSetup now
  carries that instead of a bare 8-byte descriptor). Slot decryption
  derives either the v1 descriptor (SHA-512 double hash) or the v2
  identifier (HKDF-SHA512 with the kernel's info string), and compares
  against the policy.
- Install the master key the right way per version: add_key("logon", ...)
  to thread+user keyrings for v1, FS_IOC_ADD_ENCRYPTION_KEY for v2.
- home_flush_keyring_fscrypt opens the image directory, looks up the
  policy, and either calls FS_IOC_REMOVE_ENCRYPTION_KEY (v2) or walks
  the user keyring (v1).
- New homes default to v2; fall back to v1 only if the kernel rejects
  FS_IOC_ADD_ENCRYPTION_KEY with ENOTTY/EOPNOTSUPP. The v2 create path
  derives the identifier locally first, passes it to ADD_KEY as
  expected_identifier, and cleans up via REMOVE_KEY if SET_POLICY then
  fails, so the v1 fallback never sees a stranded key. Existing v1
  homes continue to unlock, rekey, and deactivate as before.
- A v2 master key installed to work on an inactive home is always
  removed again unless that home ends up activated. v2 keys persist in
  the filesystem keyring until removed explicitly (v1 keys instead died
  with the homework process' keyring), so a key left behind would leave
  a home nobody activated readable until the next deactivation or reboot.
  home_setup_fscrypt() and home_create_fscrypt() therefore arm a rollback
  right after installing the key; the activation path disarms it once the
  mount is in place (the live home owns the key), while every other path
  -- create, and passwd/update/resize of an inactive home, plus all error
  paths -- rolls it back via home_setup_done(). Activation reinstalls the
  key.

The v1 and v2 on-disk policy formats differ (v1: 8-byte descriptor;
v2: 16-byte identifier) and fscrypt has no in-place upgrade path, so
a v1 home is always unlocked via the v1 code path and a v2 home is
always unlocked via the v2 code path, regardless of kernel version.

Slot xattr format is unchanged: the master key is the same, only how it
binds to the directory changes.

Fixes #18280.

Ananth Bhaskararaman antsub@gmail.com
This commit is contained in:
Ananth Bhaskararaman
2026-07-21 10:18:06 +05:30
parent 769e35369c
commit 571253fdbe
7 changed files with 675 additions and 172 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ REQUIREMENTS:
≥ 5.2 for cgroup freezer and new mount API
≥ 5.3 for bounded loops in BPF program, keyring namespacing,
and nexthop support
≥ 5.4 for pidfd and signed Verity images
≥ 5.4 for pidfd, signed Verity images, and fscrypt v2 policies
≥ 5.6 for getrandom() GRND_INSECURE
≥ 5.7 for CLONE_INTO_CGROUP, cgroup2fs memory_recursiveprot option,
BPF links and the BPF LSM hook
+6
View File
@@ -7,6 +7,7 @@
#include "fd-util.h"
#include "homework-blob.h"
#include "homework-directory.h"
#include "homework-fscrypt.h"
#include "homework-mount.h"
#include "homework-quota.h"
#include "log.h"
@@ -103,6 +104,11 @@ int home_activate_directory(
setup->undo_mount = false;
/* The home is now activated and staying mounted, so it owns any v2 fscrypt master key
* home_setup() installed. Disarm the rollback so home_setup_done() doesn't remove the key out
* from under the live mount. (No-op for v1 or non-fscrypt storage.) */
fscrypt_v2_key_undo_disarm(&setup->fscrypt_v2_key_undo);
setup->do_drop_caches = false;
log_info("Everything completed.");
+1
View File
@@ -9,3 +9,4 @@ typedef enum HomeSetupFlags HomeSetupFlags;
typedef struct PasswordCache PasswordCache;
typedef struct HomeSetup HomeSetup;
typedef struct FscryptV2KeyUndo FscryptV2KeyUndo;
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -3,10 +3,16 @@
#include "homework-forward.h"
int home_setup_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache);
int home_setup_fscrypt(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, const PasswordCache *cache);
int home_create_fscrypt(UserRecord *h, HomeSetup *setup, char **effective_passwords, UserRecord **ret_home);
int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords);
int home_flush_keyring_fscrypt(UserRecord *h);
/* Fire the pending v2 master-key rollback (remove the key from the filesystem keyring) if armed; called
* from home_setup_done() on every teardown/error path. No-op if unarmed or v1. */
void fscrypt_v2_key_undo_done(FscryptV2KeyUndo *u);
/* Cancel a pending v2 rollback without removing the key: the home is staying active and now owns it. */
void fscrypt_v2_key_undo_disarm(FscryptV2KeyUndo *u);
+6 -1
View File
@@ -459,6 +459,11 @@ int home_setup_done(HomeSetup *setup) {
setup->key_serial = keyring_unlink(setup->key_serial);
/* Roll back a v2 fscrypt master key that home_setup_fscrypt() installed but that no activated
* home ended up owning (passwd/update/resize of an inactive home, or any error path). On the
* activation path home_activate_directory() disarms this first, so the live home keeps its key. */
fscrypt_v2_key_undo_done(&setup->fscrypt_v2_key_undo);
setup->undo_mount = false;
setup->undo_dm = false;
setup->do_offline_fitrim = false;
@@ -514,7 +519,7 @@ int home_setup(
break;
case USER_FSCRYPT:
r = home_setup_fscrypt(h, setup, cache);
r = home_setup_fscrypt(h, flags, setup, cache);
break;
case USER_CIFS:
+18 -1
View File
@@ -9,6 +9,21 @@
#include "homework-forward.h"
#include "user-record-util.h"
/* RAII rollback state for a v2 fscrypt master-key installation. v2 keys live in the persistent
* filesystem keyring (removed only via FS_IOC_REMOVE_ENCRYPTION_KEY), so any setup that installs one
* but then tears the home back down instead of activating it must remove the key again, or it lingers
* filesystem-wide until the next deactivation or reboot. Armed by home_setup_fscrypt(), fired by
* home_setup_done() on every teardown/error path, and disarmed only once a home stays active (see
* home_activate_directory()). 'dir_fd' is a dup taken at arm time so the removal target is pinned to
* the superblock and unaffected by later rename()/mount changes. (v1 keys go to the per-thread keyring
* and die with the homework process, so they need none of this.) */
typedef struct FscryptV2KeyUndo {
int dir_fd;
uint8_t identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
} FscryptV2KeyUndo;
#define FSCRYPT_V2_KEY_UNDO_INIT { .dir_fd = -EBADF }
typedef struct HomeSetup {
char *dm_name; /* "home-<username>" */
char *dm_node; /* "/dev/mapper/home-<username>" */
@@ -21,7 +36,8 @@ typedef struct HomeSetup {
sd_id128_t found_luks_uuid;
sd_id128_t found_fs_uuid;
uint8_t fscrypt_key_descriptor[FS_KEY_DESCRIPTOR_SIZE];
struct fscrypt_key_specifier fscrypt_key_spec;
FscryptV2KeyUndo fscrypt_v2_key_undo;
void *volume_key;
size_t volume_key_size;
@@ -47,6 +63,7 @@ typedef struct HomeSetup {
{ \
.root_fd = -EBADF, \
.image_fd = -EBADF, \
.fscrypt_v2_key_undo = FSCRYPT_V2_KEY_UNDO_INIT, \
.partition_offset = UINT64_MAX, \
.partition_size = UINT64_MAX, \
.key_serial = -1, \