CVE-2026-30874: procd hotplug-dispatch, OpenWrt 24.10.5 to 24.10.6
OpenWrt 24.10.6 takes procd from 2024-12-22 to 2026-03-14 (OpenWrt commit 3e77f8c), which is 24 commits. One of them is the fix of CVE-2026-30874, which Trail of Bits found (TOB-OWRT-4):
hotplug_callcopies the environment that a caller of thehotplugobject of ubus gives, and must dropPATH. It compared each variable, asPATH=/tmp, to"PATH"withstrcmp, so no variable ever matched, and a caller could set thePATHof the scripts it runs. The fix compares the first 5 bytes to"PATH="withstrncmp(procd commit 8581313).
The other commits fix an out-of-bounds write in cgroups_apply, a
use-after-free in the update of a service, and more, so much of procd
changed. OpenWrt runs sstrip on what it ships, so only the segments of
the file are left.
Get the binaries
$ for v in 24.10.5 24.10.6; do
mkdir -p $v
curl -s https://downloads.openwrt.org/releases/$v/targets/armsr/armv8/openwrt-$v-armsr-armv8-rootfs.cpio.gz \
| gunzip | (cd $v && cpio -id sbin/procd)
done
Compare
$ hereticus --format markdown 24.10.5/sbin/procd 24.10.6/sbin/procd
| FUNCTION | CHANGE | SCORE | +/-/~ | TOUCHED | MATCHED BY |
|---|---|---|---|---|---|
sub_7630 -> sub_75c0 | changed | 1.00 | 1/0/0 | 1 callee, 1 text | similar |
sub_f99c -> sub_fb64 | changed | 0.85 | 31/57/5 | 11 branch, 10 call, 5 jump, 27 mem, 14 const | similar |
sub_8ab4 -> sub_8bb0 | changed | 0.89 | 28/8/2 | 3 branch, 3 call, 5 mem, 1 text | similar |
sub_ae68 -> sub_afb8 | changed | 0.69 | 14/10/6 | 3 branch, 1 call, 2 callee, 4 jump, 5 mem, 2 const | similar |
sub_12994 -> sub_12b20 | changed | 0.92 | 19/0/1 | 2 branch, 2 call, 1 jump, 3 mem, 1 text | similar |
sub_f604 -> sub_f790 | changed | 0.93 | 16/0/0 | 1 branch, 2 call, 4 mem, 3 const, 1 text | similar |
sub_e920 -> sub_ea94 | changed | 0.94 | 7/0/0 | 2 branch, 2 mem | similar |
sub_70b0 -> sub_8220 | changed | 0.86 | 10/0/0 | 2 call, 1 mem, 2 text | similar |
sub_12e14 -> sub_1300c | changed | 0.62 | 9/4/6 | 7 branch, 1 jump, 3 const, 1 text | same references |
sub_10938 -> sub_10aac | changed | 0.94 | 9/0/19 | 18 mem, 7 const | similar |
sub_df54 -> sub_e0a8 | changed | 0.96 | 9/1/8 | 10 mem | similar |
sub_11edc -> sub_1204c | changed | 0.99 | 7/0/0 | 1 call, 1 mem, 1 const, 1 text | similar |
sub_7b08 -> sub_7a9c | changed | 0.92 | 1/2/3 | 1 branch, 2 mem | similar |
sub_115f0 -> sub_11788 | changed | 0.97 | 6/16/1 | 4 call, 1 callee, 1 mem, 7 const, 2 text | similar |
sub_fef4 -> sub_1005c | changed | 0.98 | 6/3/1 | 3 call, 3 jump, 2 mem, 2 const | similar |
sub_73f0 -> sub_73c8 | changed | 0.79 | 4/21/4 | 1 branch, 23 mem, 24 const | similar |
sub_fe74 -> sub_ffd0 | changed | 0.89 | 3/0/0 | 1 call, 2 mem, 1 const | similar |
sub_8044 -> sub_8378 | changed | 0.43 | 2/86/2 | 9 branch, 10 call, 2 jump, 15 mem, 5 text | same references |
sub_8d70 -> sub_8edc | changed | 0.82 | 1/0/1 | 2 mem | similar |
sub_6a94 -> sub_6b14 | changed | 0.89 | 0/0/1 | 7 const | similar |
sub_a34c -> sub_a4bc | changed | 0.98 | 0/8/2 | 9 mem, 7 const | similar |
sub_6ca8: newsub_806c: newsub_8eb0: new
177 matched (144 unchanged, 33 changed), 0 gone, 3 new
12 changed only a constant, hidden. Use –show-retouched to list them.
The fix is on row 1 of
24, counting the functions new.
The report puts first a function that turns a call into its bounded
form, as the fix turns strcmp into strncmp, though the fix adds only
one instruction: the length that strncmp takes.
Find the fix
When the advisory names a text or a function of the C library, search the diff of every changed function for it:
$ hereticus --view il --diff-all 24.10.5/sbin/procd 24.10.6/sbin/procd | grep -E '^(sub_|[-+].*PATH)'
...
sub_7630 -> sub_75c0
- x1 = &"DEVPATH"[3]
+ x1 = &"PATH="
...
Read the fix
$ hereticus --view il --diff sub_7630 24.10.5/sbin/procd 24.10.6/sbin/procd
...
x1 = &"LD_"
- call x3
+ call strncmp
if (w0 == 0) goto $+0x11c
x1 = &page
x0 = x20
- x1 = &"DEVPATH"[3]
- call strcmp
+ x1 = &"PATH="
+ x2 = 5
+ call strncmp
if (w0 == 0) goto $+0x104
...
The linker keeps one copy of "DEVPATH" for both texts, and a pointer
3 bytes into it for "PATH". The LD_ test calls strncmp in both
builds: 24.10.5 keeps the address of strncmp on the stack, and 24.10.6
in x26. The other lines read as the C of the fix:
- if (!strcmp(enve, "PATH"))
+ if (!strncmp(enve, "PATH=", 5))
continue;
The other architectures
| Build | Rows | Row of the fix |
|---|---|---|
armsr/armv8, AArch64 | 24 | 1 |
armsr/armv7, A32 | 29 | 1 |
malta/be, MIPS32 big endian | 25 | 1 |
malta/le, MIPS32 little endian | 25 | 1 |
This case showed four faults of --view il, all fixed (RESEARCH.md,
“Published fixes, again”). Before, the AArch64 diff named the
strncmp of the fix avl_delete, as a load on another path came first
in the code; the A32 diff showed the text as unchanged, and the words of
the literal pool after the function as ten changed instructions.
hereticus-cli/tests/truth_cves.rs locks these ranks, and
fixtures/cve_binaries.py fetches and pins the binaries.