e46a32f11e
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Adds the corpus's first XFS module and its first data-oriented kernel bug —
every other kernel entry corrupts memory; this one corrupts file contents.
xfs_direct_write_iomap_begin() reads the data-fork extent map under ILOCK,
then xfs_reflink_fill_cow_hole() drops ILOCK to wait for transaction log
space. On reacquiring it, the code re-queries the refcount btree at the
ORIGINAL imap->br_startblock and never re-reads the data fork. A second
O_DIRECT writer holding only IOLOCK completes a whole CoW cycle in that
window, so the first writer's stale mapping sees refcount 1, treats a
still-shared block as private, and writes to it in place — landing its data
on the reflink source file's on-disk blocks.
The primitive is an arbitrary overwrite of the on-disk contents of any
readable file, which has three consequences that drive the design:
- No offsets, no ROP, no KASLR/SMEP/SMAP; SELinux, containers and seccomp
are all irrelevant.
- The victim's inode is never written, so mtime/ctime/size never change
and nothing is logged — FIM and `-w /etc/passwd -p wa` cannot see it.
- The change persists across reboots.
Introduced 4.11 (3c68d44a2b49); fixed 2f4acd0fcd86 (mainline 7.2-rc4,
merged 2026-07-16), stable backports 7.1.4 / 6.18.39 / 6.12.96. Exposure is
distro-shaped: RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server
>= 31 and Amazon Linux 2023 ship XFS+reflink by default.
detect() is not a pure version gate — reachability here is safely
observable, so it pairs the backport table with a real storage precondition
(writable XFS via statfs XFS_SUPER_MAGIC, deliberately not via a successful
FICLONE since btrfs implements that too and is unaffected). --active
confirms reflink via FICLONE; SKELETONKEY_XFS_ASSUME_REFLINK=1/0 overrides.
On rpm-family hosts it warns that vendors backport without bumping the
upstream version, so the verdict speaks only to the upstream base.
exploit() forks a child that works only in a private mkdtemp scratch dir on
two files it owns: it establishes a shared extent (FICLONE, corroborated by
FIEMAP_EXTENT_SHARED) plus an O_DIRECT gate, then races a hard-bounded
8 writers / 2 helpers / 16 rounds / 2s and stops, reading the donor back
with O_DIRECT. Deliberately under-driven, and it never clones or targets a
file it does not own — the /etc/passwd overwrite -> su -> root step is
documented but NOT bundled. Always returns EXPLOIT_FAIL.
Safety rank 55, far above bad_epoll (12) and ghostlock (11): a won race
corrupts 4 KiB of our own scratch file and cannot touch kernel memory, so
there is no oops/KASAN/panic path.
Detection inverts the usual advice. auditd/sigma anchor on ioctl request
0x40049409 (FICLONE, matched exactly) and openat O_DIRECT; falco adds the
cross-uid reflink condition; and the yara rule is genuinely the right tool
here, matching the on-disk artifact because FIM is structurally blind.
VM-VERIFIED 2026-07-23 — the corpus's first rpm-family verification, taking
the empirical count to 29 of 41 CVEs. Rocky Linux 9.8 /
5.14.0-687.10.1.el9_8.0.1.x86_64 under qemu/KVM, stock GenericCloud layout
with no provisioner changes (root is XFS with reflink=1 out of the box).
detect() -> VULNERABLE, --active FICLONE witness confirmed reflink, phase A
observed FIEMAP_EXTENT_SHARED on a real shared extent, scratch self-cleaned,
clean build on el9 gcc. The underlying bug was separately confirmed winnable
on that kernel via tools/verify-vm/refluxfs_verify.c at the public PoC's
parameters (32 writers / 8 helpers, 60s): 4/4 runs won, first divergence
after 69/114/170/494 rounds. The shipped under-driven trigger did NOT win in
its 2s budget on that same vulnerable kernel — intended behaviour, and
exactly why a non-win must never be read as "patched".
14 new detect() unit rows (148 tests total, 0 failures). Bumps to v0.9.14.
Credit: Qualys Threat Research Unit (blog by Saeed Abbasi; the technical
advisory credits model-assisted kernel analysis performed with Anthropic),
and the upstream XFS maintainers who fixed it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
74 lines
3.0 KiB
C
74 lines
3.0 KiB
C
/*
|
|
* SKELETONKEY — module registry
|
|
*
|
|
* Global list of registered modules. Each family contributes via
|
|
* register_<family>_modules() called from skeletonkey main() at startup.
|
|
*/
|
|
|
|
#ifndef SKELETONKEY_REGISTRY_H
|
|
#define SKELETONKEY_REGISTRY_H
|
|
|
|
#include "module.h"
|
|
|
|
void skeletonkey_register(const struct skeletonkey_module *m);
|
|
|
|
size_t skeletonkey_module_count(void);
|
|
const struct skeletonkey_module *skeletonkey_module_at(size_t i);
|
|
|
|
/* Find a module by name. Returns NULL if not found. */
|
|
const struct skeletonkey_module *skeletonkey_module_find(const char *name);
|
|
|
|
/* Each module family declares one of these in its public header. The
|
|
* top-level skeletonkey main() calls them in order at startup. */
|
|
void skeletonkey_register_copy_fail_family(void);
|
|
void skeletonkey_register_dirty_pipe(void);
|
|
void skeletonkey_register_entrybleed(void);
|
|
void skeletonkey_register_pwnkit(void);
|
|
void skeletonkey_register_nf_tables(void);
|
|
void skeletonkey_register_overlayfs(void);
|
|
void skeletonkey_register_cls_route4(void);
|
|
void skeletonkey_register_dirty_cow(void);
|
|
void skeletonkey_register_ptrace_traceme(void);
|
|
void skeletonkey_register_netfilter_xtcompat(void);
|
|
void skeletonkey_register_af_packet(void);
|
|
void skeletonkey_register_fuse_legacy(void);
|
|
void skeletonkey_register_stackrot(void);
|
|
void skeletonkey_register_af_packet2(void);
|
|
void skeletonkey_register_cgroup_release_agent(void);
|
|
void skeletonkey_register_overlayfs_setuid(void);
|
|
void skeletonkey_register_nft_set_uaf(void);
|
|
void skeletonkey_register_af_unix_gc(void);
|
|
void skeletonkey_register_nft_fwd_dup(void);
|
|
void skeletonkey_register_nft_payload(void);
|
|
void skeletonkey_register_sudo_samedit(void);
|
|
void skeletonkey_register_sequoia(void);
|
|
void skeletonkey_register_sudoedit_editor(void);
|
|
void skeletonkey_register_vmwgfx(void);
|
|
void skeletonkey_register_dirtydecrypt(void);
|
|
void skeletonkey_register_fragnesia(void);
|
|
void skeletonkey_register_pack2theroot(void);
|
|
void skeletonkey_register_sudo_chwoot(void);
|
|
void skeletonkey_register_udisks_libblockdev(void);
|
|
void skeletonkey_register_pintheft(void);
|
|
void skeletonkey_register_mutagen_astronomy(void);
|
|
void skeletonkey_register_sudo_runas_neg1(void);
|
|
void skeletonkey_register_tioscpgrp(void);
|
|
void skeletonkey_register_vsock_uaf(void);
|
|
void skeletonkey_register_nft_pipapo(void);
|
|
void skeletonkey_register_ptrace_pidfd(void);
|
|
void skeletonkey_register_sudo_host(void);
|
|
void skeletonkey_register_cifswitch(void);
|
|
void skeletonkey_register_nft_catchall(void);
|
|
void skeletonkey_register_bad_epoll(void);
|
|
void skeletonkey_register_ghostlock(void);
|
|
void skeletonkey_register_refluxfs(void);
|
|
|
|
/* Call every skeletonkey_register_<family>() above in canonical order.
|
|
* Single source of truth so the main binary and the test binary stay
|
|
* in sync — adding a new module is one register_* declaration here
|
|
* and one call inside skeletonkey_register_all_modules() in
|
|
* core/registry.c (the test harness picks it up automatically). */
|
|
void skeletonkey_register_all_modules(void);
|
|
|
|
#endif /* SKELETONKEY_REGISTRY_H */
|