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