diff --git a/TODO b/TODO index 4f2d962b3d..8ecd486be8 100644 --- a/TODO +++ b/TODO @@ -226,7 +226,6 @@ Features: - rollback when resize fails mid-operation - GNOME's side for forget key on suspend (requires rework so that lock screen runs outside of uid) - resize on login? - - fstrim on logout? - shrink fs on logout? - update LUKS password on login if we find there's a password that unlocks the JSON record but not the LUKS device. - create on activate? diff --git a/docs/HOME_DIRECTORY.md b/docs/HOME_DIRECTORY.md index c517cf573c..d82aca7065 100644 --- a/docs/HOME_DIRECTORY.md +++ b/docs/HOME_DIRECTORY.md @@ -168,6 +168,10 @@ If the UID assigned to a user does not match the owner of the home directory in the file system, the home directory is automatically and recursively `chown()`ed to the correct UID. -Depending on the `discard` setting of the user record either the backing +Depending on the `luksDiscard` setting of the user record either the backing loopback file is `fallocate()`ed during activation, or the mounted file system is `FITRIM`ed after mounting, to ensure the setting is correctly enforced. + +When deactivating a home directory, the file system or block device is trimmed +or extended as configured in the `luksOfflineDiscard` setting of the user +record. diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 72acbcc026..6269b1e6ca 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -455,6 +455,10 @@ storage. If false and `luks` storage is used turns this behavior off. In addition, depending on this setting an `FITRIM` or `fallocate()` operation is executed to make sure the image matches the selected option. +`luksOfflineDiscard` → A boolean. Similar to `luksDiscard`, it controls whether +to trim/allocate the file system/backing file when deactivating the home +directory. + `luksCipher` → A string, indicating the cipher to use for the LUKS storage mechanism. `luksCipherMode` → A string, selecting the cipher mode to use for the LUKS storage mechanism. @@ -648,11 +652,12 @@ that may be used in this section are identical to the equally named ones in the `mountNoDevices`, `mountNoSuid`, `mountNoExecute`, `cifsDomain`, `cifsUserName`, `cifsService`, `imagePath`, `uid`, `gid`, `memberOf`, `fileSystemType`, `partitionUuid`, `luksUuid`, `fileSystemUuid`, `luksDiscard`, -`luksCipher`, `luksCipherMode`, `luksVolumeKeySize`, `luksPbkdfHashAlgorithm`, -`luksPbkdfType`, `luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`, -`luksPbkdfParallelThreads`, `rateLimitIntervalUSec`, `rateLimitBurst`, -`enforcePasswordPolicy`, `autoLogin`, `stopDelayUSec`, `killProcesses`, -`passwordChangeMinUSec`, `passwordChangeMaxUSec`, `passwordChangeWarnUSec`, +`luksOfflineDiscard`, `luksOfflineDiscard`, `luksCipher`, `luksCipherMode`, +`luksVolumeKeySize`, `luksPbkdfHashAlgorithm`, `luksPbkdfType`, +`luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`, `luksPbkdfParallelThreads`, +`rateLimitIntervalUSec`, `rateLimitBurst`, `enforcePasswordPolicy`, +`autoLogin`, `stopDelayUSec`, `killProcesses`, `passwordChangeMinUSec`, +`passwordChangeMaxUSec`, `passwordChangeWarnUSec`, `passwordChangeInactiveUSec`, `passwordChangeNow`, `pkcs11TokenUri`. ## Fields in the `binding` section diff --git a/man/homectl.xml b/man/homectl.xml index 6cec7a13c8..632c8b95bb 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -544,6 +544,16 @@ loopback file) the discard logic defaults to on. + + BOOL + + Similar to , controls the trimming of the file + system. However, while controls what happens when the home directory + is active, controls what happens when it becomes inactive, + i.e. whether to trim/allocate the storage when deactivating the home directory. This option defaults + to on, to ensure disk space is minimized while a user is not logged in. + + CIPHER MODE diff --git a/src/home/homectl.c b/src/home/homectl.c index 7c1c69495c..66cbcfd9e4 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2226,6 +2226,9 @@ static int help(int argc, char *argv[], void *userdata) { " --fs-type=TYPE File system type to use in case of luks\n" " storage (ext4, xfs, btrfs)\n" " --luks-discard=BOOL Whether to use 'discard' feature of file system\n" + " when activated (mounted)\n" + " --luks-offline-discard=BOOL\n" + " Whether to trim file on logout\n" " --luks-cipher=CIPHER Cipher to use for LUKS encryption\n" " --luks-cipher-mode=MODE Cipher mode to use for LUKS encryption\n" " --luks-volume-key-size=BITS\n" @@ -2279,6 +2282,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_IMAGE_PATH, ARG_UMASK, ARG_LUKS_DISCARD, + ARG_LUKS_OFFLINE_DISCARD, ARG_JSON, ARG_SETENV, ARG_TIMEZONE, @@ -2372,6 +2376,7 @@ static int parse_argv(int argc, char *argv[]) { { "image-path", required_argument, NULL, ARG_IMAGE_PATH }, { "fs-type", required_argument, NULL, ARG_FS_TYPE }, { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD }, + { "luks-offline-discard", required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD }, { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER }, { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE }, { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE }, @@ -2941,6 +2946,25 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_LUKS_OFFLINE_DISCARD: + if (isempty(optarg)) { + r = drop_from_identity("luksOfflineDiscard"); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --luks-offline-discard= parameter: %s", optarg); + + r = json_variant_set_field_boolean(&arg_identity_extra, "luksOfflineDiscard", r); + if (r < 0) + return log_error_errno(r, "Failed to set offline discard field: %m"); + + break; + case ARG_LUKS_VOLUME_KEY_SIZE: case ARG_LUKS_PBKDF_PARALLEL_THREADS: case ARG_RATE_LIMIT_BURST: { diff --git a/src/home/homed-home.c b/src/home/homed-home.c index c98e18c686..07356e60c4 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -1002,6 +1002,8 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord if (r < 0) return r; if (r == 0) { + const char *homework; + /* Child */ if (setenv("NOTIFY_SOCKET", "/run/systemd/home/notify", 1) < 0) { @@ -1017,7 +1019,11 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord stdin_fd = stdout_fd = -1; /* have been invalidated by rearrange_stdio() */ - execl(SYSTEMD_HOMEWORK_PATH, SYSTEMD_HOMEWORK_PATH, verb, NULL); + /* Allow overriding the homework path via an environment variable, to make debugging + * easier. */ + homework = getenv("SYSTEMD_HOMEWORK_PATH") ?: SYSTEMD_HOMEWORK_PATH; + + execl(homework, homework, verb, NULL); log_error_errno(errno, "Failed to invoke " SYSTEMD_HOMEWORK_PATH ": %m"); _exit(EXIT_FAILURE); } diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index 5eb67bc2b3..694f215b30 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -893,19 +893,19 @@ int home_store_header_identity_luks( return 1; } -static int run_fitrim(int root_fd) { +int run_fitrim(int root_fd) { char buf[FORMAT_BYTES_MAX]; struct fstrim_range range = { .len = UINT64_MAX, }; /* If discarding is on, discard everything right after mounting, so that the discard setting takes - * effect on activation. */ + * effect on activation. (Also, optionally, trim on logout) */ assert(root_fd >= 0); if (ioctl(root_fd, FITRIM, &range) < 0) { - if (IN_SET(errno, ENOTTY, EOPNOTSUPP, EBADF)) { + if (ERRNO_IS_NOT_SUPPORTED(errno) || errno == EBADF) { log_debug_errno(errno, "File system does not support FITRIM, not trimming."); return 0; } @@ -918,15 +918,32 @@ static int run_fitrim(int root_fd) { return 1; } -static int run_fallocate(int backing_fd, const struct stat *st) { +int run_fitrim_by_path(const char *root_path) { + _cleanup_close_ int root_fd = -1; + + root_fd = open(root_path, O_RDONLY|O_DIRECTORY|O_CLOEXEC); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open file system '%s' for trimming: %m", root_path); + + return run_fitrim(root_fd); +} + +int run_fallocate(int backing_fd, const struct stat *st) { char buf[FORMAT_BYTES_MAX]; + struct stat stbuf; assert(backing_fd >= 0); - assert(st); /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes * effect on activation */ + if (!st) { + if (fstat(backing_fd, &stbuf) < 0) + return log_error_errno(errno, "Failed to fstat(): %m"); + + st = &stbuf; + } + if (!S_ISREG(st->st_mode)) return 0; @@ -955,6 +972,16 @@ static int run_fallocate(int backing_fd, const struct stat *st) { return 1; } +int run_fallocate_by_path(const char *backing_path) { + _cleanup_close_ int backing_fd = -1; + + backing_fd = open(backing_path, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (backing_fd < 0) + return log_error_errno(errno, "Failed to open '%s' for fallocate(): %m", backing_path); + + return run_fallocate(backing_fd, NULL); +} + int home_prepare_luks( UserRecord *h, bool already_activated, @@ -1111,7 +1138,7 @@ int home_prepare_luks( h->luks_volume_key_size, h->password, pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, - user_record_luks_discard(h), + user_record_luks_discard(h) || user_record_luks_offline_discard(h), &cd, &found_luks_uuid, &volume_key, @@ -1147,6 +1174,9 @@ int home_prepare_luks( if (user_record_luks_discard(h)) (void) run_fitrim(root_fd); + + setup->image_fd = TAKE_FD(fd); + setup->do_offline_fallocate = !(setup->do_offline_fitrim = user_record_luks_offline_discard(h)); } setup->loop = TAKE_PTR(loop); @@ -1259,6 +1289,7 @@ int home_activate_luks( return r; setup.undo_mount = false; + setup.do_offline_fitrim = false; loop_device_relinquish(setup.loop); @@ -1267,6 +1298,7 @@ int home_activate_luks( log_warning_errno(r, "Failed to relinquish DM device, ignoring: %m"); setup.undo_dm = false; + setup.do_offline_fallocate = false; log_info("Everything completed."); @@ -1279,6 +1311,7 @@ int home_activate_luks( int home_deactivate_luks(UserRecord *h) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + bool we_detached; int r; /* Note that the DM device and loopback device are set to auto-detach, hence strictly speaking we @@ -1293,23 +1326,45 @@ int home_deactivate_luks(UserRecord *h) { r = crypt_init_by_name(&cd, dm_name); if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) { - log_debug_errno(r, "LUKS device %s is already detached.", dm_name); - return false; + log_debug_errno(r, "LUKS device %s has already been detached.", dm_name); + we_detached = false; } else if (r < 0) return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + else { + log_info("Discovered used LUKS device %s.", dm_node); - log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); - crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + r = crypt_deactivate(cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) { + log_debug_errno(r, "LUKS device %s is already detached.", dm_node); + we_detached = false; + } else if (r < 0) + return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node); + else { + log_info("LUKS device detaching completed."); + we_detached = true; + } + } - r = crypt_deactivate(cd, dm_name); - if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) - log_debug_errno(r, "LUKS device %s is already detached.", dm_node); - else if (r < 0) - return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node); + if (user_record_luks_offline_discard(h)) + log_debug("Not allocating on logout."); + else + (void) run_fallocate_by_path(user_record_image_path(h)); - log_info("LUKS device detaching completed."); - return true; + return we_detached; +} + +int home_trim_luks(UserRecord *h) { + assert(h); + + if (!user_record_luks_offline_discard(h)) { + log_debug("Not trimming on logout."); + return 0; + } + + (void) run_fitrim_by_path(user_record_home_directory(h)); + return 0; } static int run_mkfs( @@ -1918,7 +1973,9 @@ int home_create_luks( if (asprintf(&disk_uuid_path, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(luks_uuid)) < 0) return log_oom(); - if (user_record_luks_discard(h)) { + if (user_record_luks_discard(h) || user_record_luks_offline_discard(h)) { + /* If we want online or offline discard, discard once before we start using things. */ + if (ioctl(image_fd, BLKDISCARD, (uint64_t[]) { 0, block_device_size }) < 0) log_full_errno(errno == EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, errno, "Failed to issue full-device BLKDISCARD on device, ignoring: %m"); @@ -2004,7 +2061,7 @@ int home_create_luks( user_record_user_name_and_realm(h), pkcs11_decrypted_passwords, effective_passwords, - user_record_luks_discard(h), + user_record_luks_discard(h) || user_record_luks_offline_discard(h), h, &cd); if (r < 0) @@ -2084,6 +2141,12 @@ int home_create_luks( goto fail; } + if (user_record_luks_offline_discard(h)) { + r = run_fitrim(root_fd); + if (r < 0) + goto fail; + } + root_fd = safe_close(root_fd); r = umount_verbose("/run/systemd/user-home-mount"); @@ -2102,6 +2165,12 @@ int home_create_luks( loop = loop_device_unref(loop); + if (!user_record_luks_offline_discard(h)) { + r = run_fallocate(image_fd, NULL /* refresh stat() data */); + if (r < 0) + goto fail; + } + if (disk_uuid_path) (void) ioctl(image_fd, BLKRRPART, 0); diff --git a/src/home/homework-luks.h b/src/home/homework-luks.h index 581255a223..bd51f5da50 100644 --- a/src/home/homework-luks.h +++ b/src/home/homework-luks.h @@ -9,6 +9,7 @@ int home_prepare_luks(UserRecord *h, bool already_activated, const char *force_i int home_activate_luks(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); int home_deactivate_luks(UserRecord *h); +int home_trim_luks(UserRecord *h); int home_store_header_identity_luks(UserRecord *h, HomeSetup *setup, UserRecord *old_home); @@ -36,3 +37,8 @@ static inline uint64_t luks_volume_key_size_convert(struct crypt_device *cd) { return (uint64_t) k; } + +int run_fitrim(int root_fd); +int run_fitrim_by_path(const char *root_path); +int run_fallocate(int backing_fd, const struct stat *st); +int run_fallocate_by_path(const char *backing_path); diff --git a/src/home/homework.c b/src/home/homework.c index 38b740729c..77afc402d3 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -163,7 +163,15 @@ int home_setup_undo(HomeSetup *setup) { assert(setup); - setup->root_fd = safe_close(setup->root_fd); + if (setup->root_fd >= 0) { + if (setup->do_offline_fitrim) { + q = run_fitrim(setup->root_fd); + if (q < 0) + r = q; + } + + setup->root_fd = safe_close(setup->root_fd); + } if (setup->undo_mount) { q = umount_verbose("/run/systemd/user-home-mount"); @@ -177,8 +185,20 @@ int home_setup_undo(HomeSetup *setup) { r = q; } + if (setup->image_fd >= 0) { + if (setup->do_offline_fallocate) { + q = run_fallocate(setup->image_fd, NULL); + if (q < 0) + r = q; + } + + setup->image_fd = safe_close(setup->image_fd); + } + setup->undo_mount = false; setup->undo_dm = false; + setup->do_offline_fitrim = false; + setup->do_offline_fallocate = false; setup->dm_name = mfree(setup->dm_name); setup->dm_node = mfree(setup->dm_node); @@ -666,6 +686,12 @@ static int home_deactivate(UserRecord *h, bool force) { if (r < 0) return r; if (r == USER_TEST_MOUNTED) { + if (user_record_storage(h) == USER_LUKS) { + r = home_trim_luks(h); + if (r < 0) + return r; + } + if (umount2(user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0)) < 0) return log_error_errno(errno, "Failed to unmount %s: %m", user_record_home_directory(h)); diff --git a/src/home/homework.h b/src/home/homework.h index 81698b7601..3bcf3ad9b0 100644 --- a/src/home/homework.h +++ b/src/home/homework.h @@ -17,6 +17,7 @@ typedef struct HomeSetup { LoopDevice *loop; struct crypt_device *crypt_device; int root_fd; + int image_fd; sd_id128_t found_partition_uuid; sd_id128_t found_luks_uuid; sd_id128_t found_fs_uuid; @@ -28,6 +29,8 @@ typedef struct HomeSetup { bool undo_dm; bool undo_mount; + bool do_offline_fitrim; + bool do_offline_fallocate; uint64_t partition_offset; uint64_t partition_size; @@ -36,6 +39,7 @@ typedef struct HomeSetup { #define HOME_SETUP_INIT \ { \ .root_fd = -1, \ + .image_fd = -1, \ .partition_offset = UINT64_MAX, \ .partition_size = UINT64_MAX, \ } diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index 7b14636ebf..d8b46f2db2 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -279,7 +279,7 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { printf(" Access Mode: 0%03oo\n", user_record_access_mode(hr)); if (storage == USER_LUKS) { - printf("LUKS Discard: %s\n", yes_no(user_record_luks_discard(hr))); + printf("LUKS Discard: online=%s offline=%s\n", yes_no(user_record_luks_discard(hr)), yes_no(user_record_luks_offline_discard(hr))); if (!sd_id128_is_null(hr->luks_uuid)) printf(" LUKS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->luks_uuid)); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 7832aca8dc..f648311208 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -52,6 +52,7 @@ UserRecord* user_record_new(void) { .nodev = true, .nosuid = true, .luks_discard = -1, + .luks_offline_discard = -1, .luks_volume_key_size = UINT64_MAX, .luks_pbkdf_time_cost_usec = UINT64_MAX, .luks_pbkdf_memory_cost = UINT64_MAX, @@ -944,6 +945,7 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, }, + { "luksOfflineDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_offline_discard), 0, }, { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, @@ -1276,6 +1278,7 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 }, + { "luksOfflineDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_offline_discard), 0 }, { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, @@ -1500,6 +1503,27 @@ bool user_record_luks_discard(UserRecord *h) { return path_startswith(ip, "/dev/"); } +bool user_record_luks_offline_discard(UserRecord *h) { + const char *ip; + + assert(h); + + if (h->luks_offline_discard >= 0) + return h->luks_offline_discard; + + /* Discard while we are logged out should generally be a good idea, except when operating directly on + * physical media, where we should just bind it to the online discard mode. */ + + ip = user_record_image_path(h); + if (!ip) + return false; + + if (path_startswith(ip, "/dev/")) + return user_record_luks_discard(h); + + return true; +} + const char *user_record_luks_cipher(UserRecord *h) { assert(h); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 5bac304767..83c5a71d4e 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -261,6 +261,7 @@ typedef struct UserRecord { sd_id128_t file_system_uuid; int luks_discard; + int luks_offline_discard; char *luks_cipher; char *luks_cipher_mode; uint64_t luks_volume_key_size; @@ -332,6 +333,7 @@ const char *user_record_cifs_user_name(UserRecord *h); const char *user_record_shell(UserRecord *h); const char *user_record_real_name(UserRecord *h); bool user_record_luks_discard(UserRecord *h); +bool user_record_luks_offline_discard(UserRecord *h); const char *user_record_luks_cipher(UserRecord *h); const char *user_record_luks_cipher_mode(UserRecord *h); uint64_t user_record_luks_volume_key_size(UserRecord *h);