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.
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2648 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_testableConfirmed 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.
No comments yet.