
PoC and exploit chain for CVE-2026-89633, a heap out-of-bounds write in the Linux cifs.ko SMB1 TRANS2 coalescing path, escalating to uid 0 via cred overwrite and PTE aliasing.
coalesce_t2 heap OOB writeEnglish | 简体中文
Research PoC/EXP for a heap out-of-bounds write in the Linux cifs.ko SMB1
client. A malicious SMB1 server controls DataOffset in a TRANS2 secondary
response, and coalesce_t2() copies the 8-byte secondary payload to
&primary->Protocol + DataOffset + accumulated without any target-bounds
check. The copy destination is wherever the server points it, relative to the
response buffer (server->bigbuf).
This repository keeps two demonstrations with distinct roles; they are not substitutes:
DataOffset at runtime.uid/gid/suid/sgid/euid/egid/fsuid/fsgid. It is the most
direct proof that the OOB write is controllable and can modify a cred, with
the fewest dependencies.selinux_state, and the clone becomes uid 0 with full caps
without root-side observation. It is an extension of EXP 1, not a
replacement.Kernel-module loading is deliberately out of scope for this repository: the
chain stops at uid 0 (and optionally selinux_state permissive) and does not
install a module. Even without a loaded module, uid 0 + permissive is the
complete LPE demonstration this repository claims.
EXP 2's costs remain explicit: a per-window lottery (~25–33%, ~97–99% over 12
attempts), a bare PTE that stays live until reboot, a dependency on
shell-writable tracefs, and build-specific direct-map/call-site/SEL_PFN
assumptions. EXP 1 remains the simplest primitive proof and the fallback
when shell-writable tracefs is unavailable.
| Item | Value |
|---|---|
| CVE | CVE-2026-89633 |
| Class | Heap out-of-bounds write (CWE-787) |
| Component | cifs.ko, SMB1 TRANS2 secondary coalescing |
| Upstream fix | 36d01415d881 ("smb: client: fix ... coalesce_t2", 6.12.113) |
| POC | Trigger and OOB write verified (post-mount and during mount) |
| EXP 1 (minimal primitive) | 4-frame OOB cred write under root-side observation; uid 0 verified (post-mount and mount-time held response) |
| EXP 2 (advanced primitive) | shell-only PTE chain; uid 2000 → uid 0 + full caps + Permissive verified (SEL_PFN/SEL_OFF); stops at a bare PTE, no module load |
| Requirements | an SMB1 mount to an attacker-controlled server (privileged gate); the reference gate uses su, and the shell-only placement needs shell-writable tracefs |
Tested environment: an arm64 Android 16 vendor build, kernel 6.12.38 with
cifs.ko built from the 6.12.23 KMI baseline, SELinux enforcing.
statfs, no access to the mount point), the EXP 2 shell-only placement
(tracefs only, no root-side observation), the full EXP 2 acceptance
(uid 0 + all cap bits + Permissive with SEL_PFN/SEL_OFF), and the
su-driven mount gate (only the mount runs as root; the placement runs as
uid 2000) were tested there under SELinux enforcing.For an SMB1 TRANS2 response larger than MAX_CIFS_SMALL_BUFFER_SIZE - 4
(444 bytes) the client keeps the primary in server->bigbuf and stores it in
mid->resp_buf. The following SMB_COM_TRANSACTION2_SECONDARY responses go
through cifs_check_trans2() -> check2ndT2() -> coalesce_t2():
data_area_of_tgt = &primary->Protocol + primary->DataOffset + total_in_tgt;
memcpy(data_area_of_tgt, data_area_of_src, secondary->DataCount);
DataOffset is taken verbatim from the server response. The counter checks
(total_in_tgt/byte_count overflow, source-buffer bounds) are present, but
there is no check that data_area_of_tgt stays inside the response buffer.
Each secondary frame lands 8 bytes further (the accumulated DataCount), so a
multi-frame response gives an 8*N byte write at an attacker-chosen offset
(16-bit) from the buffer base.
Trigger conditions:
vers=1.0) from a server the attacker
controls;statfs);TotalDataCount > DataCount and
is large enough for the bigbuf path, then one or more secondary frames with
TotalDataCount > DataCount.Makefile
src/poc/poc_smb1_trans2.py malicious SMB1 server (QFS hook, runtime ctl)
src/poc/smb1_proxy.py TCP proxy: patches DataOffset, repeats secondaries
src/exp/pipesearch.c pipe placement/write driver (modes 3, 8, 9)
src/exp/shcred.c shell-side drain + clone helper (uid 2000)
src/exp/shp4a2.c trace-based drainer/observer/clone (modes 8/9)
src/exp/pte_fault.c 2 MiB fault + PTE alias writes
src/exp/bscan.c bigbuf discovery by call site (no kprobes)
src/exp/kslide.c KASLR slide from a raw call site
src/exp/smbd.c device-side SMB1 server (held TRANS2)
src/exp/smbd_templates.h SMB1 response templates for smbd
src/exp/pfn.c linear-map VA to pfn
src/exp/kflags.c /proc/kpageflags reader
scripts/setup_device.sh trace instance + kprobes (root on device)
scripts/start_servers.sh host: SMB1 server + proxy + mount helper
scripts/probe_round.sh one probe round: bigbuf base + free target page
scripts/place.sh placement: pipesearch mode 3 + shcred
scripts/write_cred.sh 4-frame coalesce write + /proc verification
scripts/mount_su.sh generic su mount gate (target/decoy/abort/unmount)
scripts/chain_shell.sh shell-only placement for an already-held TRANS2
scripts/gen_smb_templates.py regenerates smbd_templates.h from a capture
docs/analysis.md vulnerability and chain analysis
docs/exp-protocol.md shared-page and smbd ctl protocols
make deps # checks impacket
make helpers # NDK cross-build of all C helpers
make push # adb push to /data/local/tmp
NDK, API, ARCH, ADB, DEVICE_DIR are overridable.
The trigger itself only needs the SMB1 mount and the running server/proxy.
The kprobes and kflags are observation aids, not part of the trigger —
install them when you want the dst/src/len evidence (and they are required by
the EXP chain below, which needs the bigbuf base, the placement watch and the
target cred address).
Device (root, optional — observation only): install the probes.
adb push scripts/setup_device.sh /data/local/tmp/
adb shell su -c 'sh /data/local/tmp/setup_device.sh'
adb push build/kflags /data/local/tmp/kflags
Host: start the SMB1 server and the proxy (the script also creates the share directory and prints the mount command):
scripts/start_servers.sh /tmp/cifs01-ctl
It runs src/poc/poc_smb1_trans2.py on 1470 and src/poc/smb1_proxy.py
(1460 -> 1470) with the control directory /tmp/cifs01-ctl.