diff --git a/contrib/pam_zfs_key/pam_zfs_key.c b/contrib/pam_zfs_key/pam_zfs_key.c index 08a8640669b3..c617a6e6b370 100644 --- a/contrib/pam_zfs_key/pam_zfs_key.c +++ b/contrib/pam_zfs_key/pam_zfs_key.c @@ -63,6 +63,7 @@ pam_syslog(pam_handle_t *pamh, int loglevel, const char *fmt, ...) #include #include #include +#include #include @@ -370,67 +371,6 @@ change_key(pam_handle_t *pamh, const char *ds_name, return (0); } -static int -decrypt_mount(pam_handle_t *pamh, const char *ds_name, - const char *passphrase, boolean_t noop) -{ - zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); - if (ds == NULL) { - pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); - return (-1); - } - pw_password_t *key = prepare_passphrase(pamh, ds, passphrase, NULL); - if (key == NULL) { - zfs_close(ds); - return (-1); - } - int ret = lzc_load_key(ds_name, noop, (uint8_t *)key->value, - WRAPPING_KEY_LEN); - pw_free(key); - if (ret && ret != EEXIST) { - pam_syslog(pamh, LOG_ERR, "load_key failed: %d", ret); - zfs_close(ds); - return (-1); - } - if (noop) { - goto out; - } - ret = zfs_mount(ds, NULL, 0); - if (ret) { - pam_syslog(pamh, LOG_ERR, "mount failed: %d", ret); - zfs_close(ds); - return (-1); - } -out: - zfs_close(ds); - return (0); -} - -static int -unmount_unload(pam_handle_t *pamh, const char *ds_name, boolean_t force) -{ - zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); - if (ds == NULL) { - pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); - return (-1); - } - int ret = zfs_unmount(ds, NULL, force ? MS_FORCE : 0); - if (ret) { - pam_syslog(pamh, LOG_ERR, "zfs_unmount failed with: %d", ret); - zfs_close(ds); - return (-1); - } - - ret = lzc_unload_key(ds_name); - if (ret) { - pam_syslog(pamh, LOG_ERR, "unload_key failed with: %d", ret); - zfs_close(ds); - return (-1); - } - zfs_close(ds); - return (0); -} - typedef struct { char *homes_prefix; char *runstatedir; @@ -443,6 +383,7 @@ typedef struct { boolean_t unmount_and_unload; boolean_t force_unmount; boolean_t recursive_homes; + boolean_t mount_recursively; } zfs_key_config_t; static int @@ -481,6 +422,7 @@ zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, config->unmount_and_unload = B_TRUE; config->force_unmount = B_FALSE; config->recursive_homes = B_FALSE; + config->mount_recursively = B_FALSE; config->dsname = NULL; config->homedir = NULL; for (int c = 0; c < argc; c++) { @@ -500,6 +442,8 @@ zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, config->force_unmount = B_TRUE; } else if (strcmp(argv[c], "recursive_homes") == 0) { config->recursive_homes = B_TRUE; + } else if (strcmp(argv[c], "mount_recursively") == 0) { + config->mount_recursively = B_TRUE; } else if (strcmp(argv[c], "prop_mountpoint") == 0) { if (config->homedir == NULL) config->homedir = strdup(entry->pw_dir); @@ -508,6 +452,217 @@ zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, return (PAM_SUCCESS); } +typedef struct { + pam_handle_t *pamh; + zfs_key_config_t *target; +} mount_umount_dataset_data_t; + +static int +mount_dataset(zfs_handle_t *zhp, void *data) +{ + mount_umount_dataset_data_t *mount_umount_dataset_data = data; + + zfs_key_config_t *target = mount_umount_dataset_data->target; + pam_handle_t *pamh = mount_umount_dataset_data->pamh; + + /* Refresh properties to get the latest key status */ + zfs_refresh_properties(zhp); + + int ret = 0; + + /* Check if dataset type is filesystem */ + if (zhp->zfs_type != ZFS_TYPE_FILESYSTEM) { + pam_syslog(pamh, LOG_DEBUG, + "dataset is not filesystem: %s, skipping.", + zfs_get_name(zhp)); + return (0); + } + + /* Check if encryption key is available */ + if (zfs_prop_get_int(zhp, ZFS_PROP_KEYSTATUS) == + ZFS_KEYSTATUS_UNAVAILABLE) { + pam_syslog(pamh, LOG_WARNING, + "key unavailable for: %s, skipping", + zfs_get_name(zhp)); + return (0); + } + + /* Check if prop canmount is on */ + if (zfs_prop_get_int(zhp, ZFS_PROP_CANMOUNT) != ZFS_CANMOUNT_ON) { + pam_syslog(pamh, LOG_INFO, + "canmount is not on for: %s, skipping", + zfs_get_name(zhp)); + return (0); + } + + /* Get mountpoint prop for check */ + char mountpoint[ZFS_MAXPROPLEN]; + if ((ret = zfs_prop_get(zhp, ZFS_PROP_MOUNTPOINT, mountpoint, + sizeof (mountpoint), NULL, NULL, 0, 1)) != 0) { + pam_syslog(pamh, LOG_ERR, + "failed to get mountpoint prop: %d", ret); + return (-1); + } + + /* Check if mountpoint isn't none or legacy */ + if (strcmp(mountpoint, ZFS_MOUNTPOINT_NONE) == 0 || + strcmp(mountpoint, ZFS_MOUNTPOINT_LEGACY) == 0) { + pam_syslog(pamh, LOG_INFO, + "mountpoint is none or legacy for: %s, skipping", + zfs_get_name(zhp)); + return (0); + } + + /* Don't mount the dataset if already mounted */ + if (zfs_is_mounted(zhp, NULL)) { + pam_syslog(pamh, LOG_INFO, "already mounted: %s", + zfs_get_name(zhp)); + return (0); + } + + /* Mount the dataset */ + ret = zfs_mount(zhp, NULL, 0); + if (ret) { + pam_syslog(pamh, LOG_ERR, + "zfs_mount failed for %s with: %d", zfs_get_name(zhp), + ret); + return (ret); + } + + /* Recursively mount children if the recursive flag is set */ + if (target->mount_recursively) { + ret = zfs_iter_filesystems_v2(zhp, 0, mount_dataset, data); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, + "child iteration failed: %d", ret); + return (-1); + } + } + + return (ret); +} + +static int +umount_dataset(zfs_handle_t *zhp, void *data) +{ + mount_umount_dataset_data_t *mount_umount_dataset_data = data; + + zfs_key_config_t *target = mount_umount_dataset_data->target; + pam_handle_t *pamh = mount_umount_dataset_data->pamh; + + int ret = 0; + /* Recursively umount children if the recursive flag is set */ + if (target->mount_recursively) { + ret = zfs_iter_filesystems_v2(zhp, 0, umount_dataset, data); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, + "child iteration failed: %d", ret); + return (-1); + } + } + + /* Check if dataset type is filesystem */ + if (zhp->zfs_type != ZFS_TYPE_FILESYSTEM) { + pam_syslog(pamh, LOG_DEBUG, + "dataset is not filesystem: %s, skipping", + zfs_get_name(zhp)); + return (0); + } + + /* Don't umount the dataset if already unmounted */ + if (zfs_is_mounted(zhp, NULL) == 0) { + pam_syslog(pamh, LOG_INFO, "already unmounted: %s", + zfs_get_name(zhp)); + return (0); + } + + /* Unmount the dataset */ + ret = zfs_unmount(zhp, NULL, target->force_unmount ? MS_FORCE : 0); + if (ret) { + pam_syslog(pamh, LOG_ERR, + "zfs_unmount failed for %s with: %d", zfs_get_name(zhp), + ret); + return (ret); + } + + return (ret); +} + +static int +decrypt_mount(pam_handle_t *pamh, zfs_key_config_t *config, const char *ds_name, + const char *passphrase, boolean_t noop) +{ + zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + pw_password_t *key = prepare_passphrase(pamh, ds, passphrase, NULL); + if (key == NULL) { + zfs_close(ds); + return (-1); + } + int ret = lzc_load_key(ds_name, noop, (uint8_t *)key->value, + WRAPPING_KEY_LEN); + pw_free(key); + if (ret && ret != EEXIST) { + pam_syslog(pamh, LOG_ERR, "load_key failed: %d", ret); + zfs_close(ds); + return (-1); + } + + if (noop) { + zfs_close(ds); + return (0); + } + + mount_umount_dataset_data_t data; + data.pamh = pamh; + data.target = config; + + ret = mount_dataset(ds, &data); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "mount failed: %d", ret); + zfs_close(ds); + return (-1); + } + + zfs_close(ds); + return (0); +} + +static int +unmount_unload(pam_handle_t *pamh, const char *ds_name, + zfs_key_config_t *target) +{ + zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + + mount_umount_dataset_data_t data; + data.pamh = pamh; + data.target = target; + + int ret = umount_dataset(ds, &data); + if (ret) { + pam_syslog(pamh, LOG_ERR, + "unmount_dataset failed with: %d", ret); + zfs_close(ds); + return (-1); + } + + ret = lzc_unload_key(ds_name); + if (ret) { + pam_syslog(pamh, LOG_ERR, "unload_key failed with: %d", ret); + zfs_close(ds); + return (-1); + } + zfs_close(ds); + return (0); +} + static void zfs_key_config_free(zfs_key_config_t *config) { @@ -548,7 +703,7 @@ find_dsname_by_prop_value(zfs_handle_t *zhp, void *data) } static char * -zfs_key_config_get_dataset(zfs_key_config_t *config) +zfs_key_config_get_dataset(pam_handle_t *pamh, zfs_key_config_t *config) { if (config->homedir != NULL && config->homes_prefix != NULL) { @@ -559,7 +714,7 @@ zfs_key_config_get_dataset(zfs_key_config_t *config) zfs_handle_t *zhp = zfs_open(g_zfs, config->homes_prefix, ZFS_TYPE_FILESYSTEM); if (zhp == NULL) { - pam_syslog(NULL, LOG_ERR, + pam_syslog(pamh, LOG_ERR, "dataset %s not found", config->homes_prefix); return (NULL); @@ -697,13 +852,13 @@ pam_sm_authenticate(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - if (decrypt_mount(pamh, dataset, token->value, B_TRUE) == -1) { + if (decrypt_mount(pamh, &config, dataset, token->value, B_TRUE) == -1) { free(dataset); pam_zfs_free(); zfs_key_config_free(&config); @@ -749,7 +904,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); @@ -763,7 +918,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - if (decrypt_mount(pamh, dataset, + if (decrypt_mount(pamh, &config, dataset, old_token->value, B_TRUE) == -1) { pam_syslog(pamh, LOG_ERR, "old token mismatch"); @@ -784,7 +939,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, pw_clear(pamh, OLD_PASSWORD_VAR_NAME); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); @@ -793,7 +948,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, return (PAM_SERVICE_ERR); } int was_loaded = is_key_loaded(pamh, dataset); - if (!was_loaded && decrypt_mount(pamh, dataset, + if (!was_loaded && decrypt_mount(pamh, &config, dataset, old_token->value, B_FALSE) == -1) { free(dataset); pam_zfs_free(); @@ -804,7 +959,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, } int changed = change_key(pamh, dataset, token->value); if (!was_loaded) { - unmount_unload(pamh, dataset, config.force_unmount); + unmount_unload(pamh, dataset, &config); } free(dataset); pam_zfs_free(); @@ -856,13 +1011,14 @@ pam_sm_open_session(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - if (decrypt_mount(pamh, dataset, token->value, B_FALSE) == -1) { + if (decrypt_mount(pamh, &config, dataset, + token->value, B_FALSE) == -1) { free(dataset); pam_zfs_free(); zfs_key_config_free(&config); @@ -910,13 +1066,13 @@ pam_sm_close_session(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); return (PAM_SESSION_ERR); } - if (unmount_unload(pamh, dataset, config.force_unmount) == -1) { + if (unmount_unload(pamh, dataset, &config) == -1) { free(dataset); pam_zfs_free(); zfs_key_config_free(&config); diff --git a/tests/runfiles/freebsd.run b/tests/runfiles/freebsd.run index e1ae0c6b7721..943c8eab2715 100644 --- a/tests/runfiles/freebsd.run +++ b/tests/runfiles/freebsd.run @@ -27,8 +27,8 @@ tests = ['zfs_jail_001_pos'] tags = ['functional', 'cli_root', 'zfs_jail'] [tests/functional/pam:FreeBSD] -tests = ['pam_basic', 'pam_change_unmounted', 'pam_nounmount', 'pam_recursive', - 'pam_short_password'] +tests = ['pam_basic', 'pam_change_unmounted', 'pam_mount_recursively', + 'pam_nounmount', 'pam_recursive', 'pam_short_password'] tags = ['functional', 'pam'] [tests/functional/direct:FreeBSD] diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index e55ec583d2cc..9cb6d9fdb410 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -169,8 +169,8 @@ tests = ['umount_unlinked_drain'] tags = ['functional', 'mount'] [tests/functional/pam:Linux] -tests = ['pam_basic', 'pam_change_unmounted', 'pam_nounmount', 'pam_recursive', - 'pam_short_password'] +tests = ['pam_basic', 'pam_change_unmounted', 'pam_mount_recursively', + 'pam_nounmount', 'pam_recursive', 'pam_short_password'] tags = ['functional', 'pam'] [tests/functional/procfs:Linux] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index df183825dc68..13f57d965395 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -1694,6 +1694,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/pam/cleanup.ksh \ functional/pam/pam_basic.ksh \ functional/pam/pam_change_unmounted.ksh \ + functional/pam/pam_mount_recursively.ksh \ functional/pam/pam_nounmount.ksh \ functional/pam/pam_recursive.ksh \ functional/pam/pam_short_password.ksh \ diff --git a/tests/zfs-tests/tests/functional/pam/cleanup.ksh b/tests/zfs-tests/tests/functional/pam/cleanup.ksh index bfb98cd30707..5bb6e518edb0 100755 --- a/tests/zfs-tests/tests/functional/pam/cleanup.ksh +++ b/tests/zfs-tests/tests/functional/pam/cleanup.ksh @@ -26,5 +26,6 @@ rmconfig destroy_pool $TESTPOOL del_user ${username} del_user ${username}rec +del_user ${username}mrec del_group pamtestgroup log_must rm -rf "$runstatedir" diff --git a/tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh b/tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh new file mode 100755 index 000000000000..be9604cfcdff --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh @@ -0,0 +1,90 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/tests/functional/pam/utilities.kshlib + +if [ -n "$ASAN_OPTIONS" ]; then + export LD_PRELOAD=$(ldd "$(command -v zfs)" | awk '/libasan\.so/ {print $3}') +fi + +username="${username}mrec" + +# Set up a deeper hierarchy, a mountpoint that doesn't interfere with other tests, +# and a user which references that mountpoint +log_must zfs create "$TESTPOOL/mrec" +log_must zfs create -o mountpoint="$TESTDIR/mrec" "$TESTPOOL/mrec/pam" +echo "recurpass" | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase \ + -o keylocation=prompt "$TESTPOOL/mrec/pam/${username}" +log_must zfs create "$TESTPOOL/mrec/pam/${username}/deep" +log_must zfs create "$TESTPOOL/mrec/pam/${username}/deep/deeper" +log_must zfs create -o mountpoint=none "$TESTPOOL/mrec/pam/${username}/deep/none" +log_must zfs create -o canmount=noauto "$TESTPOOL/mrec/pam/${username}/deep/noauto" +log_must zfs create -o canmount=off "$TESTPOOL/mrec/pam/${username}/deep/off" +log_must zfs unmount "$TESTPOOL/mrec/pam/${username}" +log_must zfs unload-key "$TESTPOOL/mrec/pam/${username}" +log_must add_user pamtestgroup ${username} "$TESTDIR/mrec" + +function keystatus { + log_must [ "$(get_prop keystatus "$TESTPOOL/mrec/pam/${username}")" = "$1" ] +} + +log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}" +keystatus unavailable + +function test_session { + echo "recurpass" | pamtester ${pamservice} ${username} open_session + references 1 + log_must ismounted "$TESTPOOL/mrec/pam/${username}" + log_must ismounted "$TESTPOOL/mrec/pam/${username}/deep" + log_must ismounted "$TESTPOOL/mrec/pam/${username}/deep/deeper" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/none" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/noauto" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/off" + keystatus available + + log_must pamtester ${pamservice} ${username} close_session + references 0 + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/deeper" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/none" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/noauto" + log_mustnot ismounted "$TESTPOOL/mrec/pam/${username}/deep/off" + keystatus unavailable +} + +genconfig "homes=$TESTPOOL/mrec/pam mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=$TESTPOOL/mrec/pam prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=$TESTPOOL/mrec recursive_homes prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=$TESTPOOL recursive_homes prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=* recursive_homes prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +log_pass "done."