CVE-2026-31431
Confirmed the exploit. It’s real.
CVE-2026-31431
Confirmed the exploit. It’s real.
Some further links:
https://www.cve.org/CVERecord?id=CVE-2026-31431
What kernels are vulnerable? At and after 4.14.
6.18.22-6.18.* not affected.
unaffected from 6.19.12 through 6.19.*
unaffected from 7.0
Debian Tracker: CVE-2026-31431
RHEL Tracker: cve-details
Ubuntu Tracker: CVE-2026-31431 | Ubuntu
SUSE Tracker: CVE-2026-31431 Common Vulnerabilities and Exposures | SUSE
Amazon Linux Tracker: CVE-2026-31431
Fedora: No tracker but latest is not vulnerable, everything running pre 6.18.22 kernel IS vulnerable
Arch Linux Tracker: CVE-2026-31431 - linux - Arch Linux
Github Advisory: In the Linux kernel, the following vulnerability has been... · CVE-2026-31431 · GitHub Advisory Database · GitHub
Assessment of publisher: legitimate. Won third place in AI DARPA Challenge.
GitHub Repo (currently thin on confirmed kernel versions).
Published exploit code (direct from team). Modifies in-memory binary.
Blog post:
Hacker News comments:
To test whether your userspace blocking of AF_ALG is working, as a non-root user (note that this is different from testing whether the kernel module has been disabled):
python3 -c "import socket; s = socket.socket(38, 5, 0); print('AF_ALG socket created - UNMITIGATED')" 2>&1 || echo "Blocked"
An oss-sec thread:
If you happen to be a Tenable shop:
Plugin ID: 309203
Of note, musl distributions appear to be unaffected due to not linking suid binaries against the kernel and setting BIND_NOW on all of them. So all of the algif_aead involved are already blocked and there’s nowhere to create or insert.
I tried a couple different suid binaries on a vulnerable kernel as well as dragging some statics over to a confirmed vulnerable Alma box, and no joy with any of them at all.
edit: thinking on this more, I think it’s probably correct to say any suid binary not linked against the kernel directly and with BIND_NOW would be impossible to exploit as the necessary symbols can’t be there. There shouldn’t be any possible path to authencesn.
@rootwyrm Apologies, I’m not a kernel hacker, but from the description it looks like the exploit is simply … overwriting the beginning of the in-memory target binary, and the resulting execution is super simple, not linking anything or using any symbols, etc.?
While the committed issue tracker is thin, the issues have a lot of detail.
Seems to not work on stock Alpine but if you have a readable setuid bin then it can
I’m still poking through it but it’s not that simple; just dropping a suid bin on the box doesn’t work. The key part is 1) linking 2) not having BIND_NOW set. BIND_NOW requires all symbols in the library be resolved before dlopen() return, which means you can’t load symbols after the fact. (This is not the ‘so just put it on everything’ flag many folks think it is though, because it impacts load, execution, and dynamic linking.)
So that said, I’m fully expecting non-default dynamically linked packages to be be exploitable. e.g. sudo from packages likely will be, certain pam modules almost definitely, iputils because it’s all dynamically linked to the kernel there, etc. Which would make it considerably more difficult in the wild. e.g. setting up pam on Alpine is… involved, let us just say. And it’s uncommon to install iputils-* as there are already built-ins for most of them. sudo is, well, obvious. But the guidance is that people should be using doas which behaves the same as su and suexec and has the same linking and flags.
The key thing here is that the built-ins on Alpine and other musl distributions do not use or even know about the vulnerable syscalls, and by default protect against bringing in any random symbols you feel like. So even if one were to say take away BIND_NOW NOW PIE from doas, the exploit essentially depends on the kernel cryptographic authencesn function. suid but no authencesn, no exploit. (At least that’s how it should work.)
This is where I’m getting lost. The exploit isn’t involving syscall references in binaries. It’s using a AF_ALG socket with authencesn to perform the page cache write, resulting in the /bin/su or whatever overwrite with the shellcode. Static linking doesn’t change that flow, as far as I can see. I agree that exploiting Alpine, etc. would be non-default, but I am missing how BIND_NOW is involved with this process at all.
Trying to parse out what they detailed, but it seems like BIND_NOW is why the symbols are loaded before dlopen(), allowing this exploit.
To me this seems like a search order exploit akin to popping windows with DLL search order hijacking. BIND_NOW is bypassing the “checks” dlopen() has in place for linked objects?
Correct me if I am wrong, anyone. This is just my take on it after reading up on it.
Particularly this bit in the documentation I linked above:
If filename begins with ``/‘’ (i.e., it’s an absolute path), dlopen() will just try to use it (it won’t search for a library). Otherwise, dlopen() will search for the library in the following order:
My understanding is that the splice() call during the AEAD construction allows the socket to write to any file: in this case, a suid-set binary.
Step 2: Construct the write. For each 4-byte chunk of the shellcode payload, construct a sendmsg() + splice() pair. The sendmsg provides the AAD: bytes 4–7 carry the 4 bytes to write (seqno_lo). The splice provides the target file’s page cache pages as the ciphertext and tag. The AEAD parameters (assoclen, splice offset, splice length) are chosen so that dst[assoclen + cryptlen] falls on the target offset within /usr/bin/su’s .text section.
Step 3: Trigger the write. recv() triggers the decrypt operation. Inside authencesn, the kernel reads the ESN bytes from the AAD and writes seqno_lo at dst[assoclen + cryptlen]. The scatterwalk crosses from the output buffer into the chained page cache page. Four bytes are written to the kernel’s cached copy of /usr/bin/su. The HMAC is computed over the rearranged data and fails. The kernel reads seqno_lo back to restore the AAD, but the original bytes at the tag position are never restored. recvmsg returns an error. The page cache is corrupted.
So not so much a search order thing as a lack of restriction in this API?
This goes to the very edges of my understanding of linux internals, unfortunately. I can’t say with any certainty what explicitly is being exploited here.
Have we seen patch diffs yet, or are those in progress?
It’s using a
AF_ALGsocket withauthencesnto perform the page cache write, resulting in the/bin/suor whatever overwrite with the shellcode.
This is the correct understanding. What isn’t being well explained is what page cache, how it gets into page cache, and how things run from page cache. Bear with me because this is going to be somewhat stream of consciousness rather than trying to step through it.
If you run the PoC on Alpine, you will get the relevant kernel modules loaded. But if you just run su, they aren’t. Follow so far? Cool.
Now there’s the page cache. Which is both complicated and dumb because Linux is still the absolute worst at all forms of memory management. The page cache fundamentally sits between disk and userland because there is now only page cache. (Which is a different kind of dumb but still dumb.) Still very clear, right?
Now, observe:
netbox [~]$ strace su
execve("/bin/su", ["su"], 0x7ffd2d127340 /* 12 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x7fde3b80fb28) = 0
set_tid_address(0x7fde3b80ff90) = 14819
brk(NULL) = 0x558f69c68000
brk(0x558f69c6a000) = 0x558f69c6a000
mmap(0x558f69c68000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x558f69c68000
mprotect(0x7fde3b80c000, 4096, PROT_READ) = 0
mprotect(0x558f3a159000, 4096, PROT_READ) = 0
poll(0x7ffe5a2724f0, 3, 0) = 0 (Timeout)
execve(0x558f3a158000, 0x7fde3b810ca0, 0x7ffe5a272688) = 0
arch_prctl(ARCH_SET_FS, 0x7f9dafe00b28) = 0
set_tid_address(0x7f9dafe00f90) = 14819
brk(NULL) = 0x55efc19ff000
brk(0x55efc1a01000) = 0x55efc1a01000
mmap(0x55efc19ff000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x55efc19ff000
mprotect(0x7f9dafdfd000, 4096, PROT_READ) = 0
mprotect(0x55ef89b3f000, 16384, PROT_READ) = 0
getuid() = 1000
getgid() = 1000
geteuid() = 1000
write(2, "su: must be suid to work properl"..., 34su: must be suid to work properly
) = 34
exit_group(1) = ?
“So your su is broken!”
netbox [~]$ su
Password:
/home/testuser #
Nope. It’s just strace. Which on the surface seems like it’s working just fine, right? Yep. It is. And there’s two very important lines:
mprotect(0x7fde3b80c000, 4096, PROT_READ) = 0
mprotect(0x558f3a159000, 4096, PROT_READ) = 0
THAT’S why the exploit fails. Prior to entering execve, su is already setting the pages to PROT_READ meaning strict read-only. The exploit is trying to write into these pages, which in musl, will cause an EACCES for any modification. Simple on the surface, “but wait, everyone’s going to mprotect()!!”
Yep. This is why it’s not working on musl and linking dependent. Apologies for how miserable this is to read, but this is ‘normal’ for Alma/RHEL/etc. And you’ll see REAL fast what the problem is.
execve("/usr/bin/su", ["su"], 0x7ffd5663c040 /* 60 vars */) = 0
access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
brk(NULL) = 0x5577c2969000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffd07c9260) = -1 EINVAL (Invalid argument)
fcntl(0, F_GETFD) = 0
fcntl(1, F_GETFD) = 0
fcntl(2, F_GETFD) = 0
access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6ee93fe000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=51931, ...}) = 0
mmap(NULL, 51931, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f6ee93f1000
close(3) = 0
openat(AT_FDCWD, "/lib64/libpam.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P:\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=69480, ...}) = 0
mmap(NULL, 69640, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee93df000
mmap(0x7f6ee93e2000, 36864, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f6ee93e2000
mmap(0x7f6ee93eb000, 16384, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xc000) = 0x7f6ee93eb000
mmap(0x7f6ee93ef000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xf000) = 0x7f6ee93ef000
close(3) = 0
openat(AT_FDCWD, "/lib64/libpam_misc.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\23\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=19600, ...}) = 0
mmap(NULL, 20552, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee93d9000
mmap(0x7f6ee93da000, 8192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7f6ee93da000
mmap(0x7f6ee93dc000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f6ee93dc000
mmap(0x7f6ee93dd000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f6ee93dd000
close(3) = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\247\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\306ni[3\351l\355\246p:\3366\262PT"..., 68, 896) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2549264, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2133968, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee9000000
mmap(0x7f6ee9029000, 1527808, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x29000) = 0x7f6ee9029000
mmap(0x7f6ee919e000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19e000) = 0x7f6ee919e000
mmap(0x7f6ee91f6000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1f6000) = 0x7f6ee91f6000
mmap(0x7f6ee91fc000, 53200, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f6ee91fc000
close(3) = 0
openat(AT_FDCWD, "/lib64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p<\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=163368, ...}) = 0
mmap(NULL, 209072, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee93a5000
mmap(0x7f6ee93a8000, 57344, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f6ee93a8000
mmap(0x7f6ee93b6000, 86016, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x11000) = 0x7f6ee93b6000
mmap(0x7f6ee93cb000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x26000) = 0x7f6ee93cb000
mmap(0x7f6ee93cd000, 45232, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f6ee93cd000
close(3) = 0
openat(AT_FDCWD, "/lib64/libeconf.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p&\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=40512, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6ee93a3000
mmap(NULL, 42032, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee9398000
mmap(0x7f6ee939a000, 20480, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f6ee939a000
mmap(0x7f6ee939f000, 8192, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7f6ee939f000
mmap(0x7f6ee93a1000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8000) = 0x7f6ee93a1000
mmap(0x7f6ee93a2000, 1072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f6ee93a2000
close(3) = 0
openat(AT_FDCWD, "/lib64/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\323\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=904760, ...}) = 0
mmap(NULL, 893184, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee92bd000
mmap(0x7f6ee92ca000, 458752, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xd000) = 0x7f6ee92ca000
mmap(0x7f6ee933a000, 376832, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7d000) = 0x7f6ee933a000
mmap(0x7f6ee9396000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xd8000) = 0x7f6ee9396000
close(3) = 0
openat(AT_FDCWD, "/lib64/libcap-ng.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20&\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=32528, ...}) = 0
mmap(NULL, 32792, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6ee92b4000
mprotect(0x7f6ee92b6000, 20480, PROT_NONE) = 0
mmap(0x7f6ee92b6000, 12288, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f6ee92b6000
mmap(0x7f6ee92b9000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x5000) = 0x7f6ee92b9000
mmap(0x7f6ee92bb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f6ee92bb000
mmap(0x7f6ee92bc000, 24, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f6ee92bc000
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6ee92b2000
arch_prctl(ARCH_SET_FS, 0x7f6ee92b2bc0) = 0
set_tid_address(0x7f6ee92b2e90) = 1824636
set_robust_list(0x7f6ee92b2ea0, 24) = 0
rseq(0x7f6ee92b3560, 0x20, 0, 0x53053053) = 0
mprotect(0x7f6ee91f6000, 16384, PROT_READ) = 0
mprotect(0x7f6ee92bb000, 4096, PROT_READ) = 0
mprotect(0x7f6ee9396000, 4096, PROT_READ) = 0
mprotect(0x7f6ee93a1000, 4096, PROT_READ) = 0
mprotect(0x7f6ee93cb000, 4096, PROT_READ) = 0
mprotect(0x7f6ee93ef000, 4096, PROT_READ) = 0
mprotect(0x7f6ee93dd000, 4096, PROT_READ) = 0
mprotect(0x5577c177f000, 4096, PROT_READ) = 0
mprotect(0x7f6ee9436000, 8192, PROT_READ) = 0
Holy fucking shit, look at all them libraries getting loaded! Good thing we’re doing mprotect() right? That should protect us.
Sure, except the exploit actually is a - direct quote - “straight-line logic flaw”. Which depends on, again quoting, “Page Cache Pages in the Writable Scatterlist”. (Set aside splice(); it’s the how-to not the why.) Now, remember how I said there’s just the one dumb page cache? This is what makes it so incredibly dumb: it’s one big dumb page cache with random attempts at isolation tacked back onto it.
This is the point in things where RootWyrm looks at the kernel developers, sighs loudly, and once again says “whoever the fuck gave you a passing grade in operating systems has done you and the world a disservice, you depleted uranium dense motherfuckers. Fuck’s sake, this is why I feel the need to drink.”
The commit: crypto: algif_aead - copy AAD from src to dst · torvalds/linux@72548b0 · GitHub
Here’s the dumb problem in crypto/algif_aead.c:
aead_request_set_crypt(&areq->aead_req, areq->tsgl, areq->first_rsgl.sgl.sg, used, ctx->iv);
The fix:
aead_request_set_crypt(&areq->aead_req, src, areq->first_rsgl.sgl.sg, used, ctx->iv);
All my C people are already seeing where this shitshow is going, right? Now here’s where it gets super mega fun:
for (i = 0; i < sgl->cur; i++) {
size_t bytes_count;
/* Skip offset */
if (offset >= sg[i].length) {
offset -= sg[i].length;
bytes -= sg[i].length;
continue;
}
bytes_count = sg[i].length - offset;
offset = 0;
sgl_count++;
That’s the fix.
The original?
for (i = 0; i < sgl->cur; i++) {
sgl_count++;
if (sg[i].length >= bytes)
return sgl_count;
bytes -= sg[i].length;
}
}
YEP! No check and scatterlists are not implicit, they are explicit and specific to the crypto API. If you aren’t calling the crypto API, you aren’t creating the scatterlist, and thus the exploit fails. Because the exploit does not simply operate against ‘the page cache.’ It operates specifically against the scatterlist created by the crypto API. Which is why they list disabling the module as a mitigation.
From algif_aead.c:
* The kernel maintains two SGLs, the TX SGL and the RX SGL. The TX SGL is
* filled by user space with the data submitted via sendmsg (maybe with
* MSG_SPLICE_PAGES). Filling up the TX SGL does not cause a crypto operation
* -- the data will only be tracked by the kernel. Upon receipt of one recvmsg
* call, the caller must provide a buffer which is tracked with the RX SGL.
So what this means is that if you are not explicitly calling the crypto API via sendmsg which then explicitly creates a scatterlist, the exploit will fail, because there is no scatterlist to exploit.
edit; oh yeah and also maybe if people DIDN’T FUCKING ALLOW MSG_SPLICE_PAGES IN SETUID SHIT. >![]()
So hopefully that explains it?
I know you think this wasn’t clear, but it was incredibly helpful. Thank you!
Apologies, but isn’t this tracking what happens when regular su is executed? I thought the whole purpose of the exploit was to overwrite the cached copy of su with an entirely different payload – a static ELF – and then execute that. mprotect would never be invoked by that payload. What happens if you strace the corrupted su after it’s been modified?
(And to be clear, I’m just barely keeping up here, so I’m totally prepared to find out I’m missing The Point!)
Yeah, that does track. When we open() the target binary, is the resulting page cache copy protected in all cases on Alpine? If yes, that is the cause for failure, since the exploit requires that file descriptor for the destination of the copy over.
And if not, under what conditions would they be?
I, too, am way out over my skis here.
Claim of making it run on Alpine.
Oh, it will run on Alpine. But not with anything stock.
The part that people keep missing is that it is not just “page cache.” But Linux continues to be just Earth-shatteringly stupid about so many things when it comes to the basics of memory management and virtual memory management. So there’s just this giant nebulous “page cache” which is easily exhaustible and implodes the whole system. Great job guys. Clear as a window painted in Vantablack™ and just about as smart as it.
So that extremely necessary rant out of my system and with not anywhere near enough sleep, the actual key part is a specific TYPE of page in the cache - crypto API scatterlists. Remember splice(2)? The reason that works, is because they made very, very bad assumptions and didn’t implement basic checks in a specific module. That splice() needs to operate specifically against a page which has been created and is managed by algif_aead as an element of a scatterlist. Because again, they made bad assumptions and failed to implement basic checks.
If it was just ‘page cache’ then it would be exploitable using, for example, nothing more than a disk write or network packet with absolutely no mitigation, only patching. You literally would be able to cat your way to root in nothing flat. And we’d be mega-turbo-fucked instead of simply turbo-fucked because of the distros with the most market share.
Thankfully, it’s not (yet. I’m not ruling it out. I don’t want to look at the iscsi code - which also loves MSG_SPLICE_PAGES - for all the reasons.) As it sits, it is an absolute requirement to have algif_aead available and involved to reach the exploit.
So if a binary is setuid but does not use the crypto APIs that bring in the algif_aead module, then it is non-exploitable because there is no path to the vulnerable code.
All that said, sure. You can batter your way into making Alpine vulnerable. But that’s going to require root in the first place. If you want to make Alpine vulnerable, just install linux-pam and now you can join in the “fun.”
Same story with the linked exploit. It doesn’t work OOTB either. They used /usr/bin/chsh- this is part of shadow which is non-stock (see also: linux-pam above) and specifically uses the kernel crypto API. Same for iputils; non-stock and specifically hooks the kernel crypto API (don’t ask me why those do, I haven’t had enough coffee for that level of ‘FUCKING WHY?!’ yet.) You have to already have root to install atypical packages.
So yeah, unless you’re intentionally installing packages that bring their own setuid binaries, default-ish (e.g. normal stuff like git, curl, bash, etc.) Alpine is ‘safe enough.’
I’m waiting to see the promised container escape, but I expect that hinges around Docker/podman/etc. using the crypto API.