From 5e5253be84b60ca8a519c48c421407f905c6d4c8 Mon Sep 17 00:00:00 2001 From: Alexander Motin Date: Thu, 19 Jun 2025 17:39:20 -0400 Subject: [PATCH] FreeBSD: Wire projects support While FreeBSD itself does not support projects, there is no reason why it can't be controlled via `zfs project` and other subcommands. Most of the code is actually already there and just needs some revival and sync with Linux, plus enabling some tests not depending on the OS support. Reviewed-by: Ameer Hamza Reviewed-by: Brian Behlendorf Signed-off-by: Alexander Motin Sponsored by: iXsystems, Inc. Closes #17423 --- module/os/freebsd/zfs/zfs_dir.c | 2 +- module/os/freebsd/zfs/zfs_vnops_os.c | 219 ++++++++++++++++++++++++++- module/os/freebsd/zfs/zfs_znode_os.c | 40 +++++ module/os/linux/zfs/zfs_znode_os.c | 2 +- tests/runfiles/common.run | 9 ++ tests/runfiles/linux.run | 12 +- 6 files changed, 271 insertions(+), 13 deletions(-) diff --git a/module/os/freebsd/zfs/zfs_dir.c b/module/os/freebsd/zfs/zfs_dir.c index a0c9dd178..191df832d 100644 --- a/module/os/freebsd/zfs/zfs_dir.c +++ b/module/os/freebsd/zfs/zfs_dir.c @@ -833,7 +833,7 @@ zfs_make_xattrdir(znode_t *zp, vattr_t *vap, znode_t **xvpp, cred_t *cr) if ((error = zfs_acl_ids_create(zp, IS_XATTR, vap, cr, NULL, &acl_ids, NULL)) != 0) return (error); - if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, 0)) { + if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, zp->z_projid)) { zfs_acl_ids_free(&acl_ids); return (SET_ERROR(EDQUOT)); } diff --git a/module/os/freebsd/zfs/zfs_vnops_os.c b/module/os/freebsd/zfs/zfs_vnops_os.c index e183d1ec7..f53c94d3a 100644 --- a/module/os/freebsd/zfs/zfs_vnops_os.c +++ b/module/os/freebsd/zfs/zfs_vnops_os.c @@ -75,6 +75,7 @@ #include #include #include +#include #include #include #include @@ -267,6 +268,71 @@ zfs_close(vnode_t *vp, int flag, int count, offset_t offset, cred_t *cr) return (0); } +static int +zfs_ioctl_getxattr(vnode_t *vp, zfsxattr_t *fsx) +{ + znode_t *zp = VTOZ(vp); + + memset(fsx, 0, sizeof (*fsx)); + fsx->fsx_xflags = (zp->z_pflags & ZFS_PROJINHERIT) ? + ZFS_PROJINHERIT_FL : 0; + fsx->fsx_projid = zp->z_projid; + + return (0); +} + +static int +zfs_ioctl_setflags(vnode_t *vp, uint32_t ioctl_flags, xvattr_t *xva) +{ + uint64_t zfs_flags = VTOZ(vp)->z_pflags; + xoptattr_t *xoap; + + if (ioctl_flags & ~(ZFS_PROJINHERIT_FL)) + return (SET_ERROR(EOPNOTSUPP)); + + xva_init(xva); + xoap = xva_getxoptattr(xva); + +#define FLAG_CHANGE(iflag, zflag, xflag, xfield) do { \ + if (((ioctl_flags & (iflag)) && !(zfs_flags & (zflag))) || \ + ((zfs_flags & (zflag)) && !(ioctl_flags & (iflag)))) { \ + XVA_SET_REQ(xva, (xflag)); \ + (xfield) = ((ioctl_flags & (iflag)) != 0); \ + } \ +} while (0) + + FLAG_CHANGE(ZFS_PROJINHERIT_FL, ZFS_PROJINHERIT, XAT_PROJINHERIT, + xoap->xoa_projinherit); + +#undef FLAG_CHANGE + + return (0); +} + +static int +zfs_ioctl_setxattr(vnode_t *vp, zfsxattr_t *fsx, cred_t *cr) +{ + znode_t *zp = VTOZ(vp); + xvattr_t xva; + xoptattr_t *xoap; + int err; + + if (!zpl_is_valid_projid(fsx->fsx_projid)) + return (SET_ERROR(EINVAL)); + + err = zfs_ioctl_setflags(vp, fsx->fsx_xflags, &xva); + if (err) + return (err); + + xoap = xva_getxoptattr(&xva); + XVA_SET_REQ(&xva, XAT_PROJID); + xoap->xoa_projid = fsx->fsx_projid; + + err = zfs_setattr(zp, (vattr_t *)&xva, 0, cr, NULL); + + return (err); +} + static int zfs_ioctl(vnode_t *vp, ulong_t com, intptr_t data, int flag, cred_t *cred, int *rvalp) @@ -306,6 +372,24 @@ zfs_ioctl(vnode_t *vp, ulong_t com, intptr_t data, int flag, cred_t *cred, *(offset_t *)data = off; return (0); } + case ZFS_IOC_FSGETXATTR: { + zfsxattr_t *fsx = (zfsxattr_t *)data; + error = vn_lock(vp, LK_SHARED); + if (error) + return (error); + error = zfs_ioctl_getxattr(vp, fsx); + VOP_UNLOCK(vp); + return (error); + } + case ZFS_IOC_FSSETXATTR: { + zfsxattr_t *fsx = (zfsxattr_t *)data; + error = vn_lock(vp, LK_EXCLUSIVE); + if (error) + return (error); + error = zfs_ioctl_setxattr(vp, fsx, cred); + VOP_UNLOCK(vp); + return (error); + } case ZFS_IOC_REWRITE: { zfs_rewrite_args_t *args = (zfs_rewrite_args_t *)data; if ((flag & FWRITE) == 0) @@ -2058,6 +2142,132 @@ zfs_getattr(vnode_t *vp, vattr_t *vap, int flags, cred_t *cr) return (0); } +/* + * For the operation of changing file's user/group/project, we need to + * handle not only the main object that is assigned to the file directly, + * but also the ones that are used by the file via hidden xattr directory. + * + * Because the xattr directory may contains many EA entries, as to it may + * be impossible to change all of them via the transaction of changing the + * main object's user/group/project attributes. Then we have to change them + * via other multiple independent transactions one by one. It may be not good + * solution, but we have no better idea yet. + */ +static int +zfs_setattr_dir(znode_t *dzp) +{ + zfsvfs_t *zfsvfs = dzp->z_zfsvfs; + objset_t *os = zfsvfs->z_os; + zap_cursor_t zc; + zap_attribute_t *zap; + znode_t *zp = NULL; + dmu_tx_t *tx = NULL; + uint64_t uid, gid; + sa_bulk_attr_t bulk[4]; + int count; + int err; + + zap = zap_attribute_alloc(); + zap_cursor_init(&zc, os, dzp->z_id); + while ((err = zap_cursor_retrieve(&zc, zap)) == 0) { + count = 0; + if (zap->za_integer_length != 8 || zap->za_num_integers != 1) { + err = ENXIO; + break; + } + + err = zfs_dirent_lookup(dzp, zap->za_name, &zp, ZEXISTS); + if (err == ENOENT) + goto next; + if (err) + break; + + if (zp->z_uid == dzp->z_uid && + zp->z_gid == dzp->z_gid && + zp->z_projid == dzp->z_projid) + goto next; + + tx = dmu_tx_create(os); + if (!(zp->z_pflags & ZFS_PROJID)) + dmu_tx_hold_sa(tx, zp->z_sa_hdl, B_TRUE); + else + dmu_tx_hold_sa(tx, zp->z_sa_hdl, B_FALSE); + + err = dmu_tx_assign(tx, DMU_TX_WAIT); + if (err) + break; + + mutex_enter(&dzp->z_lock); + + if (zp->z_uid != dzp->z_uid) { + uid = dzp->z_uid; + SA_ADD_BULK_ATTR(bulk, count, SA_ZPL_UID(zfsvfs), NULL, + &uid, sizeof (uid)); + zp->z_uid = uid; + } + + if (zp->z_gid != dzp->z_gid) { + gid = dzp->z_gid; + SA_ADD_BULK_ATTR(bulk, count, SA_ZPL_GID(zfsvfs), NULL, + &gid, sizeof (gid)); + zp->z_gid = gid; + } + + uint64_t projid = dzp->z_projid; + if (zp->z_projid != projid) { + if (!(zp->z_pflags & ZFS_PROJID)) { + err = sa_add_projid(zp->z_sa_hdl, tx, projid); + if (unlikely(err == EEXIST)) { + err = 0; + } else if (err != 0) { + goto sa_add_projid_err; + } else { + projid = ZFS_INVALID_PROJID; + } + } + + if (projid != ZFS_INVALID_PROJID) { + zp->z_projid = projid; + SA_ADD_BULK_ATTR(bulk, count, + SA_ZPL_PROJID(zfsvfs), NULL, &zp->z_projid, + sizeof (zp->z_projid)); + } + } + +sa_add_projid_err: + mutex_exit(&dzp->z_lock); + + if (likely(count > 0)) { + err = sa_bulk_update(zp->z_sa_hdl, bulk, count, tx); + dmu_tx_commit(tx); + } else if (projid == ZFS_INVALID_PROJID) { + dmu_tx_commit(tx); + } else { + dmu_tx_abort(tx); + } + tx = NULL; + if (err != 0 && err != ENOENT) + break; + +next: + if (zp) { + zrele(zp); + zp = NULL; + } + zap_cursor_advance(&zc); + } + + if (tx) + dmu_tx_abort(tx); + if (zp) { + zrele(zp); + } + zap_cursor_fini(&zc); + zap_attribute_free(zap); + + return (err == ENOENT ? 0 : err); +} + /* * Set the file attributes to the values contained in the * vattr structure. @@ -2103,6 +2313,7 @@ zfs_setattr(znode_t *zp, vattr_t *vap, int flags, cred_t *cr, zidmap_t *mnt_ns) zfs_acl_t *aclp; boolean_t skipaclchk = (flags & ATTR_NOACLCHECK) ? B_TRUE : B_FALSE; boolean_t fuid_dirtied = B_FALSE; + boolean_t handle_eadir = B_FALSE; sa_bulk_attr_t bulk[7], xattr_bulk[7]; int count = 0, xattr_count = 0; @@ -2454,6 +2665,7 @@ zfs_setattr(znode_t *zp, vattr_t *vap, int flags, cred_t *cr, zidmap_t *mnt_ns) mask = vap->va_mask; if ((mask & (AT_UID | AT_GID)) || projid != ZFS_INVALID_PROJID) { + handle_eadir = B_TRUE; err = sa_lookup(zp->z_sa_hdl, SA_ZPL_XATTR(zfsvfs), &xattr_obj, sizeof (xattr_obj)); @@ -2783,6 +2995,10 @@ out: } else { err2 = sa_bulk_update(zp->z_sa_hdl, bulk, count, tx); dmu_tx_commit(tx); + if (attrzp) { + if (err2 == 0 && handle_eadir) + err = zfs_setattr_dir(attrzp); + } } out2: @@ -3439,8 +3655,7 @@ zfs_symlink(znode_t *dzp, const char *name, vattr_t *vap, return (error); } - if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, - 0 /* projid */)) { + if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, ZFS_DEFAULT_PROJID)) { zfs_acl_ids_free(&acl_ids); zfs_exit(zfsvfs, FTAG); return (SET_ERROR(EDQUOT)); diff --git a/module/os/freebsd/zfs/zfs_znode_os.c b/module/os/freebsd/zfs/zfs_znode_os.c index e97a0dd84..a720f53b0 100644 --- a/module/os/freebsd/zfs/zfs_znode_os.c +++ b/module/os/freebsd/zfs/zfs_znode_os.c @@ -564,6 +564,7 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr, uint64_t crtime[2], atime[2], mtime[2], ctime[2]; uint64_t mode, size, links, parent, pflags; uint64_t dzp_pflags = 0; + uint64_t projid = ZFS_DEFAULT_PROJID; uint64_t rdev = 0; zfsvfs_t *zfsvfs = dzp->z_zfsvfs; dmu_buf_t *db; @@ -671,6 +672,23 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr, if (flag & IS_XATTR) pflags |= ZFS_XATTR; + if (vap->va_type == VREG || vap->va_type == VDIR) { + /* + * With ZFS_PROJID flag, we can easily know whether there is + * project ID stored on disk or not. See zpl_get_file_info(). + */ + if (obj_type != DMU_OT_ZNODE && + dmu_objset_projectquota_enabled(zfsvfs->z_os)) + pflags |= ZFS_PROJID; + + /* + * Inherit project ID from parent if required. + */ + projid = zfs_inherit_projid(dzp); + if (dzp_pflags & ZFS_PROJINHERIT) + pflags |= ZFS_PROJINHERIT; + } + /* * No execs denied will be determined when zfs_mode_compute() is called. */ @@ -752,6 +770,10 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr, if (obj_type == DMU_OT_ZNODE) { SA_ADD_BULK_ATTR(sa_attrs, cnt, SA_ZPL_XATTR(zfsvfs), NULL, &empty_xattr, 8); + } else if (dmu_objset_projectquota_enabled(zfsvfs->z_os) && + pflags & ZFS_PROJID) { + SA_ADD_BULK_ATTR(sa_attrs, cnt, SA_ZPL_PROJID(zfsvfs), + NULL, &projid, 8); } if (obj_type == DMU_OT_ZNODE || (vap->va_type == VBLK || vap->va_type == VCHR)) { @@ -799,6 +821,7 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr, (*zpp)->z_pflags = pflags; (*zpp)->z_mode = mode; (*zpp)->z_dnodesize = dnodesize; + (*zpp)->z_projid = projid; if (vap->va_mask & AT_XVATTR) zfs_xvattr_set(*zpp, (xvattr_t *)vap, tx); @@ -916,6 +939,11 @@ zfs_xvattr_set(znode_t *zp, xvattr_t *xvap, dmu_tx_t *tx) zp->z_pflags, tx); XVA_SET_RTN(xvap, XAT_SPARSE); } + if (XVA_ISSET_REQ(xvap, XAT_PROJINHERIT)) { + ZFS_ATTR_SET(zp, ZFS_PROJINHERIT, xoap->xoa_projinherit, + zp->z_pflags, tx); + XVA_SET_RTN(xvap, XAT_PROJINHERIT); + } } int @@ -1068,6 +1096,7 @@ zfs_rezget(znode_t *zp) int err; int count = 0; uint64_t gen; + uint64_t projid = ZFS_DEFAULT_PROJID; /* * Remove cached pages before reloading the znode, so that they are not @@ -1148,6 +1177,17 @@ zfs_rezget(znode_t *zp) return (SET_ERROR(EIO)); } + if (dmu_objset_projectquota_enabled(zfsvfs->z_os)) { + err = sa_lookup(zp->z_sa_hdl, SA_ZPL_PROJID(zfsvfs), + &projid, 8); + if (err != 0 && err != ENOENT) { + zfs_znode_dmu_fini(zp); + ZFS_OBJ_HOLD_EXIT(zfsvfs, obj_num); + return (err); + } + } + + zp->z_projid = projid; zp->z_mode = mode; if (gen != zp->z_gen) { diff --git a/module/os/linux/zfs/zfs_znode_os.c b/module/os/linux/zfs/zfs_znode_os.c index 607b3995c..54e60b482 100644 --- a/module/os/linux/zfs/zfs_znode_os.c +++ b/module/os/linux/zfs/zfs_znode_os.c @@ -768,7 +768,7 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr, if (S_ISREG(vap->va_mode) || S_ISDIR(vap->va_mode)) { /* * With ZFS_PROJID flag, we can easily know whether there is - * project ID stored on disk or not. See zfs_space_delta_cb(). + * project ID stored on disk or not. See zpl_get_file_info(). */ if (obj_type != DMU_OT_ZNODE && dmu_objset_projectquota_enabled(zfsvfs->z_os)) diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 144b228bc..7969945a4 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -846,6 +846,15 @@ pre = post = tags = ['functional', 'pool_names'] +[tests/functional/projectquota] +tests = ['defaultprojectquota_002_pos', 'defaultprojectquota_003_neg', + 'defaultprojectquota_004_pos', 'defaultprojectquota_006_pos', + 'defaultprojectquota_007_pos', 'projectquota_002_pos', + 'projectquota_004_neg', 'projectquota_005_pos', 'projectquota_007_pos', + 'projectquota_008_pos', 'projectquota_009_pos', 'projecttree_002_pos', + 'projecttree_003_neg'] +tags = ['functional', 'projectquota'] + [tests/functional/poolversion] tests = ['poolversion_001_pos', 'poolversion_002_pos'] tags = ['functional', 'poolversion'] diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index b0c3364f6..617fe573c 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -175,17 +175,11 @@ tests = ['procfs_list_basic', 'procfs_list_concurrent_readers', tags = ['functional', 'procfs'] [tests/functional/projectquota:Linux] -tests = ['defaultprojectquota_001_pos', 'defaultprojectquota_002_pos', - 'defaultprojectquota_003_neg', 'defaultprojectquota_004_pos', - 'defaultprojectquota_005_pos', 'defaultprojectquota_006_pos', - 'defaultprojectquota_007_pos', +tests = ['defaultprojectquota_001_pos', 'defaultprojectquota_005_pos', 'projectid_001_pos', 'projectid_002_pos', 'projectid_003_pos', - 'projectquota_001_pos', 'projectquota_002_pos', 'projectquota_003_pos', - 'projectquota_004_neg', 'projectquota_005_pos', 'projectquota_006_pos', - 'projectquota_007_pos', 'projectquota_008_pos', 'projectquota_009_pos', + 'projectquota_001_pos', 'projectquota_003_pos', 'projectquota_006_pos', 'projectspace_001_pos', 'projectspace_002_pos', 'projectspace_003_pos', - 'projectspace_004_pos', 'projectspace_005_pos', - 'projecttree_001_pos', 'projecttree_002_pos', 'projecttree_003_neg'] + 'projectspace_004_pos', 'projectspace_005_pos', 'projecttree_001_pos'] tags = ['functional', 'projectquota'] [tests/functional/dos_attributes:Linux]