From f203fedde8fa84658855e57397ddc854b0e46b55 Mon Sep 17 00:00:00 2001 From: "li-nk.social" Date: Fri, 3 Apr 2026 10:38:26 -0700 Subject: [PATCH] Add zoned_uid property with additive least privilege authorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements zoned_uid - a ZFS property that delegates dataset visibility and administration to user namespaces owned by a specific UID, enabling rootless Podman/Docker with native ZFS storage. Usage: zfs set zoned_uid=1000 pool/dataset Problem solved: - zfs zone requires an existing namespace PID - Podman creates a new namespace on each container start - Solution: delegate to UID, any namespace owned by that UID is authorized Authorization model — three-layer additive (all must pass): L0 (auth): Namespace owner UID matches zoned_uid property L1 (dsl_deleg): Per-operation grants via `zfs allow` (when pool delegation is ON — the default) L2 (cap tier): Linux capability in the namespace determines the operation class permitted While CAP_SYS_ADMIN is a namespaced capability (the namespace owner always holds it within their own user namespace), granting blanket access based solely on its presence is contrary to the Principle of Least Privilege. This change introduces tiered capability requirements so that non-destructive operations (create, snapshot, set property) require only CAP_FOWNER, while destructive operations (destroy, rename, clone) continue to require CAP_SYS_ADMIN — both of which are namespaced capabilities scoped to the user namespace, not the init namespace. When pool delegation is OFF (non-default), all zoned_uid write operations are denied — delegation OFF means the pool admin has opted out of delegating access entirely. Security model: - Namespace owner UID must match zoned_uid value - Delegation root cannot be destroyed or escaped via rename - Namespace users cannot modify zoned_uid itself (only global zone admin can manage delegation assignments) - Namespace users cannot modify the 'zoned' property - Namespace users cannot override filesystem_limit or snapshot_limit set by the global admin on the delegation root (but can impose tighter sub-limits on child datasets) - Multi-UID isolation: sibling delegations with different UIDs cannot access each other's subtrees Kernel changes: - zone_dataset_attach_uid()/detach_uid() in SPL - zone_dataset_admin_check() for write authorization with tiered capabilities (CAP_FOWNER for non-destructive, CAP_SYS_ADMIN for destructive) - Callback registration for zoned_uid property lookup - New zfs_secpolicy_zoned_uid_deleg() helper that calls dsl_deleg_access_impl() directly, bypassing zfs_dozonecheck_ds() which requires the `zoned` property that zoned_uid datasets lack - Fix dsl_deleg_access_impl() hierarchy walk to accept zoned_uid datasets (not just zoned=on) - Update all 9 secpolicy call sites to require dsl_deleg grants instead of short-circuiting on ZONE_ADMIN_ALLOWED - Security policy hooks in zfs_secpolicy_*() functions - Fixed inglobalzone() to use current_user_ns() - zfs_prop_set_special() handles attach/detach as property side-effects, eliminating the need for dedicated ioctls - spa_import_os() restores zoned_uid delegations kernel-side on pool import via dmu_objset_find() walk - spa_export_os() detaches zoned_uid delegations on pool destroy/export, preventing stale kernel state on recreate - zoned_uid registered as PROP_INHERIT so child datasets inherit the delegation, enabling sub-dataset creation - zfs_get_zoned_uid() uses dsl_prop_get setpoint to identify the true delegation root, correctly distinguishing inherited values from locally-set ones for destroy/rename policy checks - zone_dataset_check_list() accepts '@' and '#' separators in addition to '/' so snapshots and bookmarks are visible from delegated namespaces - zfs_secpolicy_setprop() blocks ZFS_PROP_ZONED_UID from being set within a delegated namespace, preventing self-revocation - zfs_secpolicy_setprop() blocks filesystem_limit and snapshot_limit changes on the delegation root from within a namespace (uses dsl_prop_get setpoint to identify the root), while allowing delegated users to set tighter sub-limits on child datasets - Use kcred (not CRED()) for zone_dataset_detach_uid/attach_uid in destroy and rename cleanup paths, preventing stale tracking entries when namespace users perform these operations - Use cr parameter (not CRED()) in all secpolicy zoned_uid delegation checks for correct credential propagation Userspace changes: - check_parents() defers to kernel when zoned_uid set FreeBSD compatibility: - include/os/freebsd/spl/sys/zone.h — Added FreeBSD stubs: - zone_uid_op_t enum (ZONE_OP_CREATE, SNAPSHOT, CLONE, DESTROY, RENAME, SETPROP) - zone_admin_result_t enum (NOT_APPLICABLE, ALLOWED, DENIED) - zone_dataset_admin_check() — static inline, always returns ZONE_ADMIN_NOT_APPLICABLE - zone_dataset_attach_uid() — static inline, returns ENXIO - zone_dataset_detach_uid() — static inline, returns ENXIO - zone_get_zoned_uid_fn_t callback typedef - zone_register_zoned_uid_callback() — static inline no-op - zone_unregister_zoned_uid_callback() — static inline no-op - On FreeBSD, every zone_dataset_admin_check() call returns ZONE_ADMIN_NOT_APPLICABLE, causing all security policy functions to fall through to existing jail-based permission checks - Setting zoned_uid on FreeBSD returns ENXIO since user namespace delegation requires Linux user namespaces Test changes: - Add grant_deleg() calls to tests 006-022 for operations that now require explicit dsl_deleg grants - Add tests 023-030 validating the capability tier model - Add test 031 validating stale zone tracking cleanup after namespace rename+destroy - Fix capsh lookup in test helpers for ksh -p restricted PATH (command -v + explicit /usr/sbin fallback) - Add mountpoint=none to tests 023-026 to avoid mount-lock issues in user namespaces - Fix test 026 expectations to match kernel behavior (delegation OFF denies all writes, allows read-only) - run_in_userns helper resolves absolute zfs path to handle environments where PATH does not include zfs (source builds) - Test 004 updated: zoned_uid now inherits (PROP_INHERIT), test verifies inheritance and override behavior - Test 013 uses within_percent with parseable byte output (-Hp) for robust quota value comparison across environments - Test 014: verifies grandchild dataset creation from user namespace, confirming inherited zoned_uid delegation works - Test 015: pool destroy/recreate with zoned_uid delegation - Test 016: individual snapshot destroy from namespace - Test 017: namespace user cannot modify zoned_uid property - Test 018: clone operations from within delegated namespace - Test 019: multi-UID isolation between sibling delegations - Test 020: operations without zone_dataset_admin_check() integration are denied via zfs_dozonecheck_impl() - Test 021: 'zoned' property cannot be modified from namespace - Test 022: delegation root limit overrides blocked from namespace - Quoted shell variables across all test scripts for robustness - Shellcheck SC2155 fixes across all test scripts Reviewed-by: Tony Hutter Reviewed-by: Brian Behlendorf Signed-off-by: Colin K. Williams / LINK ORG LLC / li-nk.social Closes #18167 --- contrib/pyzfs/libzfs_core/_constants.py | 4 + include/libzfs.h | 1 + include/os/freebsd/spl/sys/zone.h | 72 +++ include/os/linux/spl/sys/zone.h | 58 +++ include/sys/fs/zfs.h | 2 + lib/libzfs/libzfs.abi | 3 +- lib/libzfs/libzfs_dataset.c | 10 +- lib/libzfs/libzfs_util.c | 6 + man/man7/zfsprops.7 | 92 ++++ man/man8/zfs-zone.8 | 15 +- module/os/linux/spl/spl-zone.c | 413 ++++++++++++++++-- module/os/linux/zfs/spa_misc_os.c | 50 ++- module/os/linux/zfs/zfs_ioctl_os.c | 4 + module/zcommon/zfs_prop.c | 4 + module/zfs/dsl_deleg.c | 13 +- module/zfs/zfs_ioctl.c | 289 +++++++++++- tests/runfiles/common.run | 16 + tests/test-runner/bin/zts-report.py.in | 2 + tests/zfs-tests/include/commands.cfg | 1 + tests/zfs-tests/tests/Makefile.am | 35 ++ .../tests/functional/zoned_uid/cleanup.ksh | 46 ++ .../tests/functional/zoned_uid/setup.ksh | 99 +++++ .../tests/functional/zoned_uid/zoned_uid.cfg | 33 ++ .../zoned_uid/zoned_uid_001_pos.ksh | 85 ++++ .../zoned_uid/zoned_uid_002_pos.ksh | 83 ++++ .../zoned_uid/zoned_uid_003_pos.ksh | 100 +++++ .../zoned_uid/zoned_uid_004_pos.ksh | 91 ++++ .../zoned_uid/zoned_uid_005_neg.ksh | 72 +++ .../zoned_uid/zoned_uid_006_pos.ksh | 109 +++++ .../zoned_uid/zoned_uid_007_pos.ksh | 110 +++++ .../zoned_uid/zoned_uid_008_pos.ksh | 128 ++++++ .../zoned_uid/zoned_uid_009_pos.ksh | 149 +++++++ .../zoned_uid/zoned_uid_010_pos.ksh | 157 +++++++ .../zoned_uid/zoned_uid_011_neg.ksh | 153 +++++++ .../zoned_uid/zoned_uid_012_pos.ksh | 120 +++++ .../zoned_uid/zoned_uid_013_pos.ksh | 122 ++++++ .../zoned_uid/zoned_uid_014_pos.ksh | 116 +++++ .../zoned_uid/zoned_uid_015_pos.ksh | 114 +++++ .../zoned_uid/zoned_uid_016_pos.ksh | 132 ++++++ .../zoned_uid/zoned_uid_017_neg.ksh | 125 ++++++ .../zoned_uid/zoned_uid_018_pos.ksh | 129 ++++++ .../zoned_uid/zoned_uid_019_neg.ksh | 141 ++++++ .../zoned_uid/zoned_uid_020_neg.ksh | 171 ++++++++ .../zoned_uid/zoned_uid_021_neg.ksh | 109 +++++ .../zoned_uid/zoned_uid_022_neg.ksh | 154 +++++++ .../zoned_uid/zoned_uid_023_pos.ksh | 131 ++++++ .../zoned_uid/zoned_uid_024_neg.ksh | 144 ++++++ .../zoned_uid/zoned_uid_025_pos.ksh | 102 +++++ .../zoned_uid/zoned_uid_026_pos.ksh | 112 +++++ .../zoned_uid/zoned_uid_027_pos.ksh | 103 +++++ .../zoned_uid/zoned_uid_028_neg.ksh | 103 +++++ .../zoned_uid/zoned_uid_029_neg.ksh | 120 +++++ .../zoned_uid/zoned_uid_030_pos.ksh | 183 ++++++++ .../zoned_uid/zoned_uid_031_pos.ksh | 110 +++++ .../zoned_uid/zoned_uid_common.kshlib | 237 ++++++++++ 55 files changed, 5238 insertions(+), 45 deletions(-) create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/cleanup.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/setup.ksh create mode 100644 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid.cfg create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_001_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_002_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_003_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_004_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_005_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_006_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_007_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_008_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_009_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_010_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_011_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_012_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_013_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_014_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_015_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_016_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_017_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_018_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_019_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_020_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_021_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_022_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_023_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_024_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_025_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_026_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_027_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_028_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_029_neg.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_030_pos.ksh create mode 100755 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_031_pos.ksh create mode 100644 tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_common.kshlib diff --git a/contrib/pyzfs/libzfs_core/_constants.py b/contrib/pyzfs/libzfs_core/_constants.py index 95c9a673828..4d52502bd21 100644 --- a/contrib/pyzfs/libzfs_core/_constants.py +++ b/contrib/pyzfs/libzfs_core/_constants.py @@ -105,6 +105,10 @@ def enum(*sequential, **named): 'ZFS_ERR_RESUME_EXISTS', 'ZFS_ERR_CRYPTO_NOTSUP', 'ZFS_ERR_RAIDZ_EXPAND_IN_PROGRESS', + 'ZFS_ERR_ASHIFT_MISMATCH', + 'ZFS_ERR_STREAM_LARGE_MICROZAP', + 'ZFS_ERR_TOO_MANY_SITOUTS', + 'ZFS_ERR_NO_USER_NS_SUPPORT', ], {} ) diff --git a/include/libzfs.h b/include/libzfs.h index a9a31c90ae7..ee0e46761fc 100644 --- a/include/libzfs.h +++ b/include/libzfs.h @@ -160,6 +160,7 @@ typedef enum zfs_error { EZFS_SHAREFAILED, /* filesystem share failed */ EZFS_RAIDZ_EXPAND_IN_PROGRESS, /* a raidz is currently expanding */ EZFS_ASHIFT_MISMATCH, /* can't add vdevs with different ashifts */ + EZFS_NO_USER_NS_SUPPORT, /* kernel built without CONFIG_USER_NS */ EZFS_UNKNOWN } zfs_error_t; diff --git a/include/os/freebsd/spl/sys/zone.h b/include/os/freebsd/spl/sys/zone.h index cfe63946706..56f762d4061 100644 --- a/include/os/freebsd/spl/sys/zone.h +++ b/include/os/freebsd/spl/sys/zone.h @@ -65,4 +65,76 @@ extern int zone_dataset_visible(const char *, int *); */ extern uint32_t zone_get_hostid(void *); +/* + * Operations that can be authorized via zoned_uid delegation. + * Shared with Linux; on FreeBSD these are defined but the check + * always returns NOT_APPLICABLE (no user namespace support). + */ +typedef enum zone_uid_op { + ZONE_OP_CREATE, + ZONE_OP_SNAPSHOT, + ZONE_OP_CLONE, + ZONE_OP_DESTROY, + ZONE_OP_RENAME, + ZONE_OP_SETPROP +} zone_uid_op_t; + +typedef enum zone_admin_result { + ZONE_ADMIN_NOT_APPLICABLE, + ZONE_ADMIN_ALLOWED, + ZONE_ADMIN_DENIED +} zone_admin_result_t; + +/* + * FreeBSD stub: zoned_uid delegation is not applicable (no user namespaces). + * Always returns NOT_APPLICABLE so callers fall through to existing + * jail-based permission checks. + */ +static inline zone_admin_result_t +zone_dataset_admin_check(const char *dataset, zone_uid_op_t op, + const char *aux_dataset) +{ + (void) dataset, (void) op, (void) aux_dataset; + return (ZONE_ADMIN_NOT_APPLICABLE); +} + +/* + * Callback type for looking up zoned_uid property. + */ +typedef uid_t (*zone_get_zoned_uid_fn_t)(const char *dataset, + char *root_out, size_t root_size); + +/* + * FreeBSD stubs: zoned_uid attach/detach require user namespaces + * which FreeBSD does not have. Return ENXIO (consistent with the + * Linux fallback when CONFIG_USER_NS is not defined). + */ +static inline int +zone_dataset_attach_uid(struct ucred *cred, const char *dataset, uid_t uid) +{ + (void) cred, (void) dataset, (void) uid; + return (ENXIO); +} + +static inline int +zone_dataset_detach_uid(struct ucred *cred, const char *dataset, uid_t uid) +{ + (void) cred, (void) dataset, (void) uid; + return (ENXIO); +} + +/* + * FreeBSD stubs: no-op since zoned_uid delegation requires user namespaces. + */ +static inline void +zone_register_zoned_uid_callback(zone_get_zoned_uid_fn_t fn) +{ + (void) fn; +} + +static inline void +zone_unregister_zoned_uid_callback(void) +{ +} + #endif /* !_OPENSOLARIS_SYS_ZONE_H_ */ diff --git a/include/os/linux/spl/sys/zone.h b/include/os/linux/spl/sys/zone.h index 4e75202fbdd..2933c5a5c63 100644 --- a/include/os/linux/spl/sys/zone.h +++ b/include/os/linux/spl/sys/zone.h @@ -41,11 +41,69 @@ extern int zone_dataset_attach(cred_t *, const char *, int); */ extern int zone_dataset_detach(cred_t *, const char *, int); +/* + * Attach the given dataset to all user namespaces owned by the given UID. + */ +extern int zone_dataset_attach_uid(cred_t *, const char *, uid_t); + +/* + * Detach the given dataset from UID-based zoning. + */ +extern int zone_dataset_detach_uid(cred_t *, const char *, uid_t); + /* * Returns true if the named pool/dataset is visible in the current zone. */ extern int zone_dataset_visible(const char *dataset, int *write); +/* + * Operations that can be authorized via zoned_uid delegation. + * Used by zone_dataset_admin_check() to apply operation-specific constraints. + */ +typedef enum zone_uid_op { + ZONE_OP_CREATE, /* Create child dataset */ + ZONE_OP_SNAPSHOT, /* Create snapshot */ + ZONE_OP_CLONE, /* Clone from snapshot */ + ZONE_OP_DESTROY, /* Destroy dataset/snapshot */ + ZONE_OP_RENAME, /* Rename (both src and dst checked) */ + ZONE_OP_SETPROP /* Set properties */ +} zone_uid_op_t; + +/* + * Result of admin authorization check for zoned_uid delegation. + */ +typedef enum zone_admin_result { + ZONE_ADMIN_NOT_APPLICABLE, /* In global zone, use normal checks */ + ZONE_ADMIN_ALLOWED, /* Authorized via zoned_uid */ + ZONE_ADMIN_DENIED /* In user ns but not authorized */ +} zone_admin_result_t; + +/* + * Check if a dataset operation is authorized via zoned_uid delegation. + * For ZONE_OP_RENAME and ZONE_OP_CLONE, aux_dataset provides the + * second dataset (destination for rename, origin for clone). + * Returns ZONE_ADMIN_ALLOWED if authorized, ZONE_ADMIN_DENIED if in a + * user namespace but not authorized, or ZONE_ADMIN_NOT_APPLICABLE if + * in the global zone (caller should use normal permission checks). + */ +extern zone_admin_result_t zone_dataset_admin_check(const char *dataset, + zone_uid_op_t op, const char *aux_dataset); + +/* + * Callback type for looking up zoned_uid property. + * Returns the zoned_uid value if found, 0 if not set or on error. + * If root_out is non-NULL, copies the delegation root dataset name. + */ +typedef uid_t (*zone_get_zoned_uid_fn_t)(const char *dataset, + char *root_out, size_t root_size); + +/* + * Register/unregister the zoned_uid property lookup callback. + * Called by ZFS module during init/fini. + */ +extern void zone_register_zoned_uid_callback(zone_get_zoned_uid_fn_t fn); +extern void zone_unregister_zoned_uid_callback(void); + int spl_zone_init(void); void spl_zone_fini(void); diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h index ba79a674e73..1ddf3ba9ba1 100644 --- a/include/sys/fs/zfs.h +++ b/include/sys/fs/zfs.h @@ -204,6 +204,7 @@ typedef enum { ZFS_PROP_DEFAULTGROUPOBJQUOTA, ZFS_PROP_DEFAULTPROJECTOBJQUOTA, ZFS_PROP_SNAPSHOTS_CHANGED_NSECS, + ZFS_PROP_ZONED_UID, ZFS_NUM_PROPS } zfs_prop_t; @@ -1782,6 +1783,7 @@ typedef enum { ZFS_ERR_ASHIFT_MISMATCH, ZFS_ERR_STREAM_LARGE_MICROZAP, ZFS_ERR_TOO_MANY_SITOUTS, + ZFS_ERR_NO_USER_NS_SUPPORT, } zfs_errno_t; /* diff --git a/lib/libzfs/libzfs.abi b/lib/libzfs/libzfs.abi index bed2c7979a1..c69f9af5607 100644 --- a/lib/libzfs/libzfs.abi +++ b/lib/libzfs/libzfs.abi @@ -2292,7 +2292,8 @@ - + + diff --git a/lib/libzfs/libzfs_dataset.c b/lib/libzfs/libzfs_dataset.c index bf276a3aa91..e5a7ca9ba3f 100644 --- a/lib/libzfs/libzfs_dataset.c +++ b/lib/libzfs/libzfs_dataset.c @@ -3347,9 +3347,13 @@ check_parents(libzfs_handle_t *hdl, const char *path, uint64_t *zoned, /* we are in a non-global zone, but parent is in the global zone */ if (getzoneid() != GLOBAL_ZONEID && !is_zoned) { - (void) zfs_standard_error(hdl, EPERM, errbuf); - zfs_close(zhp); - return (-1); + uint64_t zoned_uid = zfs_prop_get_int(zhp, ZFS_PROP_ZONED_UID); + if (zoned_uid == 0) { + (void) zfs_standard_error(hdl, EPERM, errbuf); + zfs_close(zhp); + return (-1); + } + /* zoned_uid set - let kernel decide */ } /* make sure parent is a filesystem */ diff --git a/lib/libzfs/libzfs_util.c b/lib/libzfs/libzfs_util.c index 021a1d8a407..d886bdb9786 100644 --- a/lib/libzfs/libzfs_util.c +++ b/lib/libzfs/libzfs_util.c @@ -324,6 +324,9 @@ libzfs_error_description(libzfs_handle_t *hdl) case EZFS_ASHIFT_MISMATCH: return (dgettext(TEXT_DOMAIN, "adding devices with " "different physical sector sizes is not allowed")); + case EZFS_NO_USER_NS_SUPPORT: + return (dgettext(TEXT_DOMAIN, "kernel was built without " + "user namespace support (CONFIG_USER_NS)")); case EZFS_UNKNOWN: return (dgettext(TEXT_DOMAIN, "unknown error")); default: @@ -517,6 +520,9 @@ zfs_standard_error_fmt(libzfs_handle_t *hdl, int error, const char *fmt, ...) case ZFS_ERR_NOT_USER_NAMESPACE: zfs_verror(hdl, EZFS_NOT_USER_NAMESPACE, fmt, ap); break; + case ZFS_ERR_NO_USER_NS_SUPPORT: + zfs_verror(hdl, EZFS_NO_USER_NS_SUPPORT, fmt, ap); + break; default: zfs_error_aux(hdl, "%s", zfs_strerror(error)); zfs_verror(hdl, EZFS_UNKNOWN, fmt, ap); diff --git a/man/man7/zfsprops.7 b/man/man7/zfsprops.7 index 448a7ec05cc..183e6ea9574 100644 --- a/man/man7/zfsprops.7 +++ b/man/man7/zfsprops.7 @@ -2112,6 +2112,98 @@ for more information. Zoning is a Linux feature and this property is not available on other platforms. +.It Sy zoned_uid Ns = Ns Ar uid +Delegates dataset visibility and administration to all user namespaces +owned by the specified UID. +This property enables rootless container support with native ZFS storage. +For example, setting +.Sy zoned_uid Ns = Ns 1000 +allows user 1000's rootless Podman containers to use ZFS for storage layers. +This is a Linux-only feature. +.Pp +Authorization uses an additive three-layer model where all layers must pass: +.Bl -tag -width "L2 (capability tier)" -compact +.It Sy L0 (authentication) +The user namespace owner UID must match the +.Sy zoned_uid +value. +.It Sy L1 (dsl_deleg) +The pool administrator must grant per-operation permissions on the +delegation root using +.Xr zfs-allow 8 . +When pool delegation is OFF +.Pq Nm zpool Cm set Sy delegation Ns = Ns Sy off , +all write operations are denied regardless of capabilities. +.It Sy L2 (capability tier) +Linux capabilities within the user namespace determine the permitted +operation class: +.Sy CAP_FOWNER +for non-destructive operations +.Pq create, snapshot, set property , +.Sy CAP_SYS_ADMIN +for destructive operations +.Pq destroy, rename, clone . +Both are namespaced capabilities scoped to the user namespace, +not the init namespace. +.El +.Pp +Read-only operations +.Pq Nm zfs Cm list , Nm zfs Cm get +require no capabilities and no +.Nm zfs Cm allow +grants; visibility is controlled solely by the +.Sy zoned_uid +delegation scoping. +.Pp +Write operations that can be delegated include +.Nm zfs Cm create , +.Nm zfs Cm destroy , +.Nm zfs Cm snapshot , +.Nm zfs Cm clone , +.Nm zfs Cm rename +.Pq within the delegation subtree , +and +.Nm zfs Cm set . +.Pp +The delegation root dataset +.Pq where zoned_uid is locally set +cannot be destroyed from within the user namespace, protecting the +parent dataset from unauthorized removal. +Renames are also constrained to remain within the delegation subtree. +The namespace user cannot modify the +.Sy zoned_uid +or +.Sy zoned +properties, and cannot override +.Sy filesystem_limit +or +.Sy snapshot_limit +set by the administrator on the delegation root +.Pq but can impose tighter sub-limits on child datasets . +.Pp +Set to +.Sy 0 +.Pq or inherit +to disable UID-based delegation. +.Pp +Unlike +.Nm zfs Cm zone +which requires an existing namespace file, +.Sy zoned_uid +applies to any user namespace owned by the specified UID, +making it suitable for container runtimes that create new namespaces +on each invocation. +See +.Xr zfs-zone 8 +for namespace-specific delegation. +.Pp +Example setup for rootless Podman: +.Bd -literal -offset indent +# zfs create tank/containers +# zfs set zoned_uid=1000 tank/containers +# zfs set mountpoint=none tank/containers +# zfs allow -u 1000 create,destroy,mount,snapshot,rename,clone tank/containers +.Ed .El .Pp The following three properties cannot be changed after the file system is diff --git a/man/man8/zfs-zone.8 b/man/man8/zfs-zone.8 index a56a304e82b..d00b2e217a5 100644 --- a/man/man8/zfs-zone.8 +++ b/man/man8/zfs-zone.8 @@ -114,4 +114,17 @@ dataset to a user namespace identified by user namespace file .Dl # Nm zfs Cm zone Ar /proc/1234/ns/user Ar tank/users . .Sh SEE ALSO -.Xr zfsprops 7 +.Xr zfsprops 7 , +.Xr zfs-allow 8 +.Pp +For rootless container use cases where the namespace is ephemeral, +consider using the +.Sy zoned_uid +property instead, which delegates to all namespaces owned by a UID +rather than requiring attachment to a specific namespace file. +The +.Sy zoned_uid +property uses a three-layer additive authorization model +.Pq UID match, dsl_deleg grants, capability tiers +described in +.Xr zfsprops 7 . diff --git a/module/os/linux/spl/spl-zone.c b/module/os/linux/spl/spl-zone.c index b2eae5d00b1..5992957280e 100644 --- a/module/os/linux/spl/spl-zone.c +++ b/module/os/linux/spl/spl-zone.c @@ -59,6 +59,18 @@ typedef struct zone_dataset { char zd_dsname[]; /* name of the member dataset */ } zone_dataset_t; +/* + * UID-based dataset zoning: allows delegating datasets to all user + * namespaces owned by a specific UID, enabling rootless container support. + */ +typedef struct zone_uid_datasets { + struct list_head zuds_list; /* zone_uid_datasets linkage */ + kuid_t zuds_owner; /* owner UID */ + struct list_head zuds_datasets; /* datasets for this UID */ +} zone_uid_datasets_t; + +static struct list_head zone_uid_datasets; + #ifdef CONFIG_USER_NS /* @@ -138,6 +150,18 @@ zone_datasets_lookup(unsigned int nsinum) } #ifdef CONFIG_USER_NS +static zone_uid_datasets_t * +zone_uid_datasets_lookup(kuid_t owner) +{ + zone_uid_datasets_t *zuds; + + list_for_each_entry(zuds, &zone_uid_datasets, zuds_list) { + if (uid_eq(zuds->zuds_owner, owner)) + return (zuds); + } + return (NULL); +} + static struct zone_dataset * zone_dataset_lookup(zone_datasets_t *zds, const char *dataset, size_t dsnamelen) { @@ -231,6 +255,62 @@ zone_dataset_attach(cred_t *cred, const char *dataset, int userns_fd) } EXPORT_SYMBOL(zone_dataset_attach); +int +zone_dataset_attach_uid(cred_t *cred, const char *dataset, uid_t owner_uid) +{ +#ifdef CONFIG_USER_NS + zone_uid_datasets_t *zuds; + zone_dataset_t *zd; + int error; + size_t dsnamelen; + kuid_t kowner; + + /* Only root can attach datasets to UIDs */ + if ((error = zone_dataset_cred_check(cred)) != 0) + return (error); + if ((error = zone_dataset_name_check(dataset, &dsnamelen)) != 0) + return (error); + + kowner = make_kuid(current_user_ns(), owner_uid); + if (!uid_valid(kowner)) + return (EINVAL); + + mutex_enter(&zone_datasets_lock); + + /* Find or create UID entry */ + zuds = zone_uid_datasets_lookup(kowner); + if (zuds == NULL) { + zuds = kmem_alloc(sizeof (zone_uid_datasets_t), KM_SLEEP); + INIT_LIST_HEAD(&zuds->zuds_list); + INIT_LIST_HEAD(&zuds->zuds_datasets); + zuds->zuds_owner = kowner; + list_add_tail(&zuds->zuds_list, &zone_uid_datasets); + } else { + /* Check if dataset already attached */ + list_for_each_entry(zd, &zuds->zuds_datasets, zd_list) { + if (zd->zd_dsnamelen == dsnamelen && + strncmp(zd->zd_dsname, dataset, dsnamelen) == 0) { + mutex_exit(&zone_datasets_lock); + return (EEXIST); + } + } + } + + /* Add dataset to UID's list */ + zd = kmem_alloc(sizeof (zone_dataset_t) + dsnamelen + 1, KM_SLEEP); + zd->zd_dsnamelen = dsnamelen; + strlcpy(zd->zd_dsname, dataset, dsnamelen + 1); + INIT_LIST_HEAD(&zd->zd_list); + list_add_tail(&zd->zd_list, &zuds->zuds_datasets); + + mutex_exit(&zone_datasets_lock); + return (0); +#else + return (ENXIO); +#endif /* CONFIG_USER_NS */ +} +EXPORT_SYMBOL(zone_dataset_attach_uid); + int zone_dataset_detach(cred_t *cred, const char *dataset, int userns_fd) { @@ -280,6 +360,217 @@ zone_dataset_detach(cred_t *cred, const char *dataset, int userns_fd) } EXPORT_SYMBOL(zone_dataset_detach); +int +zone_dataset_detach_uid(cred_t *cred, const char *dataset, uid_t owner_uid) +{ +#ifdef CONFIG_USER_NS + zone_uid_datasets_t *zuds; + zone_dataset_t *zd; + int error; + size_t dsnamelen; + kuid_t kowner; + + if ((error = zone_dataset_cred_check(cred)) != 0) + return (error); + if ((error = zone_dataset_name_check(dataset, &dsnamelen)) != 0) + return (error); + + kowner = make_kuid(current_user_ns(), owner_uid); + if (!uid_valid(kowner)) + return (EINVAL); + + mutex_enter(&zone_datasets_lock); + + zuds = zone_uid_datasets_lookup(kowner); + if (zuds == NULL) { + mutex_exit(&zone_datasets_lock); + return (ENOENT); + } + + /* Find and remove dataset */ + list_for_each_entry(zd, &zuds->zuds_datasets, zd_list) { + if (zd->zd_dsnamelen == dsnamelen && + strncmp(zd->zd_dsname, dataset, dsnamelen) == 0) { + list_del(&zd->zd_list); + kmem_free(zd, sizeof (*zd) + zd->zd_dsnamelen + 1); + + /* Remove UID entry if no more datasets */ + if (list_empty(&zuds->zuds_datasets)) { + list_del(&zuds->zuds_list); + kmem_free(zuds, sizeof (*zuds)); + } + + mutex_exit(&zone_datasets_lock); + return (0); + } + } + + mutex_exit(&zone_datasets_lock); + return (ENOENT); +#else + return (ENXIO); +#endif /* CONFIG_USER_NS */ +} +EXPORT_SYMBOL(zone_dataset_detach_uid); + +/* + * Callback for looking up zoned_uid property (registered by ZFS module). + */ +static zone_get_zoned_uid_fn_t zone_get_zoned_uid_fn = NULL; + +void +zone_register_zoned_uid_callback(zone_get_zoned_uid_fn_t fn) +{ + zone_get_zoned_uid_fn = fn; +} +EXPORT_SYMBOL(zone_register_zoned_uid_callback); + +void +zone_unregister_zoned_uid_callback(void) +{ + zone_get_zoned_uid_fn = NULL; +} +EXPORT_SYMBOL(zone_unregister_zoned_uid_callback); + +#ifdef CONFIG_USER_NS +/* + * Check if a dataset is the delegation root (has zoned_uid set locally). + */ +static boolean_t +zone_dataset_is_zoned_uid_root(const char *dataset, uid_t zoned_uid) +{ + char *root; + uid_t found_uid; + boolean_t is_root; + + if (zone_get_zoned_uid_fn == NULL) + return (B_FALSE); + + root = kmem_alloc(MAXPATHLEN, KM_SLEEP); + found_uid = zone_get_zoned_uid_fn(dataset, root, MAXPATHLEN); + is_root = (found_uid == zoned_uid && strcmp(root, dataset) == 0); + kmem_free(root, MAXPATHLEN); + return (is_root); +} +#endif /* CONFIG_USER_NS */ + +/* + * Core authorization check for zoned_uid write delegation. + */ +zone_admin_result_t +zone_dataset_admin_check(const char *dataset, zone_uid_op_t op, + const char *aux_dataset) +{ +#ifdef CONFIG_USER_NS + struct user_namespace *user_ns; + char *delegation_root; + uid_t zoned_uid, ns_owner_uid; + int write_unused; + zone_admin_result_t result = ZONE_ADMIN_NOT_APPLICABLE; + + /* Step 1: If in global zone, not applicable */ + if (INGLOBALZONE(curproc)) + return (ZONE_ADMIN_NOT_APPLICABLE); + + /* Step 2: Need callback to be registered */ + if (zone_get_zoned_uid_fn == NULL) + return (ZONE_ADMIN_NOT_APPLICABLE); + + delegation_root = kmem_alloc(MAXPATHLEN, KM_SLEEP); + + /* Step 3: Find delegation root */ + zoned_uid = zone_get_zoned_uid_fn(dataset, delegation_root, + MAXPATHLEN); + if (zoned_uid == 0) + goto out; + + /* Step 4: Verify namespace owner matches */ + user_ns = current_user_ns(); + ns_owner_uid = from_kuid(&init_user_ns, user_ns->owner); + if (ns_owner_uid != zoned_uid) + goto out; + + /* Step 5: Tiered capability check based on operation class */ + { + int required_cap; + switch (op) { + case ZONE_OP_DESTROY: + case ZONE_OP_RENAME: + case ZONE_OP_CLONE: + required_cap = CAP_SYS_ADMIN; + break; + case ZONE_OP_CREATE: + case ZONE_OP_SNAPSHOT: + case ZONE_OP_SETPROP: + required_cap = CAP_FOWNER; + break; + default: + required_cap = CAP_SYS_ADMIN; + break; + } + if (!ns_capable(user_ns, required_cap)) { + result = ZONE_ADMIN_DENIED; + goto out; + } + } + + /* Step 6: Operation-specific constraints */ + switch (op) { + case ZONE_OP_DESTROY: + /* Cannot destroy the delegation root itself */ + if (zone_dataset_is_zoned_uid_root(dataset, zoned_uid)) { + result = ZONE_ADMIN_DENIED; + goto out; + } + break; + + case ZONE_OP_RENAME: + /* Cannot rename outside delegation subtree */ + if (aux_dataset != NULL) { + char *dst_root; + uid_t dst_uid; + + dst_root = kmem_alloc(MAXPATHLEN, KM_SLEEP); + dst_uid = zone_get_zoned_uid_fn(aux_dataset, + dst_root, MAXPATHLEN); + if (dst_uid != zoned_uid || + strcmp(dst_root, delegation_root) != 0) { + kmem_free(dst_root, MAXPATHLEN); + result = ZONE_ADMIN_DENIED; + goto out; + } + kmem_free(dst_root, MAXPATHLEN); + } + break; + + case ZONE_OP_CLONE: + /* Clone source must be visible */ + if (aux_dataset != NULL) { + if (!zone_dataset_visible(aux_dataset, &write_unused)) { + result = ZONE_ADMIN_DENIED; + goto out; + } + } + break; + + case ZONE_OP_CREATE: + case ZONE_OP_SNAPSHOT: + case ZONE_OP_SETPROP: + /* No additional constraints */ + break; + } + + result = ZONE_ADMIN_ALLOWED; +out: + kmem_free(delegation_root, MAXPATHLEN); + return (result); +#else + (void) dataset, (void) op, (void) aux_dataset; + return (ZONE_ADMIN_NOT_APPLICABLE); +#endif +} +EXPORT_SYMBOL(zone_dataset_admin_check); + /* * A dataset is visible if: * - It is a parent of a namespace entry. @@ -293,34 +584,19 @@ EXPORT_SYMBOL(zone_dataset_detach); * The parent datasets of namespace entries are visible and * read-only to provide a path back to the root of the pool. */ -int -zone_dataset_visible(const char *dataset, int *write) +/* + * Helper function to check if a dataset matches against a list of + * delegated datasets. Returns visibility and sets write permission. + */ +static int +zone_dataset_check_list(struct list_head *datasets, const char *dataset, + size_t dsnamelen, int *write) { - zone_datasets_t *zds; zone_dataset_t *zd; - size_t dsnamelen, zd_len; - int visible; + size_t zd_len; + int visible = 0; - /* Default to read-only, in case visible is returned. */ - if (write != NULL) - *write = 0; - if (zone_dataset_name_check(dataset, &dsnamelen) != 0) - return (0); - if (INGLOBALZONE(curproc)) { - if (write != NULL) - *write = 1; - return (1); - } - - mutex_enter(&zone_datasets_lock); - zds = zone_datasets_lookup(crgetzoneid(curproc->cred)); - if (zds == NULL) { - mutex_exit(&zone_datasets_lock); - return (0); - } - - visible = 0; - list_for_each_entry(zd, &zds->zds_datasets, zd_list) { + list_for_each_entry(zd, datasets, zd_list) { zd_len = strlen(zd->zd_dsname); if (zd_len > dsnamelen) { /* @@ -352,7 +628,8 @@ zone_dataset_visible(const char *dataset, int *write) * the namespace entry. */ visible = memcmp(zd->zd_dsname, dataset, - zd_len) == 0 && dataset[zd_len] == '/'; + zd_len) == 0 && (dataset[zd_len] == '/' || + dataset[zd_len] == '@' || dataset[zd_len] == '#'); if (visible) { if (write != NULL) *write = 1; @@ -361,9 +638,70 @@ zone_dataset_visible(const char *dataset, int *write) } } - mutex_exit(&zone_datasets_lock); return (visible); } + +#if defined(CONFIG_USER_NS) +/* + * Check UID-based zoning visibility for the current process. + * Must be called with zone_datasets_lock held. + */ +static int +zone_dataset_visible_uid(const char *dataset, size_t dsnamelen, int *write) +{ + zone_uid_datasets_t *zuds; + + zuds = zone_uid_datasets_lookup(curproc->cred->user_ns->owner); + if (zuds != NULL) + return (zone_dataset_check_list(&zuds->zuds_datasets, dataset, + dsnamelen, write)); + return (0); +} +#endif + +int +zone_dataset_visible(const char *dataset, int *write) +{ + zone_datasets_t *zds; + size_t dsnamelen; + int visible; + + /* Default to read-only, in case visible is returned. */ + if (write != NULL) + *write = 0; + if (zone_dataset_name_check(dataset, &dsnamelen) != 0) + return (0); + if (INGLOBALZONE(curproc)) { + if (write != NULL) + *write = 1; + return (1); + } + + mutex_enter(&zone_datasets_lock); + + /* First, check namespace-specific zoning (existing behavior) */ + zds = zone_datasets_lookup(crgetzoneid(curproc->cred)); + if (zds != NULL) { + visible = zone_dataset_check_list(&zds->zds_datasets, dataset, + dsnamelen, write); + if (visible) { + mutex_exit(&zone_datasets_lock); + return (visible); + } + } + + /* Second, check UID-based zoning */ +#if defined(CONFIG_USER_NS) + visible = zone_dataset_visible_uid(dataset, dsnamelen, write); + if (visible) { + mutex_exit(&zone_datasets_lock); + return (visible); + } +#endif + + mutex_exit(&zone_datasets_lock); + return (0); +} EXPORT_SYMBOL(zone_dataset_visible); unsigned int @@ -395,8 +733,9 @@ EXPORT_SYMBOL(crgetzoneid); boolean_t inglobalzone(proc_t *proc) { + (void) proc; #if defined(CONFIG_USER_NS) - return (proc->cred->user_ns == &init_user_ns); + return (current_user_ns() == &init_user_ns); #else return (B_TRUE); #endif @@ -408,6 +747,7 @@ spl_zone_init(void) { mutex_init(&zone_datasets_lock, NULL, MUTEX_DEFAULT, NULL); INIT_LIST_HEAD(&zone_datasets); + INIT_LIST_HEAD(&zone_uid_datasets); return (0); } @@ -415,6 +755,7 @@ void spl_zone_fini(void) { zone_datasets_t *zds; + zone_uid_datasets_t *zuds; zone_dataset_t *zd; /* @@ -423,6 +764,22 @@ spl_zone_fini(void) * namespace is destroyed, just do it here, since spl is about to go * out of context. */ + + /* Clean up UID-based delegations */ + while (!list_empty(&zone_uid_datasets)) { + zuds = list_entry(zone_uid_datasets.next, + zone_uid_datasets_t, zuds_list); + while (!list_empty(&zuds->zuds_datasets)) { + zd = list_entry(zuds->zuds_datasets.next, + zone_dataset_t, zd_list); + list_del(&zd->zd_list); + kmem_free(zd, sizeof (*zd) + zd->zd_dsnamelen + 1); + } + list_del(&zuds->zuds_list); + kmem_free(zuds, sizeof (*zuds)); + } + + /* Clean up namespace-based delegations */ while (!list_empty(&zone_datasets)) { zds = list_entry(zone_datasets.next, zone_datasets_t, zds_list); while (!list_empty(&zds->zds_datasets)) { diff --git a/module/os/linux/zfs/spa_misc_os.c b/module/os/linux/zfs/spa_misc_os.c index d6323fd56a8..91010bdf642 100644 --- a/module/os/linux/zfs/spa_misc_os.c +++ b/module/os/linux/zfs/spa_misc_os.c @@ -39,8 +39,10 @@ #include #include #include +#include #include #include +#include #include "zfs_prop.h" @@ -122,16 +124,60 @@ spa_history_zone(void) return ("linux"); } +static int +spa_restore_zoned_uid_cb(const char *dsname, void *arg) +{ + (void) arg; + uint64_t zoned_uid = 0; + + if (dsl_prop_get(dsname, "zoned_uid", 8, 1, &zoned_uid, NULL) != 0) + return (0); + + if (zoned_uid != 0) { + int err = zone_dataset_attach_uid(kcred, dsname, + (uid_t)zoned_uid); + if (err != 0 && err != EEXIST) { + cmn_err(CE_WARN, "failed to restore zoned_uid for " + "'%s' (uid %llu): %d", dsname, + (unsigned long long)zoned_uid, err); + } + } + return (0); +} + void spa_import_os(spa_t *spa) { - (void) spa; + (void) dmu_objset_find(spa_name(spa), + spa_restore_zoned_uid_cb, NULL, DS_FIND_CHILDREN); +} + +static int +spa_cleanup_zoned_uid_cb(const char *dsname, void *arg) +{ + (void) arg; + uint64_t zoned_uid = 0; + + if (dsl_prop_get(dsname, "zoned_uid", 8, 1, &zoned_uid, NULL) != 0) + return (0); + + if (zoned_uid != 0) { + int err = zone_dataset_detach_uid(kcred, dsname, + (uid_t)zoned_uid); + if (err != 0 && err != ENOENT) { + cmn_err(CE_WARN, "failed to detach zoned_uid for " + "'%s' (uid %llu): %d", dsname, + (unsigned long long)zoned_uid, err); + } + } + return (0); } void spa_export_os(spa_t *spa) { - (void) spa; + (void) dmu_objset_find(spa_name(spa), + spa_cleanup_zoned_uid_cb, NULL, DS_FIND_CHILDREN); } void diff --git a/module/os/linux/zfs/zfs_ioctl_os.c b/module/os/linux/zfs/zfs_ioctl_os.c index 5421a441b32..ce6092be1da 100644 --- a/module/os/linux/zfs/zfs_ioctl_os.c +++ b/module/os/linux/zfs/zfs_ioctl_os.c @@ -170,6 +170,8 @@ zfs_ioc_userns_attach(zfs_cmd_t *zc) */ if (error == ENOTTY) error = ZFS_ERR_NOT_USER_NAMESPACE; + if (error == ENXIO) + error = ZFS_ERR_NO_USER_NS_SUPPORT; return (error); } @@ -190,6 +192,8 @@ zfs_ioc_userns_detach(zfs_cmd_t *zc) */ if (error == ENOTTY) error = ZFS_ERR_NOT_USER_NAMESPACE; + if (error == ENXIO) + error = ZFS_ERR_NO_USER_NS_SUPPORT; return (error); } diff --git a/module/zcommon/zfs_prop.c b/module/zcommon/zfs_prop.c index 71482c8d795..0866caf8795 100644 --- a/module/zcommon/zfs_prop.c +++ b/module/zcommon/zfs_prop.c @@ -525,6 +525,10 @@ zfs_prop_init(void) zprop_register_index(ZFS_PROP_ZONED, "zoned", 0, PROP_INHERIT, ZFS_TYPE_FILESYSTEM, "on | off", "ZONED", boolean_table, sfeatures); #endif + /* UID-based zoning for rootless containers */ + zprop_register_number(ZFS_PROP_ZONED_UID, "zoned_uid", 0, + PROP_INHERIT, ZFS_TYPE_FILESYSTEM, " | none", "ZONED_UID", + B_FALSE, sfeatures); zprop_register_index(ZFS_PROP_VSCAN, "vscan", 0, PROP_INHERIT, ZFS_TYPE_FILESYSTEM, "on | off", "VSCAN", boolean_table, sfeatures); zprop_register_index(ZFS_PROP_NBMAND, "nbmand", 0, PROP_INHERIT, diff --git a/module/zfs/dsl_deleg.c b/module/zfs/dsl_deleg.c index 200bee200d3..f3153d6901c 100644 --- a/module/zfs/dsl_deleg.c +++ b/module/zfs/dsl_deleg.c @@ -591,13 +591,16 @@ dsl_deleg_access_impl(dsl_dataset_t *ds, const char *perm, cred_t *cr) * the zoned property is set */ if (!INGLOBALZONE(curproc)) { - uint64_t zoned; + uint64_t zoned = 0; + uint64_t zoned_uid_val = 0; - if (dsl_prop_get_dd(dd, + (void) dsl_prop_get_dd(dd, zfs_prop_to_name(ZFS_PROP_ZONED), - 8, 1, &zoned, NULL, B_FALSE) != 0) - break; - if (!zoned) + 8, 1, &zoned, NULL, B_FALSE); + (void) dsl_prop_get_dd(dd, + zfs_prop_to_name(ZFS_PROP_ZONED_UID), + 8, 1, &zoned_uid_val, NULL, B_FALSE); + if (!zoned && zoned_uid_val == 0) break; } zapobj = dsl_dir_phys(dd)->dd_deleg_zapobj; diff --git a/module/zfs/zfs_ioctl.c b/module/zfs/zfs_ioctl.c index 3bbc9107ae2..e2498f3e3a4 100644 --- a/module/zfs/zfs_ioctl.c +++ b/module/zfs/zfs_ioctl.c @@ -286,6 +286,59 @@ static int zfs_fill_zplprops_root(uint64_t, nvlist_t *, nvlist_t *, int zfs_set_prop_nvlist(const char *, zprop_source_t, nvlist_t *, nvlist_t *); static int get_nvlist(uint64_t nvl, uint64_t size, int iflag, nvlist_t **nvp); +/* + * Callback for SPL to look up zoned_uid property. + * Walks ancestors to find the delegation root with zoned_uid set. + * Returns the zoned_uid value if found, or 0 if not set. + */ +static uid_t +zfs_get_zoned_uid(const char *dataset, char *root_out, size_t root_size) +{ + char path[ZFS_MAX_DATASET_NAME_LEN]; + char setpoint[ZFS_MAX_DATASET_NAME_LEN]; + char *slash, *at; + uint64_t zoned_uid_val = 0; + int error; + + (void) strlcpy(path, dataset, sizeof (path)); + + /* + * Strip snapshot suffix if present — snapshots inherit properties + * from their parent filesystem. + */ + at = strchr(path, '@'); + if (at != NULL) + *at = '\0'; + + /* + * Walk up the hierarchy until we find a dataset with zoned_uid set. + * This handles the case where the dataset doesn't exist yet (e.g., + * rename destination) — dsl_prop_get fails on non-existent datasets, + * so we walk up to find an existing ancestor. + * + * When the property is found (possibly via inheritance), setpoint + * tells us the actual delegation root where zoned_uid is locally + * set, rather than the dataset where we happened to query it. + */ + while (path[0] != '\0') { + error = dsl_prop_get(path, "zoned_uid", 8, 1, + &zoned_uid_val, setpoint); + + if (error == 0 && zoned_uid_val != 0) { + if (root_out != NULL) + (void) strlcpy(root_out, setpoint, root_size); + return ((uid_t)zoned_uid_val); + } + + slash = strrchr(path, '/'); + if (slash == NULL) + break; + *slash = '\0'; + } + + return (0); +} + static void history_str_free(char *buf) { @@ -501,6 +554,42 @@ zfs_secpolicy_write_perms(const char *name, const char *perm, cred_t *cr) return (error); } +/* + * Check dsl_deleg permission for zoned_uid datasets. + * + * This bypasses zfs_dozonecheck_ds() (which requires the 'zoned' property) + * because zoned_uid datasets use a different authentication model. The zone + * check was already performed by zone_dataset_admin_check(). + * + * Returns 0 if permission is granted, error otherwise. + * ECANCELED from dsl_deleg_access_impl() means delegation is disabled on the + * pool — in that case we deny access (POLP: no delegation = no access). + */ +static int +zfs_secpolicy_zoned_uid_deleg(const char *name, const char *perm, cred_t *cr) +{ + dsl_pool_t *dp; + dsl_dataset_t *ds; + int error; + + error = dsl_pool_hold(name, FTAG, &dp); + if (error != 0) + return (error); + error = dsl_dataset_hold(dp, name, FTAG, &ds); + if (error != 0) { + dsl_pool_rele(dp, FTAG); + return (error); + } + error = dsl_deleg_access_impl(ds, perm, cr); + dsl_dataset_rele(ds, FTAG); + dsl_pool_rele(dp, FTAG); + + /* ECANCELED = delegation disabled on pool; deny access (POLP) */ + if (error == ECANCELED) + return (SET_ERROR(EPERM)); + return (error); +} + /* * Policy for setting the security label property. * @@ -607,6 +696,31 @@ zfs_secpolicy_setprop(const char *dsname, zfs_prop_t prop, nvpair_t *propval, cred_t *cr) { const char *strval; + zone_admin_result_t zone_result; + + /* + * Check zoned_uid delegation first. However, even delegated + * namespace users must not be allowed to modify zoned_uid itself. + */ + zone_result = zone_dataset_admin_check(dsname, ZONE_OP_SETPROP, NULL); + if (zone_result == ZONE_ADMIN_ALLOWED) { + if (prop == ZFS_PROP_ZONED_UID) + return (SET_ERROR(EPERM)); + if (prop == ZFS_PROP_FILESYSTEM_LIMIT || + prop == ZFS_PROP_SNAPSHOT_LIMIT) { + char setpoint[ZFS_MAX_DATASET_NAME_LEN]; + uint64_t zoned_uid_val = 0; + if (dsl_prop_get(dsname, "zoned_uid", 8, 1, + &zoned_uid_val, setpoint) == 0 && + zoned_uid_val != 0 && + strcmp(dsname, setpoint) == 0) + return (SET_ERROR(EPERM)); + } + return (zfs_secpolicy_zoned_uid_deleg(dsname, + zfs_prop_to_name(prop), cr)); + } + if (zone_result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); /* * Check permissions for special properties. @@ -621,6 +735,15 @@ zfs_secpolicy_setprop(const char *dsname, zfs_prop_t prop, nvpair_t *propval, if (!INGLOBALZONE(curproc)) return (SET_ERROR(EPERM)); break; + case ZFS_PROP_ZONED_UID: + /* + * Disallow setting of 'zoned_uid' from within a + * delegated namespace -- only global zone can manage + * delegation assignments. + */ + if (!INGLOBALZONE(curproc)) + return (SET_ERROR(EPERM)); + break; case ZFS_PROP_QUOTA: case ZFS_PROP_FILESYSTEM_LIMIT: @@ -774,7 +897,21 @@ int zfs_secpolicy_destroy_perms(const char *name, cred_t *cr) { int error; + zone_admin_result_t result; + /* Check zoned_uid delegation first */ + result = zone_dataset_admin_check(name, ZONE_OP_DESTROY, NULL); + if (result == ZONE_ADMIN_ALLOWED) { + if ((error = zfs_secpolicy_zoned_uid_deleg(name, + ZFS_DELEG_PERM_DESTROY, cr)) != 0) + return (error); + return (zfs_secpolicy_zoned_uid_deleg(name, + ZFS_DELEG_PERM_MOUNT, cr)); + } + if (result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); + + /* NOT_APPLICABLE: continue with existing checks */ if ((error = zfs_secpolicy_write_perms(name, ZFS_DELEG_PERM_MOUNT, cr)) != 0) return (error); @@ -831,7 +968,21 @@ zfs_secpolicy_rename_perms(const char *from, const char *to, cred_t *cr) { char parentname[ZFS_MAX_DATASET_NAME_LEN]; int error; + zone_admin_result_t result; + /* Check zoned_uid delegation first */ + result = zone_dataset_admin_check(from, ZONE_OP_RENAME, to); + if (result == ZONE_ADMIN_ALLOWED) { + if ((error = zfs_secpolicy_zoned_uid_deleg(from, + ZFS_DELEG_PERM_RENAME, cr)) != 0) + return (error); + return (zfs_secpolicy_zoned_uid_deleg(from, + ZFS_DELEG_PERM_MOUNT, cr)); + } + if (result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); + + /* NOT_APPLICABLE: continue with existing checks */ if ((error = zfs_secpolicy_write_perms(from, ZFS_DELEG_PERM_RENAME, cr)) != 0) return (error); @@ -940,6 +1091,17 @@ zfs_secpolicy_recv(zfs_cmd_t *zc, nvlist_t *innvl, cred_t *cr) int zfs_secpolicy_snapshot_perms(const char *name, cred_t *cr) { + zone_admin_result_t result; + + /* Check zoned_uid delegation first */ + result = zone_dataset_admin_check(name, ZONE_OP_SNAPSHOT, NULL); + if (result == ZONE_ADMIN_ALLOWED) + return (zfs_secpolicy_zoned_uid_deleg(name, + ZFS_DELEG_PERM_SNAPSHOT, cr)); + if (result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); + + /* NOT_APPLICABLE: continue with existing checks */ return (zfs_secpolicy_write_perms(name, ZFS_DELEG_PERM_SNAPSHOT, cr)); } @@ -1062,13 +1224,35 @@ zfs_secpolicy_create_clone(zfs_cmd_t *zc, nvlist_t *innvl, cred_t *cr) { char parentname[ZFS_MAX_DATASET_NAME_LEN]; int error; - const char *origin; + const char *origin = NULL; + zone_admin_result_t result; if ((error = zfs_get_parent(zc->zc_name, parentname, sizeof (parentname))) != 0) return (error); - if (nvlist_lookup_string(innvl, "origin", &origin) == 0 && + (void) nvlist_lookup_string(innvl, "origin", &origin); + + /* Check zoned_uid delegation first */ + result = zone_dataset_admin_check(parentname, + origin != NULL ? ZONE_OP_CLONE : ZONE_OP_CREATE, origin); + if (result == ZONE_ADMIN_ALLOWED) { + if (origin != NULL) { + if ((error = zfs_secpolicy_zoned_uid_deleg(origin, + ZFS_DELEG_PERM_CLONE, cr)) != 0) + return (error); + } + if ((error = zfs_secpolicy_zoned_uid_deleg(parentname, + ZFS_DELEG_PERM_CREATE, cr)) != 0) + return (error); + return (zfs_secpolicy_zoned_uid_deleg(parentname, + ZFS_DELEG_PERM_MOUNT, cr)); + } + if (result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); + + /* NOT_APPLICABLE: continue with existing checks */ + if (origin != NULL && (error = zfs_secpolicy_write_perms(origin, ZFS_DELEG_PERM_CLONE, cr)) != 0) return (error); @@ -1131,6 +1315,14 @@ zfs_secpolicy_inherit_prop(zfs_cmd_t *zc, nvlist_t *innvl, cred_t *cr) if (prop == ZPROP_USERPROP) { if (!zfs_prop_user(zc->zc_value)) return (SET_ERROR(EINVAL)); + zone_admin_result_t zone_result; + zone_result = zone_dataset_admin_check(zc->zc_name, + ZONE_OP_SETPROP, NULL); + if (zone_result == ZONE_ADMIN_ALLOWED) + return (zfs_secpolicy_zoned_uid_deleg(zc->zc_name, + ZFS_DELEG_PERM_USERPROP, cr)); + if (zone_result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); return (zfs_secpolicy_write_perms(zc->zc_name, ZFS_DELEG_PERM_USERPROP, cr)); } else { @@ -2707,6 +2899,28 @@ zfs_prop_set_special(const char *dsname, zprop_source_t source, zfsvfs_rele(zfsvfs, FTAG); break; } + case ZFS_PROP_ZONED_UID: + { + uint64_t old_uid = 0; + (void) dsl_prop_get(dsname, "zoned_uid", 8, 1, &old_uid, NULL); + if (old_uid != 0) + (void) zone_dataset_detach_uid(CRED(), dsname, + (uid_t)old_uid); + if (intval != 0) { + err = zone_dataset_attach_uid(CRED(), dsname, + (uid_t)intval); + if (err == ENXIO) + err = ZFS_ERR_NO_USER_NS_SUPPORT; + if (err != 0) + break; + } + /* + * Set err to -1 to force the zfs_set_prop_nvlist code down the + * default path to set the value in the nvlist. + */ + err = -1; + break; + } default: err = -1; } @@ -3850,8 +4064,20 @@ zfs_ioc_snapshot(const char *poolname, nvlist_t *innvl, nvlist_t *outnvl) */ if (!nvlist_empty(props)) { *cp = '\0'; - error = zfs_secpolicy_write_perms(name, - ZFS_DELEG_PERM_USERPROP, CRED()); + zone_admin_result_t zone_result; + zone_result = zone_dataset_admin_check(name, + ZONE_OP_SETPROP, NULL); + if (zone_result == ZONE_ADMIN_DENIED) { + *cp = '@'; + return (SET_ERROR(EPERM)); + } + if (zone_result == ZONE_ADMIN_ALLOWED) { + error = zfs_secpolicy_zoned_uid_deleg(name, + ZFS_DELEG_PERM_USERPROP, CRED()); + } else { + error = zfs_secpolicy_write_perms(name, + ZFS_DELEG_PERM_USERPROP, CRED()); + } *cp = '@'; if (error != 0) return (error); @@ -4333,6 +4559,14 @@ zfs_ioc_destroy(zfs_cmd_t *zc) if (strchr(zc->zc_name, '@')) { err = dsl_destroy_snapshot(zc->zc_name, zc->zc_defer_destroy); } else { + /* + * Save zoned_uid before destroying so we can clean up + * kernel-side zone tracking after a successful destroy. + */ + uint64_t zoned_uid = 0; + (void) dsl_prop_get(zc->zc_name, "zoned_uid", + 8, 1, &zoned_uid, NULL); + err = dsl_destroy_head(zc->zc_name); if (err == EEXIST) { /* @@ -4362,6 +4596,11 @@ zfs_ioc_destroy(zfs_cmd_t *zc) else if (err == ENOENT) err = SET_ERROR(EEXIST); } + + if (err == 0 && zoned_uid != 0) { + (void) zone_dataset_detach_uid(kcred, + zc->zc_name, (uid_t)zoned_uid); + } } return (err); @@ -4859,7 +5098,24 @@ zfs_ioc_rename(zfs_cmd_t *zc) return (error); } else { - return (dsl_dir_rename(zc->zc_name, zc->zc_value)); + /* + * For dataset renames, update kernel-side zone tracking + * if the dataset has a zoned_uid delegation. Read the + * property before rename, then detach old / attach new. + */ + uint64_t zoned_uid = 0; + (void) dsl_prop_get(zc->zc_name, "zoned_uid", + 8, 1, &zoned_uid, NULL); + + err = dsl_dir_rename(zc->zc_name, zc->zc_value); + + if (err == 0 && zoned_uid != 0) { + (void) zone_dataset_detach_uid(kcred, + zc->zc_name, (uid_t)zoned_uid); + (void) zone_dataset_attach_uid(kcred, + zc->zc_value, (uid_t)zoned_uid); + } + return (err); } } @@ -4874,6 +5130,14 @@ zfs_check_settable(const char *dsname, nvpair_t *pair, cred_t *cr) if (prop == ZPROP_USERPROP) { if (zfs_prop_user(propname)) { + zone_admin_result_t zone_result; + zone_result = zone_dataset_admin_check(dsname, + ZONE_OP_SETPROP, NULL); + if (zone_result == ZONE_ADMIN_ALLOWED) + return (zfs_secpolicy_zoned_uid_deleg(dsname, + ZFS_DELEG_PERM_USERPROP, cr)); + if (zone_result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); if ((err = zfs_secpolicy_write_perms(dsname, ZFS_DELEG_PERM_USERPROP, cr))) return (err); @@ -4918,6 +5182,14 @@ zfs_check_settable(const char *dsname, nvpair_t *pair, cred_t *cr) return (SET_ERROR(EINVAL)); } + zone_admin_result_t zone_result; + zone_result = zone_dataset_admin_check(dsname, + ZONE_OP_SETPROP, NULL); + if (zone_result == ZONE_ADMIN_ALLOWED) + return (zfs_secpolicy_zoned_uid_deleg(dsname, + perm, cr)); + if (zone_result == ZONE_ADMIN_DENIED) + return (SET_ERROR(EPERM)); if ((err = zfs_secpolicy_write_perms(dsname, perm, cr))) return (err); return (0); @@ -8267,6 +8539,9 @@ zfs_kmod_init(void) zfs_ioctl_init(); + /* Register zoned_uid property lookup callback with SPL */ + zone_register_zoned_uid_callback(zfs_get_zoned_uid); + mutex_init(&zfsdev_state_lock, NULL, MUTEX_DEFAULT, NULL); zfsdev_state_listhead.zs_minor = -1; @@ -8305,6 +8580,10 @@ zfs_kmod_fini(void) } zfs_ereport_taskq_fini(); /* run before zfs_fini() on Linux */ + + /* Unregister zoned_uid callback before ZFS layer is torn down */ + zone_unregister_zoned_uid_callback(); + zfs_fini(); spa_fini(); zvol_fini(); diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 391609332f4..7a079c17edc 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -1103,6 +1103,22 @@ tests = ['xattr_001_pos', 'xattr_002_neg', 'xattr_003_neg', 'xattr_004_pos', 'xattr_compat'] tags = ['functional', 'xattr'] +[tests/functional/zoned_uid:Linux] +tests = ['zoned_uid_001_pos', 'zoned_uid_002_pos', 'zoned_uid_003_pos', + 'zoned_uid_004_pos', 'zoned_uid_005_neg', 'zoned_uid_006_pos', + 'zoned_uid_007_pos', 'zoned_uid_008_pos', 'zoned_uid_009_pos', + 'zoned_uid_010_pos', 'zoned_uid_011_neg', 'zoned_uid_012_pos', + 'zoned_uid_013_pos', 'zoned_uid_014_pos', + 'zoned_uid_015_pos', 'zoned_uid_016_pos', 'zoned_uid_017_neg', + 'zoned_uid_018_pos', 'zoned_uid_019_neg', 'zoned_uid_020_neg', + 'zoned_uid_021_neg', 'zoned_uid_022_neg', + 'zoned_uid_030_pos', + 'zoned_uid_023_pos', 'zoned_uid_024_neg', + 'zoned_uid_025_pos', 'zoned_uid_026_pos', + 'zoned_uid_027_pos', 'zoned_uid_028_neg', + 'zoned_uid_029_neg', 'zoned_uid_031_pos'] +tags = ['functional', 'zoned_uid'] + [tests/functional/zvol/zvol_ENOSPC] tests = ['zvol_ENOSPC_001_pos'] tags = ['functional', 'zvol', 'zvol_ENOSPC'] diff --git a/tests/test-runner/bin/zts-report.py.in b/tests/test-runner/bin/zts-report.py.in index cc30b06a158..5006b3d4d90 100755 --- a/tests/test-runner/bin/zts-report.py.in +++ b/tests/test-runner/bin/zts-report.py.in @@ -172,6 +172,7 @@ if sys.platform.startswith('freebsd'): ['FAIL', known_reason], 'cli_root/zpool_resilver/zpool_resilver_concurrent': ['SKIP', na_reason], + 'zoned_uid/setup': ['SKIP', na_reason], 'cli_root/zpool_wait/zpool_wait_trim_basic': ['SKIP', trim_reason], 'cli_root/zpool_wait/zpool_wait_trim_cancel': ['SKIP', trim_reason], 'cli_root/zpool_wait/zpool_wait_trim_flag': ['SKIP', trim_reason], @@ -367,6 +368,7 @@ elif sys.platform.startswith('linux'): 'limits/filesystem_limit': ['SKIP', known_reason], 'limits/snapshot_limit': ['SKIP', known_reason], 'stat/statx_dioalign': ['SKIP', 'statx_reason'], + 'zoned_uid/setup': ['SKIP', user_ns_reason], }) diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 4ba9aa7c8b6..ed3e9250ae5 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -129,6 +129,7 @@ export SYSTEM_FILES_LINUX='attr blkid blkdiscard blockdev + capsh chattr cryptsetup exportfs diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 6c2f7aa7782..7f8f2eac8b8 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -393,6 +393,8 @@ nobase_dist_datadir_zfs_tests_tests_DATA += \ functional/vdev_zaps/vdev_zaps.kshlib \ functional/xattr/xattr.cfg \ functional/xattr/xattr_common.kshlib \ + functional/zoned_uid/zoned_uid.cfg \ + functional/zoned_uid/zoned_uid_common.kshlib \ functional/zvol/zvol.cfg \ functional/zvol/zvol_cli/zvol_cli.cfg \ functional/zvol/zvol_common.shlib \ @@ -2267,6 +2269,39 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/xattr/xattr_013_pos.ksh \ functional/xattr/xattr_014_pos.ksh \ functional/xattr/xattr_compat.ksh \ + functional/zoned_uid/cleanup.ksh \ + functional/zoned_uid/setup.ksh \ + functional/zoned_uid/zoned_uid_001_pos.ksh \ + functional/zoned_uid/zoned_uid_002_pos.ksh \ + functional/zoned_uid/zoned_uid_003_pos.ksh \ + functional/zoned_uid/zoned_uid_004_pos.ksh \ + functional/zoned_uid/zoned_uid_005_neg.ksh \ + functional/zoned_uid/zoned_uid_006_pos.ksh \ + functional/zoned_uid/zoned_uid_007_pos.ksh \ + functional/zoned_uid/zoned_uid_008_pos.ksh \ + functional/zoned_uid/zoned_uid_009_pos.ksh \ + functional/zoned_uid/zoned_uid_010_pos.ksh \ + functional/zoned_uid/zoned_uid_011_neg.ksh \ + functional/zoned_uid/zoned_uid_012_pos.ksh \ + functional/zoned_uid/zoned_uid_013_pos.ksh \ + functional/zoned_uid/zoned_uid_014_pos.ksh \ + functional/zoned_uid/zoned_uid_015_pos.ksh \ + functional/zoned_uid/zoned_uid_016_pos.ksh \ + functional/zoned_uid/zoned_uid_017_neg.ksh \ + functional/zoned_uid/zoned_uid_018_pos.ksh \ + functional/zoned_uid/zoned_uid_019_neg.ksh \ + functional/zoned_uid/zoned_uid_020_neg.ksh \ + functional/zoned_uid/zoned_uid_021_neg.ksh \ + functional/zoned_uid/zoned_uid_022_neg.ksh \ + functional/zoned_uid/zoned_uid_023_pos.ksh \ + functional/zoned_uid/zoned_uid_024_neg.ksh \ + functional/zoned_uid/zoned_uid_025_pos.ksh \ + functional/zoned_uid/zoned_uid_026_pos.ksh \ + functional/zoned_uid/zoned_uid_027_pos.ksh \ + functional/zoned_uid/zoned_uid_028_neg.ksh \ + functional/zoned_uid/zoned_uid_029_neg.ksh \ + functional/zoned_uid/zoned_uid_030_pos.ksh \ + functional/zoned_uid/zoned_uid_031_pos.ksh \ functional/zap_shrink/cleanup.ksh \ functional/zap_shrink/zap_shrink_001_pos.ksh \ functional/zap_shrink/setup.ksh \ diff --git a/tests/zfs-tests/tests/functional/zoned_uid/cleanup.ksh b/tests/zfs-tests/tests/functional/zoned_uid/cleanup.ksh new file mode 100755 index 00000000000..c611e5a4d03 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/cleanup.ksh @@ -0,0 +1,46 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid.cfg + +# Restore AppArmor user namespace restriction if we relaxed it +APPARMOR_USERNS=/proc/sys/kernel/apparmor_restrict_unprivileged_userns +APPARMOR_RESTORE=/tmp/zoned_uid_apparmor_restore +if [ -f "$APPARMOR_RESTORE" ]; then + cat "$APPARMOR_RESTORE" > "$APPARMOR_USERNS" + rm -f "$APPARMOR_RESTORE" +fi + +# Remove test users created during setup +for uid in "$ZONED_TEST_UID" "$ZONED_OTHER_UID"; do + if id "zfs_test_$uid" >/dev/null 2>&1; then + userdel "zfs_test_$uid" 2>/dev/null + fi +done + +default_cleanup diff --git a/tests/zfs-tests/tests/functional/zoned_uid/setup.ksh b/tests/zfs-tests/tests/functional/zoned_uid/setup.ksh new file mode 100755 index 00000000000..3345a5981a3 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/setup.ksh @@ -0,0 +1,99 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# Only run on Linux - zoned_uid is Linux-specific +if ! is_linux; then + log_unsupported "zoned_uid is only supported on Linux" +fi + +# Check kernel supports user namespaces +if ! [ -f /proc/self/uid_map ]; then + log_unsupported "The kernel doesn't support user namespaces." +fi + +verify_runnable "global" + +DISK=${DISKS%% *} +default_setup_noexit $DISK + +# Check if zoned_uid property is supported (requires pool to exist) +if ! zoned_uid_supported; then + default_cleanup_noexit + log_unsupported "zoned_uid property not supported by this kernel" +fi + +# +# Provision test users if they don't exist. +# Tests use "sudo -u #" which requires the UID to have a passwd entry. +# CI environments (e.g. GitHub Actions QEMU VMs) typically don't have these. +# +for uid in "$ZONED_TEST_UID" "$ZONED_OTHER_UID"; do + if ! id "$uid" >/dev/null 2>&1; then + log_note "Creating test user for UID $uid" + log_must useradd -u "$uid" -M -N -s /usr/sbin/nologin \ + "zfs_test_$uid" + fi +done + +# Some environments (e.g., Ubuntu with AppArmor) restrict unprivileged +# user namespace creation. Try to relax the restriction for testing. +APPARMOR_USERNS=/proc/sys/kernel/apparmor_restrict_unprivileged_userns +APPARMOR_RESTORE=/tmp/zoned_uid_apparmor_restore +if [ -f "$APPARMOR_USERNS" ]; then + orig=$(cat "$APPARMOR_USERNS") + if [ "$orig" != "0" ]; then + echo "$orig" > "$APPARMOR_RESTORE" + echo 0 > "$APPARMOR_USERNS" + log_note "Relaxed AppArmor user namespace restriction for testing" + fi +fi + +# Verify user namespace creation works with the test UIDs. +if ! sudo -u \#${ZONED_TEST_UID} unshare --user --map-root-user \ + true 2>/dev/null; then + default_cleanup_noexit + log_unsupported "Cannot create user namespaces as UID $ZONED_TEST_UID" +fi + +# Verify capsh is available and works for capability control tests. +# Tests 023+ use run_in_userns_caps which requires capsh. +typeset _capsh_found +_capsh_found="$(which capsh)" +if [[ -z "$_capsh_found" ]]; then + log_note "WARNING: capsh not found; capability-tier tests will be skipped" +else + if ! verify_capsh_works; then + log_note "WARNING: capsh cap control broken; capability-tier tests may fail" + else + log_note "capsh capability control verified" + fi +fi + +log_pass diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid.cfg b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid.cfg new file mode 100644 index 00000000000..e3a98d38e96 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid.cfg @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +# Test UID for zoned_uid - the user namespace owner's UID +# On this system, the "container" user (UID 956) owns subuid range 100000-165535 +# The zoned_uid should match the user who will create user namespaces +export ZONED_TEST_UID=${ZONED_TEST_UID:-956} + +# A different UID to test non-matching case (colin) +export ZONED_OTHER_UID=${ZONED_OTHER_UID:-1000} diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_001_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_001_pos.ksh new file mode 100755 index 00000000000..775baf188bc --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_001_pos.ksh @@ -0,0 +1,85 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that the zoned_uid property can be set and retrieved. +# +# STRATEGY: +# 1. Verify default zoned_uid is 0 (none) +# 2. Set zoned_uid to a test UID +# 3. Verify the property value is correct +# 4. Clear zoned_uid (set to 0) +# 5. Verify it returns to 0 +# + +verify_runnable "global" + +function cleanup +{ + log_must zfs destroy -rf "$TESTPOOL/$TESTFS/zoned_test" +} + +log_assert "zoned_uid property can be set and retrieved" +log_onexit cleanup + +# Create test dataset +log_must zfs create "$TESTPOOL/$TESTFS/zoned_test" + +# Verify default is 0 +typeset default_val +default_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/zoned_test") +if [[ "$default_val" != "0" ]]; then + log_fail "Default zoned_uid should be 0, got: $default_val" +fi +log_note "Default zoned_uid is 0 as expected" + +# Set zoned_uid +log_must set_zoned_uid "$TESTPOOL/$TESTFS/zoned_test" "$ZONED_TEST_UID" + +# Verify the value +typeset set_val +set_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/zoned_test") +if [[ "$set_val" != "$ZONED_TEST_UID" ]]; then + log_fail "zoned_uid should be $ZONED_TEST_UID, got: $set_val" +fi +log_note "zoned_uid set to $ZONED_TEST_UID successfully" + +# Clear zoned_uid +log_must clear_zoned_uid "$TESTPOOL/$TESTFS/zoned_test" + +# Verify it's back to 0 +typeset cleared_val +cleared_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/zoned_test") +if [[ "$cleared_val" != "0" ]]; then + log_fail "Cleared zoned_uid should be 0, got: $cleared_val" +fi +log_note "zoned_uid cleared to 0 successfully" + +log_pass "zoned_uid property can be set and retrieved" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_002_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_002_pos.ksh new file mode 100755 index 00000000000..51cd5be3638 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_002_pos.ksh @@ -0,0 +1,83 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that zoned_uid property persists through pool export/import. +# +# STRATEGY: +# 1. Create a test dataset +# 2. Set zoned_uid property +# 3. Export the pool +# 4. Import the pool +# 5. Verify zoned_uid property is preserved +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -f "$TESTPOOL/$TESTFS/persist_test" 2>/dev/null +} + +log_assert "zoned_uid property persists through pool export/import" +log_onexit cleanup + +# Create test dataset +log_must zfs create "$TESTPOOL/$TESTFS/persist_test" + +# Set zoned_uid +log_must set_zoned_uid "$TESTPOOL/$TESTFS/persist_test" "$ZONED_TEST_UID" + +# Verify before export +typeset before_val +before_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/persist_test") +if [[ "$before_val" != "$ZONED_TEST_UID" ]]; then + log_fail "Before export: zoned_uid should be $ZONED_TEST_UID, got: $before_val" +fi +log_note "zoned_uid is $ZONED_TEST_UID before export" + +# Export the pool +log_must zpool export "$TESTPOOL" + +# Import the pool +log_must zpool import "$TESTPOOL" + +# Verify after import +typeset after_val +after_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/persist_test") +if [[ "$after_val" != "$ZONED_TEST_UID" ]]; then + log_fail "After import: zoned_uid should be $ZONED_TEST_UID, got: $after_val" +fi +log_note "zoned_uid is $ZONED_TEST_UID after import" + +# Cleanup +log_must zfs destroy "$TESTPOOL/$TESTFS/persist_test" + +log_pass "zoned_uid property persists through pool export/import" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_003_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_003_pos.ksh new file mode 100755 index 00000000000..8be7d5cc092 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_003_pos.ksh @@ -0,0 +1,100 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that setting zoned_uid property does not break normal +# dataset operations from the global zone. +# +# STRATEGY: +# 1. Create a test dataset with zoned_uid set +# 2. Verify dataset is still visible and accessible from global zone +# 3. Create a child dataset +# 4. Verify child dataset operations work +# 5. Verify the property is shown in zfs list output +# + +verify_runnable "global" + +function cleanup +{ + log_must zfs destroy -rf "$TESTPOOL/$TESTFS/zoned_test" +} + +log_assert "zoned_uid property does not break global zone operations" +log_onexit cleanup + +# Create test dataset with zoned_uid +log_must zfs create "$TESTPOOL/$TESTFS/zoned_test" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/zoned_test" "$ZONED_TEST_UID" + +# Verify dataset is visible +log_must zfs list "$TESTPOOL/$TESTFS/zoned_test" +log_note "Dataset is visible from global zone" + +# Verify we can get properties +log_must zfs get all "$TESTPOOL/$TESTFS/zoned_test" +log_note "Can retrieve properties from global zone" + +# Verify zoned_uid appears in output +typeset list_output +list_output=$(zfs get -H -o property,value all "$TESTPOOL/$TESTFS/zoned_test" | grep zoned_uid) +if [[ -z "$list_output" ]]; then + log_fail "zoned_uid not shown in property listing" +fi +log_note "zoned_uid appears in property listing: $list_output" + +# Create child dataset +log_must zfs create "$TESTPOOL/$TESTFS/zoned_test/child" +log_note "Can create child dataset" + +# Verify child is visible +log_must zfs list "$TESTPOOL/$TESTFS/zoned_test/child" +log_note "Child dataset is visible" + +# Write data to the dataset +typeset mntpt +mntpt=$(get_prop mountpoint "$TESTPOOL/$TESTFS/zoned_test") +log_must touch "$mntpt/testfile" +log_must echo "test data" > "$mntpt/testfile" +log_note "Can write data to dataset" + +# Read data back +log_must cat "$mntpt/testfile" +log_note "Can read data from dataset" + +# Take a snapshot +log_must zfs snapshot "$TESTPOOL/$TESTFS/zoned_test@snap1" +log_note "Can create snapshot" + +# List snapshots +log_must zfs list -t snapshot "$TESTPOOL/$TESTFS/zoned_test@snap1" +log_note "Snapshot is visible" + +log_pass "zoned_uid property does not break global zone operations" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_004_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_004_pos.ksh new file mode 100755 index 00000000000..3692f9df5d0 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_004_pos.ksh @@ -0,0 +1,91 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that zoned_uid property is inherited by child datasets +# and can be overridden with a different value. +# +# STRATEGY: +# 1. Create parent dataset with zoned_uid +# 2. Create child dataset +# 3. Verify child inherits parent's zoned_uid value +# 4. Override zoned_uid on child with a different value +# 5. Verify each dataset has its own value +# + +verify_runnable "global" + +function cleanup +{ + log_must zfs destroy -rf "$TESTPOOL/$TESTFS/parent" +} + +log_assert "zoned_uid property is inherited by child datasets" +log_onexit cleanup + +# Create parent dataset with zoned_uid +log_must zfs create "$TESTPOOL/$TESTFS/parent" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/parent" "$ZONED_TEST_UID" + +# Create child dataset +log_must zfs create "$TESTPOOL/$TESTFS/parent/child" + +# Verify child inherits parent's value +typeset child_val +child_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/parent/child") +if [[ "$child_val" != "$ZONED_TEST_UID" ]]; then + log_fail "Child zoned_uid should inherit $ZONED_TEST_UID, got: $child_val" +fi +log_note "Child dataset inherits zoned_uid=$ZONED_TEST_UID from parent" + +# Verify parent still has its value +typeset parent_val +parent_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/parent") +if [[ "$parent_val" != "$ZONED_TEST_UID" ]]; then + log_fail "Parent zoned_uid should be $ZONED_TEST_UID, got: $parent_val" +fi +log_note "Parent dataset retains zoned_uid=$ZONED_TEST_UID" + +# Override with different value on child +log_must set_zoned_uid "$TESTPOOL/$TESTFS/parent/child" "$ZONED_OTHER_UID" + +# Verify each has independent value +parent_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/parent") +child_val=$(get_zoned_uid "$TESTPOOL/$TESTFS/parent/child") + +if [[ "$parent_val" != "$ZONED_TEST_UID" ]]; then + log_fail "Parent zoned_uid changed unexpectedly to: $parent_val" +fi +if [[ "$child_val" != "$ZONED_OTHER_UID" ]]; then + log_fail "Child zoned_uid should be $ZONED_OTHER_UID, got: $child_val" +fi +log_note "Parent and child have independent zoned_uid values after override" + +log_pass "zoned_uid property is inherited by child datasets" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_005_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_005_neg.ksh new file mode 100755 index 00000000000..5cc0577ba67 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_005_neg.ksh @@ -0,0 +1,72 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that invalid zoned_uid values are rejected. +# +# STRATEGY: +# 1. Try to set zoned_uid with invalid string value +# 2. Verify it fails +# 3. Try to set zoned_uid with negative value +# 4. Verify it fails +# + +verify_runnable "global" + +function cleanup +{ + if datasetexists "$TESTPOOL/$TESTFS/neg_test"; then + log_must zfs destroy -rf "$TESTPOOL/$TESTFS/neg_test" + fi +} + +log_assert "Invalid zoned_uid values are rejected" +log_onexit cleanup + +# Create test dataset +log_must zfs create "$TESTPOOL/$TESTFS/neg_test" + +# Try invalid string value +log_mustnot zfs set zoned_uid=invalid "$TESTPOOL/$TESTFS/neg_test" +log_note "Invalid string value rejected" + +# Try negative value (if shell allows it) +log_mustnot zfs set zoned_uid=-1 "$TESTPOOL/$TESTFS/neg_test" +log_note "Negative value rejected" + +# Verify dataset still has default value +typeset val +val=$(get_zoned_uid "$TESTPOOL/$TESTFS/neg_test") +if [[ "$val" != "0" ]]; then + log_fail "zoned_uid should still be 0 after failed sets, got: $val" +fi +log_note "zoned_uid unchanged after invalid set attempts" + +log_pass "Invalid zoned_uid values are rejected" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_006_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_006_pos.ksh new file mode 100755 index 00000000000..3322515edd4 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_006_pos.ksh @@ -0,0 +1,109 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can create child datasets +# under a delegation root with matching zoned_uid. +# +# STRATEGY: +# 1. Create a test dataset and set zoned_uid to test UID +# 2. Enter a user namespace owned by that UID +# 3. Verify CAP_SYS_ADMIN is present in the namespace +# 4. Attempt to create a child dataset +# 5. Verify the child dataset was created successfully +# + +verify_runnable "global" + +function cleanup +{ + # Clean up from global zone + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can create child datasets" +log_onexit cleanup + +# Create delegation root and set zoned_uid +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount + +# Verify zoned_uid is set +typeset actual_uid +actual_uid=$(get_zoned_uid "$TESTPOOL/$TESTFS/deleg_root") +if [[ "$actual_uid" != "$ZONED_TEST_UID" ]]; then + log_fail "zoned_uid not set correctly: expected $ZONED_TEST_UID, got $actual_uid" +fi +log_note "Delegation root created with zoned_uid=$ZONED_TEST_UID" + +# +# Enter user namespace and attempt to create child dataset. +# unshare --user creates a new user namespace where the caller +# has CAP_SYS_ADMIN (and all other capabilities) within that namespace. +# +# The --map-user option maps the current user to root inside the namespace, +# which is the standard rootless container setup. +# +log_note "Attempting to create child dataset from user namespace..." + +# Use sudo -u to run as the zoned_uid owner, then unshare into user namespace +# The user namespace owner will be ZONED_TEST_UID +typeset create_result +create_result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +create_status=$? + +if [[ $create_status -ne 0 ]]; then + log_note "Create output: $create_result" + log_fail "Failed to create child dataset from user namespace (status=$create_status)" +fi + +log_note "Child dataset created successfully from user namespace" + +# Verify the child exists (from global zone) +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child" +log_note "Child dataset verified from global zone" + +# Verify the child is visible from the user namespace +typeset list_result +list_result=$(run_in_userns "$ZONED_TEST_UID" \ + list "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +list_status=$? + +if [[ $list_status -ne 0 ]]; then + log_note "List output: $list_result" + log_fail "Child dataset not visible from user namespace" +fi + +log_note "Child dataset visible from user namespace" + +log_pass "Authorized user namespace can create child datasets" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_007_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_007_pos.ksh new file mode 100755 index 00000000000..64de7663a5b --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_007_pos.ksh @@ -0,0 +1,110 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can create snapshots +# of datasets under the delegation root. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create a child dataset (from global zone for setup) +# 3. Enter user namespace owned by the zoned_uid +# 4. Create a snapshot from within the user namespace +# 5. Verify the snapshot was created successfully +# 6. Verify the snapshot is visible from both namespaces +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can create snapshots" +log_onexit cleanup + +# Create delegation root and child dataset +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + snapshot +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Delegation root created with zoned_uid=$ZONED_TEST_UID" + +# Enter user namespace and create snapshot +log_note "Attempting to create snapshot from user namespace..." + +typeset snap_result +snap_result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +typeset snap_status=$? + +if [[ $snap_status -ne 0 ]]; then + log_note "Snapshot output: $snap_result" + log_fail "Failed to create snapshot from user namespace (status=$snap_status)" +fi + +log_note "Snapshot created successfully from user namespace" + +# Verify snapshot exists from global zone +log_must zfs list -t snapshot "$TESTPOOL/$TESTFS/deleg_root/child@snap1" +log_note "Snapshot verified from global zone" + +# Verify snapshot is visible from user namespace +typeset list_result +list_result=$(run_in_userns "$ZONED_TEST_UID" \ + list -t snapshot "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +typeset list_status=$? + +if [[ $list_status -ne 0 ]]; then + log_note "List output: $list_result" + log_fail "Snapshot not visible from user namespace" +fi + +log_note "Snapshot visible from user namespace" + +# Also test snapshot of the delegation root itself +log_note "Testing snapshot of delegation root..." +typeset root_snap_result +root_snap_result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root@rootsnap" 2>&1) +typeset root_snap_status=$? + +if [[ $root_snap_status -ne 0 ]]; then + log_note "Root snapshot output: $root_snap_result" + log_fail "Failed to snapshot delegation root from user namespace" +fi + +log_must zfs list -t snapshot "$TESTPOOL/$TESTFS/deleg_root@rootsnap" +log_note "Delegation root snapshot created successfully" + +log_pass "Authorized user namespace can create snapshots" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_008_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_008_pos.ksh new file mode 100755 index 00000000000..fa525166559 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_008_pos.ksh @@ -0,0 +1,128 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can destroy child datasets +# and snapshots, but cannot destroy the delegation root itself. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create child datasets and snapshots +# 3. Enter user namespace and destroy a snapshot (should succeed) +# 4. Destroy a child dataset (should succeed) +# 5. Attempt to destroy the delegation root (should fail - protected) +# 6. Verify the delegation root still exists +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can destroy children but not delegation root" +log_onexit cleanup + +# Create delegation root with children +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + destroy,mount +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child1" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child2" +log_must zfs snapshot "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" + +log_note "Created delegation root with children and snapshot" + +# Unmount child datasets from global zone before entering user namespace. +# Mounts inherited from the parent mount namespace are MNT_LOCKED by the +# kernel and cannot be unmounted from a child mount namespace. +log_must zfs unmount "$TESTPOOL/$TESTFS/deleg_root/child1" +log_must zfs unmount "$TESTPOOL/$TESTFS/deleg_root/child2" + +# Test 1: Destroy snapshot from user namespace (should succeed) +log_note "Test 1: Destroying snapshot from user namespace..." +typeset snap_result +snap_result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>&1) +typeset snap_status=$? + +if [[ $snap_status -ne 0 ]]; then + log_note "Destroy snapshot output: $snap_result" + log_fail "Failed to destroy snapshot from user namespace" +fi + +# Verify snapshot is gone +if zfs list -t snapshot "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>/dev/null; then + log_fail "Snapshot should have been destroyed" +fi +log_note "Snapshot destroyed successfully" + +# Test 2: Destroy child dataset from user namespace (should succeed) +log_note "Test 2: Destroying child dataset from user namespace..." +typeset child_result +child_result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/child1" 2>&1) +typeset child_status=$? + +if [[ $child_status -ne 0 ]]; then + log_note "Destroy child output: $child_result" + log_fail "Failed to destroy child dataset from user namespace" +fi + +# Verify child is gone +if zfs list "$TESTPOOL/$TESTFS/deleg_root/child1" 2>/dev/null; then + log_fail "Child dataset should have been destroyed" +fi +log_note "Child dataset destroyed successfully" + +# Test 3: Attempt to destroy delegation root (should FAIL - protected) +log_note "Test 3: Attempting to destroy delegation root (should fail)..." +typeset root_result +root_result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +typeset root_status=$? + +if [[ $root_status -eq 0 ]]; then + log_fail "Destroying delegation root should have been denied" +fi + +log_note "Delegation root destruction correctly denied: $root_result" + +# Verify delegation root still exists +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root" +log_note "Delegation root still exists (protected)" + +# Verify remaining child still exists +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child2" +log_note "Remaining child dataset unaffected" + +log_pass "Authorized user namespace can destroy children but not delegation root" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_009_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_009_pos.ksh new file mode 100755 index 00000000000..4fd66d5bbce --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_009_pos.ksh @@ -0,0 +1,149 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can rename datasets within +# the delegation subtree, but cannot rename datasets outside of it. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create child datasets +# 3. Enter user namespace and rename within the subtree (should succeed) +# 4. Attempt to rename outside the subtree (should fail) +# 5. Verify the rename operations behaved correctly +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null + zfs destroy -rf "$TESTPOOL/$TESTFS/outside" 2>/dev/null +} + +log_assert "Authorized user namespace can rename within delegation subtree only" +log_onexit cleanup + +# Create delegation root with children +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + rename,mount,create +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child1" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/subdir" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/subdir/nested" + +# Create a dataset outside the delegation root (for escape test) +log_must zfs create "$TESTPOOL/$TESTFS/outside" + +log_note "Created delegation root with children and outside dataset" + +# Unmount datasets from global zone before entering user namespace. +# Mounts inherited from the parent mount namespace are MNT_LOCKED by the +# kernel and cannot be unmounted from a child mount namespace. +log_must zfs unmount "$TESTPOOL/$TESTFS/deleg_root/subdir/nested" +log_must zfs unmount "$TESTPOOL/$TESTFS/deleg_root/subdir" +log_must zfs unmount "$TESTPOOL/$TESTFS/deleg_root/child1" +log_must zfs unmount "$TESTPOOL/$TESTFS/outside" + +# Test 1: Rename within subtree (should succeed) +log_note "Test 1: Renaming within delegation subtree..." +typeset rename_result +rename_result=$(run_in_userns "$ZONED_TEST_UID" \ + rename "$TESTPOOL/$TESTFS/deleg_root/child1" \ + "$TESTPOOL/$TESTFS/deleg_root/child1_renamed" 2>&1) +typeset rename_status=$? + +if [[ $rename_status -ne 0 ]]; then + log_note "Rename output: $rename_result" + log_fail "Failed to rename within delegation subtree" +fi + +# Verify old name is gone and new name exists +if zfs list "$TESTPOOL/$TESTFS/deleg_root/child1" 2>/dev/null; then + log_fail "Old dataset name should not exist after rename" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child1_renamed" +log_note "Rename within subtree succeeded" + +# Test 2: Rename to a different location within subtree (should succeed) +log_note "Test 2: Moving dataset within subtree..." +typeset move_result +move_result=$(run_in_userns "$ZONED_TEST_UID" \ + rename "$TESTPOOL/$TESTFS/deleg_root/subdir/nested" \ + "$TESTPOOL/$TESTFS/deleg_root/nested_moved" 2>&1) +typeset move_status=$? + +if [[ $move_status -ne 0 ]]; then + log_note "Move output: $move_result" + log_fail "Failed to move dataset within delegation subtree" +fi + +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/nested_moved" +log_note "Move within subtree succeeded" + +# Test 3: Attempt to rename outside the subtree (should FAIL) +log_note "Test 3: Attempting to rename outside subtree (should fail)..." +typeset escape_result +escape_result=$(run_in_userns "$ZONED_TEST_UID" \ + rename "$TESTPOOL/$TESTFS/deleg_root/child1_renamed" \ + "$TESTPOOL/$TESTFS/outside/escaped" 2>&1) +typeset escape_status=$? + +if [[ $escape_status -eq 0 ]]; then + log_fail "Renaming outside delegation subtree should have been denied" +fi + +log_note "Rename outside subtree correctly denied: $escape_result" + +# Verify the dataset is still in its original location +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child1_renamed" +log_note "Dataset remains in delegation subtree" + +# Test 4: Attempt to rename from outside into subtree (should FAIL) +# This tests that we can't "steal" datasets from outside +log_note "Test 4: Attempting to rename from outside into subtree (should fail)..." +typeset steal_result +steal_result=$(run_in_userns "$ZONED_TEST_UID" \ + rename "$TESTPOOL/$TESTFS/outside" \ + "$TESTPOOL/$TESTFS/deleg_root/stolen" 2>&1) +typeset steal_status=$? + +if [[ $steal_status -eq 0 ]]; then + log_fail "Renaming from outside into subtree should have been denied" +fi + +log_note "Rename from outside correctly denied: $steal_result" + +# Verify outside dataset still exists in original location +log_must zfs list "$TESTPOOL/$TESTFS/outside" +log_note "Outside dataset remains in place" + +log_pass "Authorized user namespace can rename within delegation subtree only" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_010_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_010_pos.ksh new file mode 100755 index 00000000000..c5f10048be4 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_010_pos.ksh @@ -0,0 +1,157 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can set properties on +# datasets within the delegation subtree. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create a child dataset +# 3. Enter user namespace and set various properties +# 4. Verify properties were set correctly +# 5. Test setting properties on delegation root itself +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can set properties on delegated datasets" +log_onexit cleanup + +# Create delegation root with child +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + quota,compression,atime,userprop +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Created delegation root with child dataset" + +# Test 1: Set quota on child dataset +log_note "Test 1: Setting quota from user namespace..." +typeset quota_result +quota_result=$(run_in_userns "$ZONED_TEST_UID" \ + set quota=100M "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +quota_status=$? + +if [[ $quota_status -ne 0 ]]; then + log_note "Set quota output: $quota_result" + log_fail "Failed to set quota from user namespace" +fi + +# Verify quota was set +typeset actual_quota +actual_quota=$(zfs get -H -o value quota "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_quota" != "100M" ]]; then + log_fail "Quota not set correctly: expected 100M, got $actual_quota" +fi +log_note "Quota set successfully to 100M" + +# Test 2: Set compression on child dataset +log_note "Test 2: Setting compression from user namespace..." +typeset comp_result +comp_result=$(run_in_userns "$ZONED_TEST_UID" \ + set compression=lz4 "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +comp_status=$? + +if [[ $comp_status -ne 0 ]]; then + log_note "Set compression output: $comp_result" + log_fail "Failed to set compression from user namespace" +fi + +typeset actual_comp +actual_comp=$(zfs get -H -o value compression "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_comp" != "lz4" ]]; then + log_fail "Compression not set correctly: expected lz4, got $actual_comp" +fi +log_note "Compression set successfully to lz4" + +# Test 3: Set atime on delegation root +# Unmount delegation root first — setting atime triggers a remount, and +# inherited mounts are MNT_LOCKED (cannot be remounted from a child mount +# namespace). +log_must zfs unmount "$TESTPOOL/$TESTFS/deleg_root" +log_note "Test 3: Setting atime on delegation root..." +typeset atime_result +atime_result=$(run_in_userns "$ZONED_TEST_UID" \ + set atime=off "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +atime_status=$? + +if [[ $atime_status -ne 0 ]]; then + log_note "Set atime output: $atime_result" + log_fail "Failed to set atime on delegation root" +fi + +typeset actual_atime +actual_atime=$(zfs get -H -o value atime "$TESTPOOL/$TESTFS/deleg_root") +if [[ "$actual_atime" != "off" ]]; then + log_fail "Atime not set correctly: expected off, got $actual_atime" +fi +log_note "Atime set successfully on delegation root" + +# Test 4: Set a user property +log_note "Test 4: Setting user property from user namespace..." +typeset userprop_result +userprop_result=$(run_in_userns "$ZONED_TEST_UID" \ + set com.example:testprop=testvalue "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +userprop_status=$? + +if [[ $userprop_status -ne 0 ]]; then + log_note "Set user property output: $userprop_result" + log_fail "Failed to set user property from user namespace" +fi + +typeset actual_userprop +actual_userprop=$(zfs get -H -o value com.example:testprop "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_userprop" != "testvalue" ]]; then + log_fail "User property not set correctly: expected testvalue, got $actual_userprop" +fi +log_note "User property set successfully" + +# Test 5: Verify properties are visible from user namespace +log_note "Test 5: Verifying properties visible from user namespace..." +typeset get_result +get_result=$(run_in_userns "$ZONED_TEST_UID" \ + get quota,compression "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +get_status=$? + +if [[ $get_status -ne 0 ]]; then + log_note "Get properties output: $get_result" + log_fail "Failed to get properties from user namespace" +fi + +log_note "Properties visible from user namespace" + +log_pass "Authorized user namespace can set properties on delegated datasets" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_011_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_011_neg.ksh new file mode 100755 index 00000000000..bc2bbe4a8dd --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_011_neg.ksh @@ -0,0 +1,153 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that a user namespace with a non-matching UID cannot perform +# write operations on datasets delegated to a different UID. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set to ZONED_TEST_UID +# 2. Enter a user namespace owned by ZONED_OTHER_UID (different) +# 3. Verify dataset is visible (read-only path visibility) +# 4. Attempt to create child dataset (should fail) +# 5. Attempt to create snapshot (should fail) +# 6. Attempt to set property (should fail) +# 7. Attempt to destroy (should fail) +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Unauthorized user namespace cannot perform write operations" +log_onexit cleanup + +# Create delegation root owned by ZONED_TEST_UID +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Created delegation root with zoned_uid=$ZONED_TEST_UID" +log_note "Will test access from user namespace owned by $ZONED_OTHER_UID" + +# Test 1: Verify dataset visibility (should be visible via parent path) +# Note: The dataset may or may not be visible depending on implementation +# The key test is that write operations fail +log_note "Test 1: Checking visibility from wrong user namespace..." +typeset list_result +list_result=$(run_in_userns "$ZONED_OTHER_UID" \ + list "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +list_status=$? +log_note "List result (status=$list_status): $list_result" + +# Test 2: Attempt to create child dataset (should FAIL) +log_note "Test 2: Attempting to create child from wrong namespace (should fail)..." +typeset create_result +create_result=$(run_in_userns "$ZONED_OTHER_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/unauthorized_child" 2>&1) +create_status=$? + +if [[ $create_status -eq 0 ]]; then + log_fail "Creating child from unauthorized namespace should have been denied" +fi +log_note "Create correctly denied: $create_result" + +# Verify the unauthorized child was not created +if zfs list "$TESTPOOL/$TESTFS/deleg_root/unauthorized_child" 2>/dev/null; then + log_fail "Unauthorized child dataset should not exist" +fi + +# Test 3: Attempt to create snapshot (should FAIL) +log_note "Test 3: Attempting to create snapshot from wrong namespace (should fail)..." +typeset snap_result +snap_result=$(run_in_userns "$ZONED_OTHER_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/child@unauthorized" 2>&1) +snap_status=$? + +if [[ $snap_status -eq 0 ]]; then + log_fail "Creating snapshot from unauthorized namespace should have been denied" +fi +log_note "Snapshot correctly denied: $snap_result" + +# Test 4: Attempt to set property (should FAIL) +log_note "Test 4: Attempting to set property from wrong namespace (should fail)..." +typeset prop_result +prop_result=$(run_in_userns "$ZONED_OTHER_UID" \ + set quota=1G "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +prop_status=$? + +if [[ $prop_status -eq 0 ]]; then + log_fail "Setting property from unauthorized namespace should have been denied" +fi +log_note "Set property correctly denied: $prop_result" + +# Verify quota was not changed +typeset actual_quota +actual_quota=$(zfs get -H -o value quota "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_quota" == "1G" ]]; then + log_fail "Quota should not have been changed by unauthorized namespace" +fi + +# Test 5: Attempt to destroy (should FAIL) +log_note "Test 5: Attempting to destroy from wrong namespace (should fail)..." +typeset destroy_result +destroy_result=$(run_in_userns "$ZONED_OTHER_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +destroy_status=$? + +if [[ $destroy_status -eq 0 ]]; then + log_fail "Destroying from unauthorized namespace should have been denied" +fi +log_note "Destroy correctly denied: $destroy_result" + +# Verify child still exists +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child" +log_note "Child dataset still exists (protected from unauthorized access)" + +# Test 6: Attempt to rename (should FAIL) +log_note "Test 6: Attempting to rename from wrong namespace (should fail)..." +typeset rename_result +rename_result=$(run_in_userns "$ZONED_OTHER_UID" \ + rename "$TESTPOOL/$TESTFS/deleg_root/child" \ + "$TESTPOOL/$TESTFS/deleg_root/child_renamed" 2>&1) +rename_status=$? + +if [[ $rename_status -eq 0 ]]; then + log_fail "Renaming from unauthorized namespace should have been denied" +fi +log_note "Rename correctly denied: $rename_result" + +# Verify child still has original name +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child" + +log_pass "Unauthorized user namespace cannot perform write operations" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_012_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_012_pos.ksh new file mode 100755 index 00000000000..db90ff1bade --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_012_pos.ksh @@ -0,0 +1,120 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can inherit properties +# on datasets within the delegation subtree. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create a child dataset +# 3. Set properties on the child, then inherit them from user namespace +# 4. Verify properties were inherited correctly +# 5. Test inheriting both native and user properties +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can inherit properties on delegated datasets" +log_onexit cleanup + +# Create delegation root with child +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + userprop,compression +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Created delegation root with child dataset" + +# Set a native property on child that we will then inherit +log_must zfs set compression=lz4 "$TESTPOOL/$TESTFS/deleg_root/child" + +typeset actual_comp +actual_comp=$(zfs get -H -o value compression "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_comp" != "lz4" ]]; then + log_fail "Failed to set compression: expected lz4, got $actual_comp" +fi + +# Set a user property on child that we will then inherit +log_must zfs set com.example:testprop=localvalue "$TESTPOOL/$TESTFS/deleg_root/child" + +typeset actual_userprop +actual_userprop=$(zfs get -H -o value com.example:testprop "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_userprop" != "localvalue" ]]; then + log_fail "Failed to set user property: expected localvalue, got $actual_userprop" +fi + +# Test 1: Inherit native property from user namespace +log_note "Test 1: Inheriting native property from user namespace..." +typeset inherit_result +inherit_result=$(run_in_userns "$ZONED_TEST_UID" \ + inherit compression "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +inherit_status=$? + +if [[ $inherit_status -ne 0 ]]; then + log_note "Inherit compression output: $inherit_result" + log_fail "Failed to inherit compression from user namespace" +fi + +# Verify compression was inherited (should match parent's value) +actual_comp=$(zfs get -H -o value compression "$TESTPOOL/$TESTFS/deleg_root/child") +typeset comp_source +comp_source=$(zfs get -H -o source compression "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$comp_source" == "local" ]]; then + log_fail "Compression still local after inherit: $actual_comp (source=$comp_source)" +fi +log_note "Compression inherited successfully (value=$actual_comp, source=$comp_source)" + +# Test 2: Inherit user property from user namespace +log_note "Test 2: Inheriting user property from user namespace..." +typeset inherit_userprop_result +inherit_userprop_result=$(run_in_userns "$ZONED_TEST_UID" \ + inherit com.example:testprop "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +inherit_userprop_status=$? + +if [[ $inherit_userprop_status -ne 0 ]]; then + log_note "Inherit user property output: $inherit_userprop_result" + log_fail "Failed to inherit user property from user namespace" +fi + +# Verify user property was removed (inherited means no local value) +actual_userprop=$(zfs get -H -o value com.example:testprop "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$actual_userprop" == "localvalue" ]]; then + log_fail "User property still has local value after inherit" +fi +log_note "User property inherited successfully (value=$actual_userprop)" + +log_pass "Authorized user namespace can inherit properties on delegated datasets" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_013_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_013_pos.ksh new file mode 100755 index 00000000000..c5a8bfe598b --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_013_pos.ksh @@ -0,0 +1,122 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib +. $STF_SUITE/include/math.shlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can set userquota +# and groupquota properties on delegated datasets. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create a child dataset +# 3. Enter user namespace and set userquota on child +# 4. Set groupquota on child +# 5. Verify quotas were applied correctly +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can set userquota/groupquota on delegated datasets" +log_onexit cleanup + +# Create delegation root with child +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + userquota,groupquota +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Created delegation root with child dataset" + +# Test 1: Set userquota from user namespace +log_note "Test 1: Setting userquota from user namespace..." +typeset uq_result +uq_result=$(run_in_userns "$ZONED_TEST_UID" \ + set userquota@0=50M "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +typeset uq_status=$? + +if [[ $uq_status -ne 0 ]]; then + log_note "Set userquota output: $uq_result" + log_fail "Failed to set userquota from user namespace" +fi + +# Verify userquota was set (use -p for parseable/raw bytes) +typeset actual_uq +actual_uq=$(zfs get -Hp -o value userquota@0 "$TESTPOOL/$TESTFS/deleg_root/child") +if ! within_percent "$actual_uq" $((50 * 1048576)) 99; then + log_fail "Userquota not set correctly: expected ~50M, got $actual_uq" +fi +log_note "Userquota set successfully ($actual_uq bytes)" + +# Test 2: Set groupquota from user namespace +log_note "Test 2: Setting groupquota from user namespace..." +typeset gq_result +gq_result=$(run_in_userns "$ZONED_TEST_UID" \ + set groupquota@0=100M "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +typeset gq_status=$? + +if [[ $gq_status -ne 0 ]]; then + log_note "Set groupquota output: $gq_result" + log_fail "Failed to set groupquota from user namespace" +fi + +# Verify groupquota was set (use -p for parseable/raw bytes) +typeset actual_gq +actual_gq=$(zfs get -Hp -o value groupquota@0 "$TESTPOOL/$TESTFS/deleg_root/child") +if ! within_percent "$actual_gq" $((100 * 1048576)) 99; then + log_fail "Groupquota not set correctly: expected ~100M, got $actual_gq" +fi +log_note "Groupquota set successfully ($actual_gq bytes)" + +# Test 3: Set userquota on delegation root itself +log_note "Test 3: Setting userquota on delegation root..." +typeset root_uq_result +root_uq_result=$(run_in_userns "$ZONED_TEST_UID" \ + set userquota@0=200M "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +typeset root_uq_status=$? + +if [[ $root_uq_status -ne 0 ]]; then + log_note "Set userquota on root output: $root_uq_result" + log_fail "Failed to set userquota on delegation root" +fi + +typeset actual_root_uq +actual_root_uq=$(zfs get -Hp -o value userquota@0 "$TESTPOOL/$TESTFS/deleg_root") +if ! within_percent "$actual_root_uq" $((200 * 1048576)) 99; then + log_fail "Root userquota not set correctly: expected ~200M, got $actual_root_uq" +fi +log_note "Delegation root userquota set successfully ($actual_root_uq bytes)" + +log_pass "Authorized user namespace can set userquota/groupquota on delegated datasets" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_014_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_014_pos.ksh new file mode 100755 index 00000000000..131addf6aa9 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_014_pos.ksh @@ -0,0 +1,116 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that an authorized user namespace can create sub-datasets +# (grandchildren) under a delegation root. The zoned_uid property +# must inherit so that children of children are also authorized. +# +# STRATEGY: +# 1. Create a delegation root and set zoned_uid +# 2. From user namespace, create a child dataset +# 3. From user namespace, create a grandchild under the child +# 4. Verify the grandchild exists and is visible from the namespace +# 5. Verify zoned_uid inherited to both child and grandchild +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Authorized user namespace can create sub-datasets (grandchildren)" +log_onexit cleanup + +# Create delegation root and set zoned_uid +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount +log_note "Delegation root created with zoned_uid=$ZONED_TEST_UID" + +# Step 1: Create child from user namespace +typeset create_result +create_result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +create_status=$? + +if [[ $create_status -ne 0 ]]; then + log_note "Create child output: $create_result" + log_fail "Failed to create child dataset (status=$create_status)" +fi +log_note "Child dataset created successfully" + +# Step 2: Create grandchild from user namespace +typeset grandchild_result +grandchild_result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child/grandchild" 2>&1) +grandchild_status=$? + +if [[ $grandchild_status -ne 0 ]]; then + log_note "Create grandchild output: $grandchild_result" + log_fail "Failed to create grandchild dataset (status=$grandchild_status)" +fi +log_note "Grandchild dataset created successfully" + +# Step 3: Verify both exist from global zone +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child" +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child/grandchild" +log_note "Both datasets verified from global zone" + +# Step 4: Verify grandchild is visible from user namespace +typeset list_result +list_result=$(run_in_userns "$ZONED_TEST_UID" \ + list "$TESTPOOL/$TESTFS/deleg_root/child/grandchild" 2>&1) +list_status=$? + +if [[ $list_status -ne 0 ]]; then + log_note "List output: $list_result" + log_fail "Grandchild not visible from user namespace" +fi +log_note "Grandchild visible from user namespace" + +# Step 5: Verify zoned_uid inherited to child and grandchild +typeset child_uid +child_uid=$(get_zoned_uid "$TESTPOOL/$TESTFS/deleg_root/child") +typeset grandchild_uid +grandchild_uid=$(get_zoned_uid "$TESTPOOL/$TESTFS/deleg_root/child/grandchild") + +if [[ "$child_uid" != "$ZONED_TEST_UID" ]]; then + log_fail "zoned_uid not inherited to child: expected $ZONED_TEST_UID, got $child_uid" +fi +if [[ "$grandchild_uid" != "$ZONED_TEST_UID" ]]; then + log_fail "zoned_uid not inherited to grandchild: expected $ZONED_TEST_UID, got $grandchild_uid" +fi +log_note "zoned_uid correctly inherited to child ($child_uid) and grandchild ($grandchild_uid)" + +log_pass "Authorized user namespace can create sub-datasets (grandchildren)" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_015_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_015_pos.ksh new file mode 100755 index 00000000000..9c5ad675e76 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_015_pos.ksh @@ -0,0 +1,114 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that destroying and recreating a pool with zoned_uid works +# without stale kernel state. Exercises the spa_export_os() cleanup +# path that must detach zone_uid_datasets entries on pool destroy. +# +# STRATEGY: +# 1. Create a delegation root with zoned_uid set +# 2. Create child datasets with inherited zoned_uid +# 3. Verify delegation works (create from namespace) +# 4. Destroy the pool +# 5. Recreate the pool with same zoned_uid +# 6. Verify delegation works again on the new pool +# + +verify_runnable "global" + +function cleanup +{ + if poolexists "$TESTPOOL"; then + # Ensure pool is in a clean state + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null + else + # Pool was destroyed by test; recreate it for the framework + DISK=${DISKS%% *} + default_setup_noexit "$DISK" + fi +} + +log_assert "Pool destroy/recreate with zoned_uid works without stale state" +log_onexit cleanup + +# Step 1-2: Create delegation root with children +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child1" + +log_note "Created delegation root with child, zoned_uid=$ZONED_TEST_UID" + +# Step 3: Verify delegation works +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/ns_child" 2>&1) +typeset status=$? + +if [[ $status -ne 0 ]]; then + log_note "Create output: $result" + log_fail "Initial delegation failed (status=$status)" +fi +log_note "Initial delegation works: created ns_child from namespace" + +# Step 4: Destroy the pool +log_must zpool destroy "$TESTPOOL" + +log_note "Pool destroyed" + +# Step 5: Recreate the pool with same zoned_uid +DISK=${DISKS%% *} +log_must zpool create -f "$TESTPOOL" "$DISK" +log_must zfs create "$TESTPOOL/$TESTFS" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount + +log_note "Pool recreated with zoned_uid=$ZONED_TEST_UID" + +# Step 6: Verify delegation works again on the new pool +typeset result2 +result2=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/ns_child2" 2>&1) +typeset status2=$? + +if [[ $status2 -ne 0 ]]; then + log_note "Create output after recreate: $result2" + log_fail "Delegation failed after pool destroy/recreate (status=$status2)" +fi + +# Verify the dataset exists +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/ns_child2" +log_note "Delegation works after pool destroy/recreate" + +log_pass "Pool destroy/recreate with zoned_uid works without stale state" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_016_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_016_pos.ksh new file mode 100755 index 00000000000..aeb97e20d58 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_016_pos.ksh @@ -0,0 +1,132 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that snapshots can be individually destroyed from within a +# delegated user namespace. Covers the zone_dataset_check_list() +# visibility fix for '@' separator. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. From namespace: create child, create snapshot on child +# 3. From namespace: verify snapshot is visible via zfs list -t snapshot +# 4. From namespace: destroy snapshot individually (zfs destroy ds@snap) +# 5. Verify snapshot is gone +# 6. From namespace: create snapshot on delegation root itself +# 7. From namespace: destroy that snapshot individually +# 8. Verify snapshot is gone +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Individual snapshot destroy works from delegated user namespace" +log_onexit cleanup + +# Step 1: Create delegation root +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount,snapshot,destroy + +# Step 2: Create child and snapshot from namespace +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create child output: $result" + log_fail "Failed to create child from namespace" +fi + +result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create snapshot output: $result" + log_fail "Failed to create snapshot from namespace" +fi + +log_note "Created child1@snap1 from namespace" + +# Step 3: Verify snapshot is visible from namespace +result=$(run_in_userns "$ZONED_TEST_UID" \ + list -t snapshot "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "List snapshot output: $result" + log_fail "Snapshot not visible from namespace" +fi +log_note "Snapshot visible from namespace" + +# Step 4: Destroy snapshot individually from namespace +result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>&1) +typeset status=$? + +if [[ $status -ne 0 ]]; then + log_note "Destroy snapshot output: $result" + log_fail "Failed to destroy individual snapshot from namespace (status=$status)" +fi + +# Step 5: Verify snapshot is gone +if zfs list -t snapshot "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>/dev/null; then + log_fail "Snapshot child1@snap1 should have been destroyed" +fi +log_note "child1@snap1 destroyed successfully from namespace" + +# Step 6: Create snapshot on delegation root itself, then destroy it +result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root@rootsnap" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create root snapshot output: $result" + log_fail "Failed to create snapshot on delegation root" +fi + +log_note "Created deleg_root@rootsnap from namespace" + +# Step 7: Destroy the root snapshot individually from namespace +result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root@rootsnap" 2>&1) +status=$? + +if [[ $status -ne 0 ]]; then + log_note "Destroy root snapshot output: $result" + log_fail "Failed to destroy root snapshot from namespace (status=$status)" +fi + +# Step 8: Verify root snapshot is gone +if zfs list -t snapshot "$TESTPOOL/$TESTFS/deleg_root@rootsnap" 2>/dev/null; then + log_fail "Snapshot deleg_root@rootsnap should have been destroyed" +fi +log_note "deleg_root@rootsnap destroyed successfully from namespace" + +log_pass "Individual snapshot destroy works from delegated user namespace" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_017_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_017_neg.ksh new file mode 100755 index 00000000000..40c314dcd98 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_017_neg.ksh @@ -0,0 +1,125 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that a namespace user cannot modify the zoned_uid property, +# even on datasets they have delegation over. Only the global zone +# admin should be able to manage delegation assignments. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid=$ZONED_TEST_UID +# 2. Create child dataset (inherits zoned_uid) +# 3. From namespace: attempt zfs set zoned_uid=none on child (should FAIL) +# 4. Verify zoned_uid still inherited on child +# 5. From namespace: attempt zfs set zoned_uid=$ZONED_OTHER_UID (should FAIL) +# 6. From namespace: attempt zfs set zoned_uid=$ZONED_TEST_UID (should FAIL) +# 7. From namespace: attempt zfs set zoned_uid=none on root (should FAIL) +# 8. Verify delegation root still has original zoned_uid +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Namespace user cannot modify zoned_uid property" +log_onexit cleanup + +# Step 1-2: Create delegation root and child +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Created delegation root and child with zoned_uid=$ZONED_TEST_UID" + +# Step 3: Attempt to clear zoned_uid on child from namespace (should FAIL) +log_note "Test 1: Attempting zfs set zoned_uid=none on child from namespace..." +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + set zoned_uid=none "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +typeset status=$? + +if [[ $status -eq 0 ]]; then + log_fail "Setting zoned_uid=none on child should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 4: Verify zoned_uid still inherited on child +typeset child_uid +child_uid=$(get_zoned_uid "$TESTPOOL/$TESTFS/deleg_root/child") +if [[ "$child_uid" != "$ZONED_TEST_UID" ]]; then + log_fail "Child zoned_uid changed to '$child_uid', expected '$ZONED_TEST_UID'" +fi +log_note "Child zoned_uid still $ZONED_TEST_UID (inherited)" + +# Step 5: Attempt to change zoned_uid to different UID (should FAIL) +log_note "Test 2: Attempting zfs set zoned_uid=$ZONED_OTHER_UID on child..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set "zoned_uid=$ZONED_OTHER_UID" "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "Setting zoned_uid to different UID should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 6: Attempt to set zoned_uid to same UID (should still FAIL) +log_note "Test 3: Attempting zfs set zoned_uid=$ZONED_TEST_UID on child..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set "zoned_uid=$ZONED_TEST_UID" "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "Setting zoned_uid (even to same value) should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 7: Attempt to clear zoned_uid on delegation root (should FAIL) +log_note "Test 4: Attempting zfs set zoned_uid=none on delegation root..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set zoned_uid=none "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "Setting zoned_uid=none on delegation root should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 8: Verify delegation root still has original zoned_uid +typeset root_uid +root_uid=$(get_zoned_uid "$TESTPOOL/$TESTFS/deleg_root") +if [[ "$root_uid" != "$ZONED_TEST_UID" ]]; then + log_fail "Root zoned_uid changed to '$root_uid', expected '$ZONED_TEST_UID'" +fi +log_note "Delegation root zoned_uid still $ZONED_TEST_UID" + +log_pass "Namespace user cannot modify zoned_uid property" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_018_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_018_pos.ksh new file mode 100755 index 00000000000..770b6bbcabc --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_018_pos.ksh @@ -0,0 +1,129 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that clone operations work from within a delegated user +# namespace, and that cloning outside the subtree is denied. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. From namespace: create child dataset +# 3. From namespace: create snapshot on child +# 4. From namespace: clone the snapshot to a new dataset within subtree +# 5. Verify clone exists and is writable +# 6. From namespace: attempt to clone outside the subtree (should FAIL) +# 7. Verify the failed clone doesn't exist +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null + zfs destroy -rf "$TESTPOOL/$TESTFS/outside_clone" 2>/dev/null +} + +log_assert "Clone operations work from delegated user namespace" +log_onexit cleanup + +# Step 1: Create delegation root +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount,snapshot,clone + +# Step 2: Create child from namespace +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create child output: $result" + log_fail "Failed to create child from namespace" +fi + +# Step 3: Create snapshot from namespace +result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create snapshot output: $result" + log_fail "Failed to create snapshot from namespace" +fi + +log_note "Created child@snap1 from namespace" + +# Step 4: Clone snapshot to new dataset within subtree +result=$(run_in_userns "$ZONED_TEST_UID" \ + clone "$TESTPOOL/$TESTFS/deleg_root/child@snap1" \ + "$TESTPOOL/$TESTFS/deleg_root/myclone" 2>&1) +typeset status=$? + +if [[ $status -ne 0 ]]; then + log_note "Clone output: $result" + log_fail "Failed to clone within subtree from namespace (status=$status)" +fi + +# Step 5: Verify clone exists and is writable +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/myclone" + +typeset origin +origin=$(zfs get -H -o value origin "$TESTPOOL/$TESTFS/deleg_root/myclone") +if [[ "$origin" != "$TESTPOOL/$TESTFS/deleg_root/child@snap1" ]]; then + log_fail "Clone origin should be child@snap1, got: $origin" +fi +log_note "Clone exists with correct origin" + +# Verify writable: create a child under the clone from namespace +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/myclone/subchild" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create under clone output: $result" + log_fail "Clone is not writable from namespace" +fi +log_note "Clone is writable from namespace" + +# Step 6: Attempt to clone outside the subtree (should FAIL) +log_note "Attempting clone to outside subtree..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + clone "$TESTPOOL/$TESTFS/deleg_root/child@snap1" \ + "$TESTPOOL/$TESTFS/outside_clone" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "Clone to outside subtree should have been denied" +fi +log_note "Correctly denied clone to outside subtree: $result" + +# Step 7: Verify the failed clone doesn't exist +if datasetexists "$TESTPOOL/$TESTFS/outside_clone"; then + log_fail "Outside clone should not exist" +fi +log_note "Outside clone correctly does not exist" + +log_pass "Clone operations work from delegated user namespace" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_019_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_019_neg.ksh new file mode 100755 index 00000000000..60393758357 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_019_neg.ksh @@ -0,0 +1,141 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that two different UIDs with sibling delegations cannot +# access each other's subtrees (multi-UID isolation). +# +# STRATEGY: +# 1. Create two sibling delegation roots with different zoned_uids +# 2. Create a child under each from global zone +# 3. From UID A's namespace: verify can create under deleg_root_a +# 4. From UID A's namespace: attempt create under deleg_root_b (FAIL) +# 5. From UID A's namespace: attempt destroy child under deleg_root_b (FAIL) +# 6. From UID A's namespace: attempt set property on deleg_root_b/child (FAIL) +# 7. From UID B's namespace: verify can create under deleg_root_b +# 8. From UID B's namespace: attempt create under deleg_root_a (FAIL) +# 9. Verify both subtrees remain intact +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root_a" 2>/dev/null + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root_b" 2>/dev/null +} + +log_assert "Multi-UID isolation: sibling delegations cannot cross boundaries" +log_onexit cleanup + +# Step 1: Create two sibling delegation roots +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root_a" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root_a" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root_a" "$ZONED_TEST_UID" \ + create,mount + +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root_b" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root_b" "$ZONED_OTHER_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root_b" "$ZONED_OTHER_UID" \ + create,mount + +# Step 2: Create a child under each from global zone +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root_a/child_a" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root_b/child_b" + +log_note "Created two delegation roots: A(uid=$ZONED_TEST_UID) B(uid=$ZONED_OTHER_UID)" + +# Step 3: UID A can create under its own subtree +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root_a/ns_child_a" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create output: $result" + log_fail "UID A should be able to create under deleg_root_a" +fi +log_note "UID A can create under its own subtree" + +# Step 4: UID A cannot create under UID B's subtree +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root_b/intruder_a" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "UID A should NOT be able to create under deleg_root_b" +fi +log_note "UID A correctly denied create under deleg_root_b" + +# Step 5: UID A cannot destroy child under UID B's subtree +result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root_b/child_b" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "UID A should NOT be able to destroy under deleg_root_b" +fi +log_note "UID A correctly denied destroy under deleg_root_b" + +# Step 6: UID A cannot set property on UID B's subtree +result=$(run_in_userns "$ZONED_TEST_UID" \ + set mountpoint=none "$TESTPOOL/$TESTFS/deleg_root_b/child_b" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "UID A should NOT be able to set properties on deleg_root_b" +fi +log_note "UID A correctly denied setprop on deleg_root_b" + +# Step 7: UID B can create under its own subtree +result=$(run_in_userns "$ZONED_OTHER_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root_b/ns_child_b" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create output: $result" + log_fail "UID B should be able to create under deleg_root_b" +fi +log_note "UID B can create under its own subtree" + +# Step 8: UID B cannot create under UID A's subtree +result=$(run_in_userns "$ZONED_OTHER_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root_a/intruder_b" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "UID B should NOT be able to create under deleg_root_a" +fi +log_note "UID B correctly denied create under deleg_root_a" + +# Step 9: Verify both subtrees remain intact +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root_a/child_a" +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root_a/ns_child_a" +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root_b/child_b" +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root_b/ns_child_b" + +# Verify intruder datasets don't exist +if datasetexists "$TESTPOOL/$TESTFS/deleg_root_b/intruder_a"; then + log_fail "Intruder dataset from UID A should not exist" +fi +if datasetexists "$TESTPOOL/$TESTFS/deleg_root_a/intruder_b"; then + log_fail "Intruder dataset from UID B should not exist" +fi +log_note "Both subtrees intact, no cross-contamination" + +log_pass "Multi-UID isolation: sibling delegations cannot cross boundaries" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_020_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_020_neg.ksh new file mode 100755 index 00000000000..4de33b30e54 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_020_neg.ksh @@ -0,0 +1,171 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that operations without zone_dataset_admin_check() integration +# are denied from a delegated namespace. These operations go through +# zfs_dozonecheck_impl() which requires zoned=on (not set in the +# zoned_uid-only flow), so they should all fail with EPERM. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid, create child, create snapshot +# 2. From namespace: attempt zfs send (should FAIL) +# 3. From namespace: attempt zfs rollback (should FAIL) +# 4. From namespace: attempt zfs hold (should FAIL) +# 5. From namespace: attempt zfs bookmark (should FAIL) +# 6. From namespace: attempt zfs allow (should FAIL) +# 7. From namespace: attempt zfs promote on a clone (should FAIL) +# 8. Verify dataset state unchanged +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Operations without admin_check integration are denied from namespace" +log_onexit cleanup + +# Step 1: Setup — create delegation root, child, snapshot, and clone +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount,snapshot + +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create child output: $result" + log_fail "Failed to create child from namespace" +fi + +result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create snapshot output: $result" + log_fail "Failed to create snapshot from namespace" +fi + +# Create a clone from global zone for promote test +log_must zfs clone "$TESTPOOL/$TESTFS/deleg_root/child@snap1" \ + "$TESTPOOL/$TESTFS/deleg_root/myclone" + +log_note "Setup complete: child, child@snap1, myclone" + +# Step 2: Attempt zfs send (should FAIL) +log_note "Test 1: zfs send from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + send "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +typeset status=$? + +if [[ $status -eq 0 ]]; then + log_fail "zfs send should have been denied from namespace" +fi +log_note "Correctly denied: zfs send (status=$status)" + +# Step 3: Attempt zfs rollback (should FAIL) +log_note "Test 2: zfs rollback from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + rollback "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "zfs rollback should have been denied from namespace" +fi +log_note "Correctly denied: zfs rollback (status=$status)" + +# Step 4: Attempt zfs hold (should FAIL) +log_note "Test 3: zfs hold from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + hold mytag "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "zfs hold should have been denied from namespace" +fi +log_note "Correctly denied: zfs hold (status=$status)" + +# Step 5: Attempt zfs bookmark (should FAIL) +log_note "Test 4: zfs bookmark from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + bookmark "$TESTPOOL/$TESTFS/deleg_root/child@snap1" \ + "$TESTPOOL/$TESTFS/deleg_root/child#bmark1" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "zfs bookmark should have been denied from namespace" +fi +log_note "Correctly denied: zfs bookmark (status=$status)" + +# Step 6: Attempt zfs allow (should FAIL) +log_note "Test 5: zfs allow from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + allow -e create "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "zfs allow should have been denied from namespace" +fi +log_note "Correctly denied: zfs allow (status=$status)" + +# Step 7: Attempt zfs promote (should FAIL) +log_note "Test 6: zfs promote from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + promote "$TESTPOOL/$TESTFS/deleg_root/myclone" 2>&1) +status=$? + +if [[ $status -eq 0 ]]; then + log_fail "zfs promote should have been denied from namespace" +fi +log_note "Correctly denied: zfs promote (status=$status)" + +# Step 8: Verify dataset state unchanged +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child" +log_must zfs list -t snapshot "$TESTPOOL/$TESTFS/deleg_root/child@snap1" +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/myclone" + +# Verify no holds were placed +typeset holds +holds=$(zfs holds "$TESTPOOL/$TESTFS/deleg_root/child@snap1" 2>&1 | wc -l) +if [[ $holds -gt 1 ]]; then + log_fail "Unexpected holds found on snapshot" +fi + +# Verify no bookmarks were created +if zfs list -t bookmark "$TESTPOOL/$TESTFS/deleg_root/child#bmark1" 2>/dev/null; then + log_fail "Bookmark should not exist" +fi + +log_note "All datasets unchanged after denied operations" + +log_pass "Operations without admin_check integration are denied from namespace" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_021_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_021_neg.ksh new file mode 100755 index 00000000000..6a3a7c3030c --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_021_neg.ksh @@ -0,0 +1,109 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that the 'zoned' property cannot be modified from within +# a delegated namespace. The ZFS_PROP_ZONED case blocks this via +# !INGLOBALZONE(curproc), but this is never tested in the zoned_uid +# delegation context. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. Create child dataset +# 3. From namespace: attempt zfs set zoned=on on child (should FAIL) +# 4. From namespace: attempt zfs set zoned=off on child (should FAIL) +# 5. From namespace: attempt zfs set zoned=on on delegation root (FAIL) +# 6. Verify zoned property unchanged on all datasets +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Cannot set 'zoned' property from delegated namespace" +log_onexit cleanup + +# Step 1-2: Create delegation root and child +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +log_note "Created delegation root and child" + +# Record original zoned values +typeset orig_root_zoned orig_child_zoned +orig_root_zoned=$(zfs get -H -o value zoned "$TESTPOOL/$TESTFS/deleg_root") +orig_child_zoned=$(zfs get -H -o value zoned "$TESTPOOL/$TESTFS/deleg_root/child") + +# Step 3: Attempt zfs set zoned=on on child (should FAIL) +log_note "Test 1: zfs set zoned=on on child from namespace..." +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + set zoned=on "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Setting zoned=on on child should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 4: Attempt zfs set zoned=off on child (should FAIL) +log_note "Test 2: zfs set zoned=off on child from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set zoned=off "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Setting zoned=off on child should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 5: Attempt zfs set zoned=on on delegation root (should FAIL) +log_note "Test 3: zfs set zoned=on on delegation root from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set zoned=on "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Setting zoned=on on delegation root should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 6: Verify zoned property unchanged on all datasets +typeset cur_root_zoned cur_child_zoned +cur_root_zoned=$(zfs get -H -o value zoned "$TESTPOOL/$TESTFS/deleg_root") +cur_child_zoned=$(zfs get -H -o value zoned "$TESTPOOL/$TESTFS/deleg_root/child") + +if [[ "$cur_root_zoned" != "$orig_root_zoned" ]]; then + log_fail "Root zoned changed from '$orig_root_zoned' to '$cur_root_zoned'" +fi +if [[ "$cur_child_zoned" != "$orig_child_zoned" ]]; then + log_fail "Child zoned changed from '$orig_child_zoned' to '$cur_child_zoned'" +fi +log_note "zoned property unchanged on all datasets" + +log_pass "Cannot set 'zoned' property from delegated namespace" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_022_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_022_neg.ksh new file mode 100755 index 00000000000..cf1775e5dbb --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_022_neg.ksh @@ -0,0 +1,154 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that delegated users cannot override filesystem_limit and +# snapshot_limit set by the global admin on the delegation root. +# Delegated users CAN set tighter sub-limits on child datasets. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. Global admin: set filesystem_limit=10, snapshot_limit=5 +# 3. From namespace: attempt filesystem_limit=none on root (FAIL) +# 4. From namespace: attempt snapshot_limit=none on root (FAIL) +# 5. Verify limits unchanged on delegation root +# 6. From namespace: create child dataset +# 7. From namespace: set filesystem_limit=3 on child (SUCCEED) +# 8. From namespace: set snapshot_limit=2 on child (SUCCEED) +# 9. Verify child has the sub-limits set +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Delegated user cannot override admin limits on delegation root" +log_onexit cleanup + +# Step 1: Create delegation root +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + create,mount,filesystem_limit,snapshot_limit + +# Step 2: Global admin sets limits +log_must zfs set filesystem_limit=10 "$TESTPOOL/$TESTFS/deleg_root" +log_must zfs set snapshot_limit=5 "$TESTPOOL/$TESTFS/deleg_root" + +log_note "Admin set filesystem_limit=10, snapshot_limit=5 on delegation root" + +# Step 3: Attempt to remove filesystem_limit from namespace (should FAIL) +log_note "Test 1: filesystem_limit=none on root from namespace..." +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + set filesystem_limit=none "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Removing filesystem_limit on root should have been denied" +fi +log_note "Correctly denied: $result" + +# Also try raising the limit +log_note "Test 2: filesystem_limit=100 on root from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set filesystem_limit=100 "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Raising filesystem_limit on root should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 4: Attempt to remove snapshot_limit from namespace (should FAIL) +log_note "Test 3: snapshot_limit=none on root from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set snapshot_limit=none "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Removing snapshot_limit on root should have been denied" +fi +log_note "Correctly denied: $result" + +# Step 5: Verify limits unchanged +typeset fs_limit snap_limit +fs_limit=$(get_prop filesystem_limit "$TESTPOOL/$TESTFS/deleg_root") +snap_limit=$(get_prop snapshot_limit "$TESTPOOL/$TESTFS/deleg_root") + +if [[ "$fs_limit" != "10" ]]; then + log_fail "filesystem_limit changed to '$fs_limit', expected '10'" +fi +if [[ "$snap_limit" != "5" ]]; then + log_fail "snapshot_limit changed to '$snap_limit', expected '5'" +fi +log_note "Admin limits unchanged on delegation root" + +# Step 6: Create child from namespace +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Create child output: $result" + log_fail "Failed to create child from namespace" +fi + +# Step 7: Set filesystem_limit on child (should SUCCEED - tighter sub-limit) +log_note "Test 4: filesystem_limit=3 on child from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set filesystem_limit=3 "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +typeset status=$? +if [[ $status -ne 0 ]]; then + log_note "Set filesystem_limit on child output: $result" + log_fail "Setting filesystem_limit on child should succeed (status=$status)" +fi + +# Step 8: Set snapshot_limit on child (should SUCCEED) +log_note "Test 5: snapshot_limit=2 on child from namespace..." +result=$(run_in_userns "$ZONED_TEST_UID" \ + set snapshot_limit=2 "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +status=$? +if [[ $status -ne 0 ]]; then + log_note "Set snapshot_limit on child output: $result" + log_fail "Setting snapshot_limit on child should succeed (status=$status)" +fi + +# Step 9: Verify child has the sub-limits +typeset child_fs_limit child_snap_limit +child_fs_limit=$(get_prop filesystem_limit \ + "$TESTPOOL/$TESTFS/deleg_root/child") +child_snap_limit=$(get_prop snapshot_limit \ + "$TESTPOOL/$TESTFS/deleg_root/child") + +if [[ "$child_fs_limit" != "3" ]]; then + log_fail "Child filesystem_limit should be 3, got: $child_fs_limit" +fi +if [[ "$child_snap_limit" != "2" ]]; then + log_fail "Child snapshot_limit should be 2, got: $child_snap_limit" +fi +log_note "Child has correct sub-limits: filesystem_limit=3, snapshot_limit=2" + +log_pass "Delegated user cannot override admin limits on delegation root" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_023_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_023_pos.ksh new file mode 100755 index 00000000000..9cdc73aa72d --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_023_pos.ksh @@ -0,0 +1,131 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Additive least privilege: non-destructive operations (create, snapshot, +# setprop) succeed only when BOTH dsl_deleg grants the permission AND +# the namespace has at least CAP_FOWNER. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. Grant create,snapshot,mount via zfs allow +# 3. With CAP_FOWNER: create succeeds (L1 yes + L2 yes) +# 4. With no caps: create fails (L1 yes, L2 no) +# 5. Without zfs allow grant: create fails even with CAP_FOWNER (L1 no) +# 6. With CAP_FOWNER + snapshot grant: snapshot succeeds +# 7. With CAP_FOWNER + create grant only: snapshot fails (wrong perm) +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Additive L1+L2: non-destructive ops need dsl_deleg AND CAP_FOWNER" +log_onexit cleanup + +# Step 1: Create delegation root. +# Use mountpoint=none so create/snapshot from the namespace don't +# trigger mount operations that would fail without CAP_SYS_ADMIN. +log_must zfs create -o mountpoint=none "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" + +# Step 2: Grant create,snapshot,mount +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,snapshot,mount" + +# ADD-1: L1 grants create + L2 has CAP_FOWNER → allowed +log_note "Test ADD-1: create with dsl_deleg + CAP_FOWNER" +typeset result +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + create "$TESTPOOL/$TESTFS/deleg_root/add1_child" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "ADD-1: create should succeed with dsl_deleg + CAP_FOWNER" +fi +log_note "ADD-1 passed: create allowed" + +# ADD-2: L1 grants create + L2 has no caps → denied +log_note "Test ADD-2: create with dsl_deleg + no caps" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + create "$TESTPOOL/$TESTFS/deleg_root/add2_child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-2: create should fail without capabilities" +fi +log_note "ADD-2 passed: create denied without caps" + +# Verify the dataset was NOT created +if datasetexists "$TESTPOOL/$TESTFS/deleg_root/add2_child"; then + log_fail "ADD-2: dataset should not exist" +fi + +# ADD-3: No dsl_deleg grant + CAP_FOWNER → denied +log_note "Test ADD-3: create without dsl_deleg grant" +log_must revoke_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + create "$TESTPOOL/$TESTFS/deleg_root/add3_child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-3: create should fail without dsl_deleg grant" +fi +log_note "ADD-3 passed: create denied without dsl_deleg" + +if datasetexists "$TESTPOOL/$TESTFS/deleg_root/add3_child"; then + log_fail "ADD-3: dataset should not exist" +fi + +# ADD-5: Restore grants, test snapshot with CAP_FOWNER +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,snapshot,mount" + +log_note "Test ADD-5: snapshot with dsl_deleg + CAP_FOWNER" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/add1_child@snap1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "ADD-5: snapshot should succeed with dsl_deleg + CAP_FOWNER" +fi +log_note "ADD-5 passed: snapshot allowed" + +# ADD-6: create grant only, snapshot should fail (wrong perm) +log_must revoke_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,mount" + +log_note "Test ADD-6: snapshot with create-only dsl_deleg" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/add1_child@snap_bad" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-6: snapshot should fail with create-only dsl_deleg" +fi +log_note "ADD-6 passed: snapshot denied (wrong perm in dsl_deleg)" + +log_pass "Additive L1+L2: non-destructive ops need dsl_deleg AND CAP_FOWNER" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_024_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_024_neg.ksh new file mode 100755 index 00000000000..487b3baa99c --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_024_neg.ksh @@ -0,0 +1,144 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Additive least privilege: destructive operations (destroy, rename, +# clone) require BOTH dsl_deleg grant AND CAP_SYS_ADMIN. +# CAP_FOWNER alone is insufficient for destructive operations. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid and child datasets +# 2. Grant destroy,rename,clone,mount,create via zfs allow +# 3. With CAP_SYS_ADMIN + destroy grant: destroy succeeds +# 4. With CAP_FOWNER + destroy grant: destroy fails (wrong cap tier) +# 5. With CAP_SYS_ADMIN but no destroy grant: destroy fails (L1 no) +# 6. With no caps + destroy grant: destroy fails (L2 no) +# 7. Test rename similarly: needs SYS_ADMIN + rename grant +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Additive L1+L2: destructive ops need dsl_deleg AND CAP_SYS_ADMIN" +log_onexit cleanup + +# Setup: delegation root with children. +# Use mountpoint=none so datasets aren't mounted in the host namespace; +# otherwise destroy from a user namespace fails because mount-locked +# mounts (created by the host) cannot be unmounted from a child namespace. +log_must zfs create -o mountpoint=none "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/victim1" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/victim2" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/victim3" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/victim4" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/rename_src" + +# Grant destructive permissions +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,destroy,rename,clone,mount,snapshot" + +# ADD-8: destroy with dsl_deleg + CAP_SYS_ADMIN → allowed +log_note "Test ADD-8: destroy with dsl_deleg + CAP_SYS_ADMIN" +typeset result +result=$(run_in_userns_caps "$ZONED_TEST_UID" "all" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/victim1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "ADD-8: destroy should succeed with dsl_deleg + CAP_SYS_ADMIN" +fi +if datasetexists "$TESTPOOL/$TESTFS/deleg_root/victim1"; then + log_fail "ADD-8: victim1 should not exist after destroy" +fi +log_note "ADD-8 passed: destroy allowed with SYS_ADMIN" + +# ADD-9: destroy with dsl_deleg + CAP_FOWNER → denied (wrong tier) +log_note "Test ADD-9: destroy with dsl_deleg + CAP_FOWNER only" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/victim2" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-9: destroy should fail with CAP_FOWNER (needs SYS_ADMIN)" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/victim2" +log_note "ADD-9 passed: destroy denied with CAP_FOWNER only" + +# ADD-10: destroy with dsl_deleg + no caps → denied +log_note "Test ADD-10: destroy with dsl_deleg + no caps" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/victim3" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-10: destroy should fail without any capabilities" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/victim3" +log_note "ADD-10 passed: destroy denied without caps" + +# ADD-11: destroy with CAP_SYS_ADMIN but NO dsl_deleg grant → denied +log_note "Test ADD-11: destroy without dsl_deleg grant" +log_must revoke_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "all" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/victim4" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-11: destroy should fail without dsl_deleg grant" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/victim4" +log_note "ADD-11 passed: destroy denied without dsl_deleg" + +# ADD-12: rename with dsl_deleg + CAP_SYS_ADMIN → allowed +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,destroy,rename,clone,mount,snapshot" + +log_note "Test ADD-12: rename with dsl_deleg + CAP_SYS_ADMIN" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "all" \ + rename "$TESTPOOL/$TESTFS/deleg_root/rename_src" \ + "$TESTPOOL/$TESTFS/deleg_root/rename_dst" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "ADD-12: rename should succeed with dsl_deleg + CAP_SYS_ADMIN" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/rename_dst" +log_note "ADD-12 passed: rename allowed with SYS_ADMIN" + +# ADD-13: clone with dsl_deleg + CAP_FOWNER → denied (destructive tier) +log_note "Test ADD-13: clone with dsl_deleg + CAP_FOWNER" +# Create a snapshot to clone from +log_must zfs snapshot "$TESTPOOL/$TESTFS/deleg_root/victim2@snap" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + clone "$TESTPOOL/$TESTFS/deleg_root/victim2@snap" \ + "$TESTPOOL/$TESTFS/deleg_root/clone_dst" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "ADD-13: clone should fail with CAP_FOWNER (needs SYS_ADMIN)" +fi +log_note "ADD-13 passed: clone denied with CAP_FOWNER only" + +log_pass "Additive L1+L2: destructive ops need dsl_deleg AND CAP_SYS_ADMIN" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_025_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_025_pos.ksh new file mode 100755 index 00000000000..77ecd631631 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_025_pos.ksh @@ -0,0 +1,102 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Read-only operations (list, get properties) require no capabilities +# and no dsl_deleg grants. Visibility is controlled solely by the +# zoned_uid delegation scoping. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid and a child +# 2. No dsl_deleg grants, no capabilities +# 3. From namespace: zfs list succeeds for delegated dataset +# 4. From namespace: zfs get succeeds for delegated dataset +# 5. From namespace: zfs list fails for non-delegated dataset +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Read-only operations need no caps and no dsl_deleg" +log_onexit cleanup + +# Setup: delegation root with a child, NO zfs allow grants. +# Use mountpoint=none to avoid mount-lock issues in user namespaces. +log_must zfs create -o mountpoint=none "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +# ADD-14: list with no caps, no dsl_deleg → allowed (read-only) +log_note "Test ADD-14: list delegated dataset with no caps" +typeset result +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + list "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "ADD-14: list should succeed with no caps (read-only)" +fi +log_note "ADD-14 passed: list allowed" + +# Get properties with no caps → should work +log_note "Test: get properties with no caps" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + get zoned_uid "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "get properties should succeed with no caps (read-only)" +fi +log_note "Get properties passed" + +# List child dataset with no caps → should work (child of delegation) +log_note "Test: list child dataset with no caps" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + list "$TESTPOOL/$TESTFS/deleg_root/child" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "list child should succeed with no caps" +fi +log_note "List child passed" + +# Non-delegated dataset should NOT be visible +log_note "Test: list non-delegated dataset from namespace" +log_must zfs create "$TESTPOOL/$TESTFS/other_ds" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + list "$TESTPOOL/$TESTFS/other_ds" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Non-delegated dataset should not be visible" +fi +log_note "Non-delegated dataset correctly not visible" +log_must zfs destroy "$TESTPOOL/$TESTFS/other_ds" + +log_pass "Read-only operations need no caps and no dsl_deleg" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_026_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_026_pos.ksh new file mode 100755 index 00000000000..10913fbdb4a --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_026_pos.ksh @@ -0,0 +1,112 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# When pool delegation is disabled (zpool set delegation=off), +# ALL zoned_uid write operations are denied regardless of +# capabilities. Delegation OFF means the pool admin has opted +# out of delegating access entirely (POLP). +# Read-only operations (list, get) still succeed. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. Disable delegation on the pool +# 3. DOFF-1: create with CAP_FOWNER → denied (delegation off) +# 4. DOFF-2: destroy with CAP_SYS_ADMIN → denied (delegation off) +# 5. DOFF-3: create with all caps → denied (delegation off) +# 6. DOFF-4: list with no caps → allowed (read-only) +# 7. Re-enable delegation +# + +verify_runnable "global" + +function cleanup +{ + # Always re-enable delegation + log_must zpool set delegation=on "$TESTPOOL" + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Delegation OFF: all zoned_uid writes denied" +log_onexit cleanup + +# Setup. +# Use mountpoint=none to avoid mount-lock issues in user namespaces. +log_must zfs create -o mountpoint=none "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/victim" + +# Disable delegation on pool +log_must zpool set delegation=off "$TESTPOOL" +log_note "Pool delegation disabled" + +# DOFF-1: create with CAP_FOWNER → denied (delegation off overrides caps) +log_note "Test DOFF-1: create with CAP_FOWNER (delegation off)" +typeset result +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + create "$TESTPOOL/$TESTFS/deleg_root/doff1_child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "DOFF-1: create should fail when delegation is off" +fi +log_note "DOFF-1 passed: create denied (delegation off)" + +# DOFF-2: destroy with CAP_SYS_ADMIN → denied (delegation off) +log_note "Test DOFF-2: destroy with CAP_SYS_ADMIN (delegation off)" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "all" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/victim" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "DOFF-2: destroy should fail when delegation is off" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/victim" +log_note "DOFF-2 passed: destroy denied (delegation off)" + +# DOFF-3: create with all caps → denied (delegation off) +log_note "Test DOFF-3: create with all caps (delegation off)" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "all" \ + create "$TESTPOOL/$TESTFS/deleg_root/doff3_child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "DOFF-3: create should fail when delegation is off" +fi +log_note "DOFF-3 passed: create denied (delegation off)" + +# DOFF-4: list with no caps → allowed (read-only) +log_note "Test DOFF-4: list with no caps (delegation off)" +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + list "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "DOFF-4: list should succeed with no caps (read-only)" +fi +log_note "DOFF-4 passed" + +# Re-enable delegation +log_must zpool set delegation=on "$TESTPOOL" + +log_pass "Delegation OFF: all zoned_uid writes denied" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_027_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_027_pos.ksh new file mode 100755 index 00000000000..c753145f154 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_027_pos.ksh @@ -0,0 +1,103 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# CAP_SYS_ADMIN satisfies the L2 requirement for ALL operation tiers +# (both non-destructive and destructive). This verifies that +# SYS_ADMIN is a superset of FOWNER for L2 purposes. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid +# 2. Grant all permissions via zfs allow +# 3. With CAP_SYS_ADMIN: create succeeds (SYS_ADMIN covers FOWNER tier) +# 4. With CAP_SYS_ADMIN: snapshot succeeds +# 5. With CAP_SYS_ADMIN: destroy succeeds +# 6. Verify CAP_SYS_ADMIN is a complete L2 pass for all ops +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "CAP_SYS_ADMIN satisfies L2 for all operation tiers" +log_onexit cleanup + +# Setup +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,destroy,snapshot,rename,clone,mount" + +# ADD-7: create with dsl_deleg + CAP_SYS_ADMIN → allowed +log_note "Test ADD-7: create with CAP_SYS_ADMIN" +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + create "$TESTPOOL/$TESTFS/deleg_root/child1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "ADD-7: create should succeed with SYS_ADMIN" +fi +log_note "ADD-7 passed: create allowed with SYS_ADMIN" + +# Snapshot with CAP_SYS_ADMIN +log_note "Test: snapshot with CAP_SYS_ADMIN" +result=$(run_in_userns "$ZONED_TEST_UID" \ + snapshot "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "snapshot should succeed with SYS_ADMIN" +fi +log_note "Snapshot passed with SYS_ADMIN" + +# Clone with CAP_SYS_ADMIN (destructive tier) +log_note "Test: clone with CAP_SYS_ADMIN" +result=$(run_in_userns "$ZONED_TEST_UID" \ + clone "$TESTPOOL/$TESTFS/deleg_root/child1@snap1" \ + "$TESTPOOL/$TESTFS/deleg_root/clone1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "clone should succeed with SYS_ADMIN" +fi +log_note "Clone passed with SYS_ADMIN" + +# Destroy with CAP_SYS_ADMIN (destructive tier) +log_note "Test: destroy with CAP_SYS_ADMIN" +result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/clone1" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "destroy should succeed with SYS_ADMIN" +fi +log_note "Destroy passed with SYS_ADMIN" + +log_pass "CAP_SYS_ADMIN satisfies L2 for all operation tiers" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_028_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_028_neg.ksh new file mode 100755 index 00000000000..f1dbed22d35 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_028_neg.ksh @@ -0,0 +1,103 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that the additive model does not affect non-zoned datasets. +# Standard ZFS permission checks (secpolicy_zfs → dsl_deleg) continue +# to work unchanged when zone_dataset_admin_check returns NOT_APPLICABLE. +# +# STRATEGY: +# 1. Create a dataset WITHOUT zoned_uid +# 2. From global zone as root: all operations succeed (existing behavior) +# 3. Grant permissions to a non-root user via zfs allow +# 4. As non-root user (not in namespace): operations succeed via dsl_deleg +# 5. Without zfs allow grant: operations fail +# 6. Verify zoned_uid model doesn't interfere +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/normal_ds" 2>/dev/null +} + +log_assert "Non-zoned datasets use standard permission model unchanged" +log_onexit cleanup + +# Create a normal dataset (no zoned_uid) +log_must zfs create "$TESTPOOL/$TESTFS/normal_ds" + +# Verify zoned_uid is 0 (unset) +typeset val +val=$(get_zoned_uid "$TESTPOOL/$TESTFS/normal_ds") +if [[ "$val" != "0" ]]; then + log_fail "Default zoned_uid should be 0, got: $val" +fi + +# EXIST-1: Root in global zone can do everything (existing behavior) +log_note "Test EXIST-1: root in global zone" +log_must zfs create "$TESTPOOL/$TESTFS/normal_ds/child" +log_must zfs snapshot "$TESTPOOL/$TESTFS/normal_ds/child@snap" +log_must zfs destroy "$TESTPOOL/$TESTFS/normal_ds/child@snap" +log_must zfs destroy "$TESTPOOL/$TESTFS/normal_ds/child" +log_note "EXIST-1 passed: root can do everything" + +# EXIST-2: Non-root with zfs allow can perform delegated operations +log_note "Test EXIST-2: non-root with zfs allow" +log_must grant_deleg "$TESTPOOL/$TESTFS/normal_ds" "$ZONED_TEST_UID" \ + "create,snapshot,mount,destroy" + +# Run as the test user (NOT in a namespace, just sudo -u) +typeset zfs_cmd result +zfs_cmd="$(which zfs)" +result=$(sudo -u \#"$ZONED_TEST_UID" "$zfs_cmd" \ + create "$TESTPOOL/$TESTFS/normal_ds/deleg_child" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "EXIST-2: non-root with zfs allow should be able to create" +fi +log_note "EXIST-2 passed: dsl_deleg works for non-root" + +# EXIST-3: Non-root WITHOUT zfs allow is denied +log_note "Test EXIST-3: non-root without zfs allow" +log_must revoke_deleg "$TESTPOOL/$TESTFS/normal_ds" "$ZONED_TEST_UID" + +result=$(sudo -u \#"$ZONED_TEST_UID" "$zfs_cmd" \ + create "$TESTPOOL/$TESTFS/normal_ds/denied_child" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "EXIST-3: non-root without zfs allow should be denied" +fi +log_note "EXIST-3 passed: denied without dsl_deleg" + +# Cleanup the child we created +log_must zfs destroy "$TESTPOOL/$TESTFS/normal_ds/deleg_child" + +log_pass "Non-zoned datasets use standard permission model unchanged" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_029_neg.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_029_neg.ksh new file mode 100755 index 00000000000..fa6ec3ce431 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_029_neg.ksh @@ -0,0 +1,120 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Cross-cutting constraints still apply under the additive model. +# Even with full dsl_deleg grants AND CAP_SYS_ADMIN, certain +# operations are always denied to protect the delegation boundary. +# +# STRATEGY: +# 1. Create delegation root with full grants + CAP_SYS_ADMIN +# 2. CROSS-1: Cannot destroy delegation root itself +# 3. CROSS-2: Cannot rename dataset outside delegation subtree +# 4. CROSS-3: Cannot modify zoned_uid property from namespace +# 5. CROSS-4: Cannot override admin-set limits on delegation root +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null + zfs destroy -rf "$TESTPOOL/$TESTFS/outside" 2>/dev/null +} + +log_assert "Cross-cutting constraints enforced under additive model" +log_onexit cleanup + +# Setup: full permissions +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" +log_must zfs create "$TESTPOOL/$TESTFS/outside" +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,destroy,snapshot,rename,clone,mount" +log_must zfs set filesystem_limit=10 "$TESTPOOL/$TESTFS/deleg_root" +log_must zfs set snapshot_limit=5 "$TESTPOOL/$TESTFS/deleg_root" + +# CROSS-1: Cannot destroy the delegation root itself +log_note "Test CROSS-1: destroy delegation root" +run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root" >/dev/null 2>&1 +if [[ $? -eq 0 ]]; then + log_fail "CROSS-1: should not be able to destroy delegation root" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root" +log_note "CROSS-1 passed: delegation root protected" + +# CROSS-2: Cannot rename outside delegation subtree +log_note "Test CROSS-2: rename outside subtree" +run_in_userns "$ZONED_TEST_UID" \ + rename "$TESTPOOL/$TESTFS/deleg_root/child" \ + "$TESTPOOL/$TESTFS/outside/escaped" >/dev/null 2>&1 +if [[ $? -eq 0 ]]; then + log_fail "CROSS-2: should not be able to rename outside subtree" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child" +log_note "CROSS-2 passed: cannot escape delegation" + +# CROSS-3: Cannot modify zoned_uid from namespace +log_note "Test CROSS-3: set zoned_uid from namespace" +run_in_userns "$ZONED_TEST_UID" \ + set zoned_uid=0 "$TESTPOOL/$TESTFS/deleg_root" >/dev/null 2>&1 +if [[ $? -eq 0 ]]; then + log_fail "CROSS-3: should not be able to modify zoned_uid" +fi +typeset val +val=$(get_zoned_uid "$TESTPOOL/$TESTFS/deleg_root") +if [[ "$val" != "$ZONED_TEST_UID" ]]; then + log_fail "CROSS-3: zoned_uid changed from $ZONED_TEST_UID to $val" +fi +log_note "CROSS-3 passed: zoned_uid protected" + +# CROSS-4: Cannot override admin limits on delegation root +log_note "Test CROSS-4: override filesystem_limit on root" +run_in_userns "$ZONED_TEST_UID" \ + set filesystem_limit=none "$TESTPOOL/$TESTFS/deleg_root" >/dev/null 2>&1 +if [[ $? -eq 0 ]]; then + log_fail "CROSS-4: should not be able to remove admin limits" +fi +typeset fs_limit +fs_limit=$(get_prop filesystem_limit "$TESTPOOL/$TESTFS/deleg_root") +if [[ "$fs_limit" != "10" ]]; then + log_fail "CROSS-4: filesystem_limit changed to $fs_limit" +fi + +run_in_userns "$ZONED_TEST_UID" \ + set snapshot_limit=none "$TESTPOOL/$TESTFS/deleg_root" >/dev/null 2>&1 +if [[ $? -eq 0 ]]; then + log_fail "CROSS-4: should not be able to remove snapshot_limit" +fi +log_note "CROSS-4 passed: admin limits protected" + +log_pass "Cross-cutting constraints enforced under additive model" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_030_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_030_pos.ksh new file mode 100755 index 00000000000..8536b36e294 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_030_pos.ksh @@ -0,0 +1,183 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Validate that the capability control mechanism (capsh --drop within +# unshare --user --map-root-user) works correctly. This is a +# prerequisite for all L2 capability-tier tests (023-027). +# +# The kernel's ns_capable() checks the effective capability set +# within the user namespace. capsh --drop removes capabilities +# from the bounding set, and the exec'd child process inherits +# the restricted set. +# +# STRATEGY: +# 1. Verify capsh is available +# 2. Full caps (all): CAP_SYS_ADMIN present, CAP_FOWNER present +# 3. Drop SYS_ADMIN only: CAP_SYS_ADMIN absent, CAP_FOWNER present +# 4. Drop all: CAP_SYS_ADMIN absent, CAP_FOWNER absent +# 5. Verify /proc/self/status CapEff reflects the drops +# 6. Verify drops work under sudo -u (as test UID) +# + +verify_runnable "global" + +log_assert "Capability control via capsh works in user namespaces" + +typeset capsh_cmd +capsh_cmd="$(which capsh)" +if [[ -z "$capsh_cmd" ]]; then + log_unsupported "capsh not found (install libcap)" +fi + +# Helper: check a capability in a namespace +function check_cap_in_ns +{ + typeset drop_arg=$1 + typeset cap_to_check=$2 + typeset expect=$3 # "yes" or "no" + + typeset result cmd_args + if [[ "$drop_arg" == "none" ]]; then + cmd_args="" + else + cmd_args="$drop_arg" + fi + + if [[ -z "$cmd_args" ]]; then + result=$(unshare --user --map-root-user \ + "$capsh_cmd" --has-p="$cap_to_check" 2>&1 \ + && echo "YES" || echo "NO") + else + # shellcheck disable=SC2086 + result=$(unshare --user --map-root-user \ + "$capsh_cmd" $cmd_args -- \ + -c "$capsh_cmd --has-p=$cap_to_check 2>&1 && echo YES || echo NO") + fi + + if [[ "$expect" == "yes" && "$result" != *"YES"* ]]; then + log_fail "Expected $cap_to_check to be present ($drop_arg), got: $result" + fi + if [[ "$expect" == "no" && "$result" != *"NO"* ]]; then + log_fail "Expected $cap_to_check to be absent ($drop_arg), got: $result" + fi +} + +# Test 1: Full caps — both present +log_note "Test 1: full caps in namespace" +check_cap_in_ns "none" "cap_sys_admin" "yes" +check_cap_in_ns "none" "cap_fowner" "yes" +log_note "Test 1 passed" + +# Test 2: Drop SYS_ADMIN — SYS_ADMIN absent, FOWNER present +log_note "Test 2: drop cap_sys_admin" +check_cap_in_ns "--drop=cap_sys_admin" "cap_sys_admin" "no" +check_cap_in_ns "--drop=cap_sys_admin" "cap_fowner" "yes" +log_note "Test 2 passed" + +# Test 3: Drop all — both absent +log_note "Test 3: drop all caps" +check_cap_in_ns "--drop=all" "cap_sys_admin" "no" +check_cap_in_ns "--drop=all" "cap_fowner" "no" +log_note "Test 3 passed" + +# Test 4: Verify via /proc/self/status CapEff bitmask +log_note "Test 4: verify CapEff bitmask" +typeset full_eff drop_eff +full_eff=$(unshare --user --map-root-user \ + grep CapEff /proc/self/status 2>&1 | awk '{print $2}') +drop_eff=$(unshare --user --map-root-user \ + "$capsh_cmd" --drop=cap_sys_admin -- \ + -c 'grep CapEff /proc/self/status' 2>&1 | awk '{print $2}') + +if [[ "$full_eff" == "$drop_eff" ]]; then + log_fail "CapEff should differ after dropping cap_sys_admin" +fi +log_note "CapEff full=$full_eff drop_sys_admin=$drop_eff" + +# CAP_SYS_ADMIN is bit 21 = 0x200000 +# The difference should be exactly this bit +typeset diff +diff=$(printf "0x%x" $(( 16#${full_eff} - 16#${drop_eff} ))) +if [[ "$diff" != "0x200000" ]]; then + log_note "Expected diff 0x200000 (CAP_SYS_ADMIN), got $diff" + log_note "This may indicate kernel cap numbering differs; non-fatal" +fi +log_note "Test 4 passed" + +# Test 5: Works under sudo -u (as test UID) +log_note "Test 5: capability drops work under sudo -u" +typeset result +result=$(sudo -u \#"$ZONED_TEST_UID" unshare --user --map-root-user \ + "$capsh_cmd" --drop=cap_sys_admin -- \ + -c "$capsh_cmd --has-p=cap_sys_admin 2>&1 && echo YES || echo NO" 2>&1) +if [[ "$result" != *"NO"* ]]; then + log_fail "cap_sys_admin should be absent under sudo -u, got: $result" +fi + +result=$(sudo -u \#"$ZONED_TEST_UID" unshare --user --map-root-user \ + "$capsh_cmd" --drop=cap_sys_admin -- \ + -c "$capsh_cmd --has-p=cap_fowner 2>&1 && echo YES || echo NO" 2>&1) +if [[ "$result" != *"YES"* ]]; then + log_fail "cap_fowner should be present under sudo -u, got: $result" +fi +log_note "Test 5 passed" + +# Test 6: Verify run_in_userns_caps helper modes work +log_note "Test 6: run_in_userns_caps helper verification" + +# "all" mode — should have SYS_ADMIN +result=$(run_in_userns_caps "$ZONED_TEST_UID" "all" \ + version 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "run_in_userns_caps 'all' should work" +fi +log_note "'all' mode works" + +# "drop_sys_admin" mode — zfs version should still work (read-only) +result=$(run_in_userns_caps "$ZONED_TEST_UID" "drop_sys_admin" \ + version 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "run_in_userns_caps 'drop_sys_admin' should work for read-only" +fi +log_note "'drop_sys_admin' mode works" + +# "none" mode — zfs version should still work (read-only) +result=$(run_in_userns_caps "$ZONED_TEST_UID" "none" \ + version 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "run_in_userns_caps 'none' should work for read-only" +fi +log_note "'none' mode works" + +log_pass "Capability control via capsh works in user namespaces" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_031_pos.ksh b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_031_pos.ksh new file mode 100755 index 00000000000..fe0fb2ab024 --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_031_pos.ksh @@ -0,0 +1,110 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid_common.kshlib + +# +# DESCRIPTION: +# Verify that namespace-initiated rename+destroy properly cleans up +# kernel-side zone tracking entries. When a namespace user renames +# a dataset, a tracking entry is created for the new name. When +# the renamed dataset is subsequently destroyed, that tracking entry +# must be removed. If it persists (stale), the delegation root +# remains visible as a parent dataset even after the admin removes +# the zoned_uid delegation — an information leak. +# +# STRATEGY: +# 1. Create delegation root with zoned_uid, grant permissions +# 2. From namespace: rename child → child2 (creates tracking entry) +# 3. From namespace: destroy child2 (should clean up tracking entry) +# 4. Admin removes zoned_uid delegation (zfs set zoned_uid=0) +# 5. Verify delegation root is NOT visible from the old namespace +# (if stale tracking persists, it would still be visible) +# + +verify_runnable "global" + +function cleanup +{ + zfs destroy -rf "$TESTPOOL/$TESTFS/deleg_root" 2>/dev/null +} + +log_assert "Zone tracking cleanup after namespace rename+destroy" +log_onexit cleanup + +# Setup: delegation root with child, mountpoint=none to avoid mount-lock issues +log_must zfs create -o mountpoint=none "$TESTPOOL/$TESTFS/deleg_root" +log_must set_zoned_uid "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" +log_must zfs create "$TESTPOOL/$TESTFS/deleg_root/child" + +# Grant all needed permissions +log_must grant_deleg "$TESTPOOL/$TESTFS/deleg_root" "$ZONED_TEST_UID" \ + "create,destroy,rename,mount" + +# Step 1: From namespace, rename child → child2 +# This internally calls zone_dataset_attach_uid for the new name +log_note "Test: rename child from namespace" +typeset result +result=$(run_in_userns "$ZONED_TEST_UID" \ + rename "$TESTPOOL/$TESTFS/deleg_root/child" \ + "$TESTPOOL/$TESTFS/deleg_root/child2" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "rename should succeed from namespace" +fi +log_must zfs list "$TESTPOOL/$TESTFS/deleg_root/child2" +log_note "Rename succeeded" + +# Step 2: From namespace, destroy child2 +# This should clean up the tracking entry created by rename +log_note "Test: destroy renamed child from namespace" +result=$(run_in_userns "$ZONED_TEST_UID" \ + destroy "$TESTPOOL/$TESTFS/deleg_root/child2" 2>&1) +if [[ $? -ne 0 ]]; then + log_note "Output: $result" + log_fail "destroy should succeed from namespace" +fi +log_note "Destroy succeeded" + +# Step 3: Admin removes the zoned_uid delegation +log_note "Test: admin removes zoned_uid delegation" +log_must zfs set zoned_uid=0 "$TESTPOOL/$TESTFS/deleg_root" + +# Step 4: Verify the delegation root is NOT visible from the old namespace. +# If the tracking entry from the rename was not cleaned up (stale), +# the delegation root would still be visible as a parent of the stale +# entry, leaking its existence after delegation was revoked. +log_note "Test: verify no stale visibility after delegation removal" +result=$(run_in_userns "$ZONED_TEST_UID" \ + list "$TESTPOOL/$TESTFS/deleg_root" 2>&1) +if [[ $? -eq 0 ]]; then + log_fail "Delegation root should NOT be visible after " \ + "zoned_uid=0 (stale tracking entry detected)" +fi +log_note "No stale visibility: delegation root correctly hidden" + +log_pass "Zone tracking cleanup after namespace rename+destroy" diff --git a/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_common.kshlib b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_common.kshlib new file mode 100644 index 00000000000..a44a3f0cc7a --- /dev/null +++ b/tests/zfs-tests/tests/functional/zoned_uid/zoned_uid_common.kshlib @@ -0,0 +1,237 @@ +# SPDX-License-Identifier: CDDL-1.0 +# +# 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 +# + +# +# Copyright 2026 Colin K. Williams / LINK ORG LLC / LI-NK.SOCIAL. All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/zoned_uid/zoned_uid.cfg + +# +# Check if the kernel supports zoned_uid property +# +function zoned_uid_supported +{ + zfs get zoned_uid "$TESTPOOL" >/dev/null 2>&1 + return $? +} + +# +# Get the zoned_uid property value for a dataset +# Use -p for parseable (raw numeric) output +# +function get_zoned_uid +{ + typeset dataset=$1 + get_prop zoned_uid "$dataset" +} + +# +# Set the zoned_uid property on a dataset +# +function set_zoned_uid +{ + typeset dataset=$1 + typeset uid=$2 + zfs set zoned_uid="$uid" "$dataset" +} + +# +# Clear the zoned_uid property (set to 0/none) +# +function clear_zoned_uid +{ + typeset dataset=$1 + zfs set zoned_uid=0 "$dataset" +} + +# +# Run a ZFS command inside a user namespace owned by the given UID. +# Uses absolute path to zfs so the binary is found regardless of the +# target user's PATH (e.g. when running from a source build). +# +# The namespace gets CAP_SYS_ADMIN via --map-root-user (default behavior, +# equivalent to a container launched with --cap-add SYS_ADMIN). +# +# Usage: run_in_userns +# Output is captured to stdout/stderr; return code is preserved. +# +function run_in_userns +{ + typeset uid=$1 + shift + typeset zfs_cmd + zfs_cmd="$(which zfs)" + + sudo -u \#"${uid}" unshare --user --mount --map-root-user \ + "$zfs_cmd" "$@" +} + +# +# Run a ZFS command inside a user namespace with specific capabilities. +# Uses capsh --drop to remove capabilities from the bounding set after +# creating the namespace via unshare --map-root-user. The exec'd shell +# (via capsh -- -c) inherits the restricted bounding set, so the ZFS +# binary sees only the kept capabilities in effective/permitted. +# +# Usage: run_in_userns_caps +# cap_spec: +# "all" — keep all caps (same as run_in_userns) +# "none" — drop all capabilities +# "drop_sys_admin" — drop only CAP_SYS_ADMIN (keep FOWNER etc.) +# "cap_fowner" — keep only CAP_FOWNER (drop everything else) +# +function run_in_userns_caps +{ + typeset uid=$1 + typeset cap_spec=$2 + shift 2 + typeset zfs_cmd capsh_cmd + zfs_cmd="$(which zfs)" + capsh_cmd="$(which capsh)" + + if [[ "$cap_spec" == "all" ]]; then + sudo -u \#"${uid}" unshare --user --mount --map-root-user \ + "$zfs_cmd" "$@" + return $? + fi + + if [[ "$cap_spec" == "none" ]]; then + # Drop every capability from the bounding set + sudo -u \#"${uid}" unshare --user --mount --map-root-user \ + "$capsh_cmd" --drop=all -- -c "$zfs_cmd $*" + return $? + fi + + if [[ "$cap_spec" == "drop_sys_admin" ]]; then + # Drop only CAP_SYS_ADMIN; all other caps (including + # CAP_FOWNER) remain. This simulates a default Podman + # container (default caps, no --cap-add SYS_ADMIN). + sudo -u \#"${uid}" unshare --user --mount --map-root-user \ + "$capsh_cmd" --drop=cap_sys_admin -- -c "$zfs_cmd $*" + return $? + fi + + # Generic: drop all caps except the ones listed. + # Build the drop list by enumerating all caps and excluding those + # the caller wants to keep. + typeset all_caps drop_list="" + all_caps=$("$capsh_cmd" --print 2>/dev/null | \ + grep "^Bounding set" | sed 's/.*=//;s/,/ /g') + if [[ -z "$all_caps" ]]; then + log_fail "capsh --print failed to enumerate capabilities" + fi + for cap in $all_caps; do + typeset keep=false + typeset IFS="," + for want in $cap_spec; do + if [[ "$cap" == "$want" ]]; then + keep=true + break + fi + done + unset IFS + if [[ "$keep" == "false" ]]; then + drop_list="$drop_list --drop=$cap" + fi + done + + # shellcheck disable=SC2086 + sudo -u \#"${uid}" unshare --user --mount --map-root-user \ + "$capsh_cmd" $drop_list -- -c "$zfs_cmd $*" +} + +# +# Verify that capability control via capsh works in user namespaces. +# Returns 0 if the mechanism is functional, non-zero otherwise. +# This should be called in setup.ksh to skip tests if capsh is broken. +# +function verify_capsh_works +{ + typeset capsh_cmd + capsh_cmd="$(which capsh)" + if [[ -z "$capsh_cmd" ]]; then + return 1 + fi + + # Test 1: after --drop=cap_sys_admin, cap should be absent + typeset result + result=$(unshare --user --map-root-user \ + "$capsh_cmd" --drop=cap_sys_admin -- \ + -c "$capsh_cmd --has-p=cap_sys_admin 2>&1 && echo YES || echo NO") + if [[ "$result" != *"NO"* ]]; then + return 1 + fi + + # Test 2: cap_fowner should still be present + result=$(unshare --user --map-root-user \ + "$capsh_cmd" --drop=cap_sys_admin -- \ + -c "$capsh_cmd --has-p=cap_fowner 2>&1 && echo YES || echo NO") + if [[ "$result" != *"YES"* ]]; then + return 1 + fi + + # Test 3: --drop=all should remove everything + result=$(unshare --user --map-root-user \ + "$capsh_cmd" --drop=all -- \ + -c "$capsh_cmd --has-p=cap_fowner 2>&1 && echo YES || echo NO") + if [[ "$result" != *"NO"* ]]; then + return 1 + fi + + return 0 +} + +# +# Grant delegated permissions to a user on a dataset. +# Wrapper around zfs allow. +# +# Usage: grant_deleg +# perms: comma-separated list, e.g. "create,snapshot,mount" +# +function grant_deleg +{ + typeset dataset=$1 + typeset uid=$2 + typeset perms=$3 + zfs allow -u "$uid" "$perms" "$dataset" +} + +# +# Revoke delegated permissions from a user on a dataset. +# Wrapper around zfs unallow. +# +# Usage: revoke_deleg [perms] +# perms: optional comma-separated list; if omitted, revokes all +# +function revoke_deleg +{ + typeset dataset=$1 + typeset uid=$2 + typeset perms=${3:-} + if [[ -n "$perms" ]]; then + zfs unallow -u "$uid" "$perms" "$dataset" + else + zfs unallow -u "$uid" "$dataset" + fi +}