Add zoned_uid property with additive least privilege authorization

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 <[email protected]>
Reviewed-by: Brian Behlendorf <[email protected]>
Signed-off-by: Colin K. Williams / LINK ORG LLC / li-nk.social <[email protected]>
Closes #18167
This commit is contained in:
li-nk.social authored and GitHub committed 2026-04-03 10:38:26 -07:00
1 parent 74504cf7fd
commit f203fedde8
55 files changed
+5238 -45

No files matched your search

+4
View File
@@ -105,6 +105,10 @@ zfs_errno = enum_with_offset(1024, [
'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',
],
{}
)
+1
View File
@@ -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;
+72
View File
@@ -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_ */
+58
View File
@@ -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);
+2
View File
@@ -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;
/*
+2 -1
View File
@@ -2292,7 +2292,8 @@
<enumerator name='ZFS_PROP_DEFAULTGROUPOBJQUOTA' value='104'/>
<enumerator name='ZFS_PROP_DEFAULTPROJECTOBJQUOTA' value='105'/>
<enumerator name='ZFS_PROP_SNAPSHOTS_CHANGED_NSECS' value='106'/>
<enumerator name='ZFS_NUM_PROPS' value='107'/>
<enumerator name='ZFS_PROP_ZONED_UID' value='107'/>
<enumerator name='ZFS_NUM_PROPS' value='108'/>
</enum-decl>
<typedef-decl name='zfs_prop_t' type-id='4b000d60' id='58603c44'/>
<enum-decl name='zprop_source_t' naming-typedef-id='a2256d42' id='5903f80e'>
+7 -3
View File
@@ -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 */
+6
View File
@@ -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);
+92
View File
@@ -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
+14 -1
View File
@@ -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 .
+385 -28
View File
@@ -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)) {
+48 -2
View File
@@ -39,8 +39,10 @@
#include <sys/dsl_prop.h>
#include <sys/fm/util.h>
#include <sys/dsl_scan.h>
#include <sys/dmu.h>
#include <sys/fs/zfs.h>
#include <sys/kstat.h>
#include <sys/zone.h>
#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
+4
View File
@@ -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);
}
+4
View File
@@ -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, "<uid> | 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,
+8 -5
View File
@@ -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;
+284 -5
View File
@@ -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();
+16
View File
@@ -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']
+2
View File
@@ -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],
})
+1
View File
@@ -129,6 +129,7 @@ export SYSTEM_FILES_LINUX='attr
blkid
blkdiscard
blockdev
capsh
chattr
cryptsetup
exportfs
+35
View File
@@ -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 \
+46
View File
@@ -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
+99
View File
@@ -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 #<uid>" 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
@@ -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}
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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)"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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 <uid> <zfs_args...>
# 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 <uid> <cap_spec> <zfs_args...>
# 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 <dataset> <uid> <perms>
# 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 <dataset> <uid> [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
}