
Analysis, POC, and arm64 exploit chain for a Linux cifs.ko DACL heap overflow (CVE-2026-97555), escalating to a uid-0 credential overwrite on the reference build.
English | 简体中文
Kernel heap overflow in the SMB client DACL rewrite. id_mode_to_cifs_acl()
sizes the output security descriptor from the DACL returned by the server,
but replace_sids_and_copy_aces() rewrites every ACE with the new
owner/group SID (up to 68 bytes). A server that returns a short-SID DACL
therefore makes the rewrite outgrow the allocation and write past the
kmalloc object.
This repository ships the analysis, a POC that reproduces the cross-object write, and an EXP that turns the overflow into a controlled end-to-end chain (heap leak, page-table reclaim, arbitrary PTE write, self-cred overwrite to uid 0) on the reference build.
| Item | Value |
|---|---|
| CVE | CVE-2026-97555 |
| Class | Heap buffer overflow (write, CWE-122) in id_mode_to_cifs_acl() |
| Component | cifs.ko (SMB client) |
| Affected | Linux cifs.ko without the fix (validated against a 6.12 GKI-based Android kernel) |
| Patch | upstream 0ee150794c75 (stable fb736ab4fb8c) |
| Reference build | Android 16 GKI-based 6.12 kernel, arm64, 4 KiB pages, 64-byte struct page |
| POC | Cross-object write reproduced (SET_INFO security descriptor of 696 bytes) |
| EXP | Full chain to a uid-0 shell on the reference build |
/proc/kpageflags as root. Obtaining the mount
capability is a test fixture, not part of the bug, and is out of scope of
this repository.uid/suid/euid/fsuid fields of its own credential;
no capability or securebits field is touched. Cleanup is a reboot.src/exp.c (symbol addresses, vmemmap geometry, BTF
offsets) belong to the reference build. Re-derive them for any other
build before running; see scripts/derive_offsets.sh.id_mode_to_cifs_acl() computes the allocation from dacl_ptr->size or
num_aces * sizeof(struct smb_ace), then replace_sids_and_copy_aces()
rebuilds each ACE with the new owner/group SID. When the server's SIDs are
short (0-subauth) and the new SID is 68 bytes, the rewritten DACL is longer
than the allocation and build_sec_desc() writes past the slab object.
The kernel's dynamic FORTIFY instrumentation checks the inlined ACE-header
stores, but the SID copies go through the out-of-line cifs_copy_sid() and
are not instrumented. A DACL aligned so that every ACE header stays inside
nsecdesclen - 28 reaches the vulnerable copy.
The cross7 server layout (7 ACEs, source sizes 76/76/76/44/44/16/16) makes
the allocation exactly 512 bytes; the 7th ACE's 68-byte SID copy starts at
offset 492 and crosses the object boundary, and the rewritten descriptor
grows to 696 bytes — 184 bytes past the object. The expanded descriptor is
still delivered in SET_INFO, so the write is observable from the server
side.
fb736ab4fb8c.kpageflags, mount,
chown) and a SELinux-permissive window (or an equivalent policy
allowance) for the cifs idmap upcall.src/poc_smb2_acl.py, mounted with
cifsacl/modefromsid through the standard kernel client
(scripts/mount_cifs.sh). The mount itself is a privileged fixture; how
the tester obtains that privilege is outside this repository.cifs.idmap keys preloaded for the target ids: the reference environment
has no /sbin/cifs.idmap upcall helper (src/poc.c and src/exp.c
preload them; src/acl_key_preload.c does it standalone).# host
python3 src/poc_smb2_acl.py # ACL_LAYOUT=cross7 for the overflow
adb reverse tcp:1468 tcp:1468
# device (root)
sh /data/local/tmp/e2e/mount_cifs.sh 127.0.0.1 share /mnt/cifs
make && make push
/data/local/tmp/poc /mnt/cifs
Expected evidence: the server log prints
SET_INFO security: len=696 ... (cross7), i.e. a descriptor longer than the
512-byte allocation, and scripts/trace_acl02.sh shows the
id_mode_to_cifs_acl entry/return around the chown.
The POC only triggers the vulnerable path; it does not groom, leak or escalate. The trigger performs an out-of-bounds kernel write: builds whose layout trips the dynamic FORTIFY checks can panic, so run it only on a test device you can reboot.
The EXP is the full chain, end to end on the reference build:
cross7 overflow rewrites a neighbouring key payload's
datalen; keyctl_read dumps 64 KiB including a live pipe_buffer
array. The leaked anon_pipe_buf_ops pointer gives the KASLR slide and
the buffer page P gives the real struct page target.buffer[0] is
rewritten with page=P, len=0x1234, ops=default_pipe_buf_ops.P; a fault at
base reclaims it as a page-table page, gated by a kretprobe proving
__alloc_pages_noprof returned P from __pte_alloc.P+16 (PTE[2]); its first eight bytes are a
self-map descriptor for P itself. Userland then reads and writes the
page-table page through base+8 KiB.KPF_SLAB pages are mapped through
fresh PTEs and scanned for this task's comm; pid/tgid, the linear-map
window and cred->uid/euid cross-check the hit, and fresh, never-used
VAs re-verify the physical page.setresuid(2000) (the
kernel allocates a fresh cred), reads task->cred through the self-map,
zeros only uid/suid/euid/fsuid, confirms getuid()==0, disarms the
self-map and execves /system/bin/sh. The shell runs as uid 0.make exp
make push
# on the device, as root:
sh /data/local/tmp/e2e/launch_exp.sh /mnt/cifs 10 1 r1 4000 400 2400 7 0 0 cred
# then read /data/local/tmp/e2e/e2e_status.txt and
# /data/local/tmp/e2e/root_shell.txt
The cred stage is opt-in through the last argument (cred); scout stops
after the read-only validation. The EXP keeps its PTEs inside the process
and cleanup is a reboot.
Requirements: Android NDK r29 (override with NDK=), GNU make, adb.
make # POC (arm64, API 24)
make exp # EXP
make build-all # both
make check # push and run the read-only pre-check (poc --check)
make push # push binaries and helper scripts
make run # push and run the POC against $(MNT) (default /mnt/cifs; destructive)
make host # static POC build for qemu-aarch64
make emu # host build and run
make logs # dump logcat/dmesg into logs/
make help