[RFC PATCH v2 17/41] quota: don't account UUID and GUID owners
sashiko-bot at kernel.org
sashiko-bot at kernel.org
Thu Oct 22 03:17:41 UTC 2026
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] quota: dqget() checks qid_has_mapping() before qid_is_legacy()
- [High] quota: stale dquot retention in dquot_transfer() for UUID and GUID owners
- [Medium] quota: dquot_initialize_needed() stays true for UUID and GUID owners
--
commit 56b2ebaac3515c69a008f64fd7b2eb53fda044a8
Author: Jan Koester <jan.koester at tuxist.de>
quota: don't account UUID and GUID owners
This commit adds a qid_is_legacy() helper and updates dqget() to return
-ESRCH for UUID and GUID owners so that non-legacy 128-bit identifiers are
not accounted in 32-bit quota files.
> diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c
> index 1c78c695d0ddf..62f333f8243f1 100644
> --- a/fs/quota/dquot.c
> +++ b/fs/quota/dquot.c
> @@ -954,6 +954,9 @@ struct dquot *dqget(struct super_block *sb, struct kqid qid)
>
> if (!qid_has_mapping(sb->s_user_ns, qid))
> return ERR_PTR(-EINVAL);
> + /* Quota files hold 32 bit ids: like quota off for UUIDs and GUIDs */
> + if (!qid_is_legacy(qid))
> + return ERR_PTR(-ESRCH);
>
> if (!sb_has_quota_active(sb, qid.type))
> return ERR_PTR(-ESRCH);
[Severity: High]
Can the !qid_is_legacy(qid) check here ever be reached for a UUID or GUID
owner?
Prior to this check, dqget() calls qid_has_mapping(sb->s_user_ns, qid):
include/linux/quota.h:qid_has_mapping()
return from_kqid(ns, qid) != (qid_t) -1;
from_kqid() delegates to from_kuid() and from_kgid(), which unconditionally
return (uid_t)-1 and (gid_t)-1 for non-legacy identifiers even in
init_user_ns:
kernel/user_namespace.c:from_kuid()
if (!uid_is_legacy(kuid))
return (uid_t) -1;
Because qid_has_mapping() always returns false for UUID and GUID owners,
dqget() returns ERR_PTR(-EINVAL) before reaching !qid_is_legacy(qid).
Callers such as __dquot_initialize() and dquot_transfer() only treat -ESRCH
as non-fatal and propagate -EINVAL, causing file creation, open for write,
and ownership changes involving UUID or GUID owners to fail on
quota-enabled filesystems.
Should the !qid_is_legacy(qid) check be placed before qid_has_mapping()?
[Severity: High]
When an inode is chowned from a legacy 32-bit owner to a UUID or GUID owner,
will leaving the inode charged to the previous owner in dquot_transfer()
leak quota accounting once the inode is evicted from the icache?
In dquot_transfer() -> __dquot_transfer(), returning -ESRCH from dqget()
sets transfer_to[cnt] = NULL:
fs/quota/dquot.c:__dquot_transfer()
for (cnt = 0; cnt < MAXQUOTAS; cnt++) {
/*
* Skip changes for same uid or gid or for turned off quota-type.
*/
if (!transfer_to[cnt])
continue;
This skips uncharging transfer_from[cnt] and leaves i_dquot(inode)[cnt]
pointing to the previous owner's dquot while inode->i_uid or inode->i_gid is
updated to the UUID or GUID.
While the inode remains cached, subsequent block allocations by the new
owner are still charged to the previous owner's dquot via
__dquot_alloc_space() and can fail with -EDQUOT against the previous
owner's limits.
Once the inode is evicted from memory, dquot_drop() -> __dquot_drop() drops
the in-memory i_dquot(inode)[cnt] reference without uncharging the quota.
When the inode is later re-read from disk, __dquot_initialize() sees the
UUID or GUID owner and leaves i_dquot(inode)[cnt] as NULL. Subsequent
truncation or unlink (__dquot_free_space(), dquot_free_inode()) will then
never decrement the previous owner's dquot, permanently leaking those blocks
and inodes in the on-disk quota file (or double-charging if chowned back to
a legacy owner).
Should __dquot_transfer() uncharge transfer_from[cnt] and clear dquots[cnt]
when transferring ownership to an unquoted UUID or GUID owner?
[Severity: Medium]
How does returning -ESRCH for UUID and GUID owners interact with
dquot_initialize_needed()?
When dqget() returns -ESRCH while sb_has_quota_active(sb, qid.type) is true,
__dquot_initialize() leaves i_dquot(inode)[cnt] as NULL. However,
dquot_initialize_needed() (as well as dqinit_needed()) assumes that any NULL
slot for an active quota type means the inode still needs quota
initialization:
fs/quota/dquot.c:dquot_initialize_needed()
dquots = i_dquot(inode);
for (i = 0; i < MAXQUOTAS; i++)
if (!dquots[i] && sb_has_quota_active(inode->i_sb, i))
return true;
For an inode with a UUID or GUID owner or group, dquot_initialize_needed()
remains permanently true even after dquot_initialize() has been called.
In ext4, this causes __ext4_expand_extra_isize() to always bail out with
-EAGAIN:
fs/ext4/inode.c:__ext4_expand_extra_isize()
if (dquot_initialize_needed(inode))
return -EAGAIN;
It also triggers WARN_ON_ONCE(dquot_initialize_needed(inode)) in
ext4_xattr_block_set() when EXT4_XATTR_DEBUG is enabled, and causes every
call to __dquot_initialize() on the inode to bypass the fast path and
acquire dq_data_lock.
Should dquot_initialize_needed(), dqinit_needed(), and the loop in
__dquot_initialize() also account for non-legacy identifiers?
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792552161.git.jan.koester@tuxist.de?part=17
More information about the Linux-security-module-archive
mailing list