Add recursive dataset mounting and unmounting support to pam_zfs_key (#16857)

Introduced functionality to recursively mount datasets with a new
config option `mount_recursively`. Adjusted existing functions to
handle the recursive behavior and added tests to validate the feature.
This enhances support for managing hierarchical ZFS datasets within
a PAM context.

Signed-off-by: Jerzy Kołosowski <jerzy@kolosowscy.pl>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Tony Hutter <hutter2@llnl.gov>
This commit is contained in:
Jerzy Kołosowski 2025-01-31 19:00:59 +00:00 committed by GitHub
parent 1e32c57893
commit 387ed5ca41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 326 additions and 78 deletions

View File

@ -63,6 +63,7 @@ pam_syslog(pam_handle_t *pamh, int loglevel, const char *fmt, ...)
#include <sys/file.h>
#include <sys/wait.h>
#include <pwd.h>
#include <lib/libzfs/libzfs_impl.h>
#include <sys/mman.h>
@ -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);

View File

@ -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]

View File

@ -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]

View File

@ -1696,6 +1696,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 \

View File

@ -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"

View File

@ -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."