β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2648

HAMMER2IOC_VERSION_GET and HAMMER2IOC_INODE_GET ignore the caps_priv_check result (privilege gate bypass)

Field Value
ID DF-2648
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N
CWE CWE-862 Missing Authorization
File sys/vfs/hammer2/hammer2_ioctl.c
Lines 86-88, 137-139
Area vfs
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

hammer2_ioctl() computes error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) (line 83) and gates nearly every command on it, but the VERSION_GET (:86-88) and INODE_GET (:137-139) cases overwrite error with the handler's return instead of checking it β€” the same class as known DF-0815, which listed only BULKFREE_SCAN/ASYNC and DEBUG_DUMP. Any user able to open a path on a hammer2 mount can invoke INODE_GET and read kernel-side inode meta (inum, size, quotas, data/inode counts) β€” stat(2)-equivalent data, so impact is hardening/consistency rather than disclosure of secrets.

Proof of concept

Verified on the stock guest as nobody (uid 65534) with findings/poc/DF-2648/inodeget_user.c: mount clean image, chmod a+rx /mnt/h2, su -m nobody -c '/tmp/inodeget_user /mnt/h2' β†’ INODE_GET: SUCCESS (ungated!) data_count=0 inode_count=0 inum=1 while PFS_GET: errno=1 (Operation not permitted) in the same process β€” the gate works everywhere else.

--- sys/vfs/hammer2/hammer2_ioctl.c
+++ sys/vfs/hammer2/hammer2_ioctl.c
@@ -86,7 +86,8 @@ hammer2_ioctl(hammer2_inode_t *ip, u_long com, void *data, int fflag,
    switch(com) {
    case HAMMER2IOC_VERSION_GET:
-       error = hammer2_ioctl_version_get(ip, data);
+       if (error == 0)
+           error = hammer2_ioctl_version_get(ip, data);
        break;
@@ -137,7 +138,8 @@
    case HAMMER2IOC_INODE_GET:
-       error = hammer2_ioctl_inode_get(ip, data);
+       if (error == 0)
+           error = hammer2_ioctl_inode_get(ip, data);
        break;

(Or, if these are intentionally unprivileged-by-design, document it at the dispatch site.)

References

  • DF-0815 (same class, other commands)

Timeline

  • 2026-08-29 Discovered during pass-2 audit of hammer2_ioctl.c (GLM 5.3); demonstrated live as nobody same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2648 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
inodeget_user.c β€” 2.1 KB view raw
run.log β€” 411 B view raw
env.txt β€” 189 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 1.8 KB view raw

DF-2648 β€” HAMMER2IOC_VERSION_GET / HAMMER2IOC_INODE_GET ignore the privilege check

What

hammer2_ioctl() (sys/vfs/hammer2/hammer2_ioctl.c:83) computes error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) and gates most commands on it, but two handlers overwrite the result instead of checking it:

  • hammer2_ioctl.c:86-88 β€” VERSION_GET: error = hammer2_ioctl_version_get(...)
  • hammer2_ioctl.c:137-139 β€” INODE_GET: error = hammer2_ioctl_inode_get(...)

(Same class as DF-0815, which covered BULKFREE_SCAN/ASYNC and DEBUG_DUMP; these two commands were not in that finding.)

Any user who can open a path on a hammer2 mount can therefore run HAMMER2IOC_INODE_GET and read the inode's kernel-side meta (hammer2_inode_data_t: inum, size, mode, uid/gid uuids, quotas, counts). The data is essentially stat(2)-equivalent, so the practical impact is low β€” this is a privilege-gating correctness bug (defense in depth, consistency with every other read-only-but-gated command such as PFS_GET/PFS_LOOKUP), not a disclosure of secret kernel state.

Reproduce (run on the stock guest, clean mounted image)

# vnconfig -c vn0 base2647.img && mount -t hammer2 /dev/vn0@testvol /mnt/h2
# chmod a+rx /mnt/h2
# su -m nobody -c "/tmp/inodeget_user /mnt/h2"
uid=65534 euid=65534 fd=3
INODE_GET: SUCCESS (ungated!) data_count=0 inode_count=0 inum=1
PFS_GET: errno=1 (Operation not permitted) -- gate works

The contrast line proves the gate exists and is enforced for other commands but ignored for INODE_GET.

Fix

Honor the check like the neighboring cases:

    case HAMMER2IOC_VERSION_GET:
-       error = hammer2_ioctl_version_get(ip, data);
+       if (error == 0)
+           error = hammer2_ioctl_version_get(ip, data);
        break;
    case HAMMER2IOC_INODE_GET:
-       error = hammer2_ioctl_inode_get(ip, data);
+       if (error == 0)
+           error = hammer2_ioctl_inode_get(ip, data);
        break;

(If the project prefers these to stay unprivileged-by-design, document it β€” but then hammer2(8) should not be the only expected caller.)

Fix verification

not_testable
per-fix-DF-2648

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: 'uid=65534 euid=65534 fd=3 / INODE_GET: SUCCESS (ungated!) data_count=0 inode_count=0 inum=1 / PFS_GET: errno=1 (Operation not permitted) -- gate works'", 'sys/vfs/hammer2/hammer2_ioctl.c:86-88 and 137-139: unconditional error overwrite vs gated neighbors']

PoC changes

none (self-contained PoC with mirrored struct layouts and _IOWR numbers from hammer2_ioctl.h)

Verified recommended fix

Gate both commands on the caps_priv_check result like the neighboring cases.

Verdict

HAMMER2IOC_VERSION_GET (hammer2_ioctl.c:86-88) and HAMMER2IOC_INODE_GET (137-139) overwrite the caps_priv_check(SYSCAP_NOVFS_IOCTL) error instead of gating on it; verified on the stock guest that uid=65534 (nobody) successfully invokes INODE_GET on an opened hammer2 path while the gated PFS_GET returns EPERM in the same process. Disclosed content (inode meta + data/inode counts) is stat(2)-equivalent, so impact is hardening/consistency (same class as DF-0815's missing gates, different commands); Low severity.