Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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_call copies the environment that a caller of the hotplug object of ubus gives, and must drop PATH. It compared each variable, as PATH=/tmp, to "PATH" with strcmp, so no variable ever matched, and a caller could set the PATH of the scripts it runs. The fix compares the first 5 bytes to "PATH=" with strncmp (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
FUNCTIONCHANGESCORE+/-/~TOUCHEDMATCHED BY
sub_7630 -> sub_75c0changed1.001/0/01 callee, 1 textsimilar
sub_f99c -> sub_fb64changed0.8531/57/511 branch, 10 call, 5 jump, 27 mem, 14 constsimilar
sub_8ab4 -> sub_8bb0changed0.8928/8/23 branch, 3 call, 5 mem, 1 textsimilar
sub_ae68 -> sub_afb8changed0.6914/10/63 branch, 1 call, 2 callee, 4 jump, 5 mem, 2 constsimilar
sub_12994 -> sub_12b20changed0.9219/0/12 branch, 2 call, 1 jump, 3 mem, 1 textsimilar
sub_f604 -> sub_f790changed0.9316/0/01 branch, 2 call, 4 mem, 3 const, 1 textsimilar
sub_e920 -> sub_ea94changed0.947/0/02 branch, 2 memsimilar
sub_70b0 -> sub_8220changed0.8610/0/02 call, 1 mem, 2 textsimilar
sub_12e14 -> sub_1300cchanged0.629/4/67 branch, 1 jump, 3 const, 1 textsame references
sub_10938 -> sub_10aacchanged0.949/0/1918 mem, 7 constsimilar
sub_df54 -> sub_e0a8changed0.969/1/810 memsimilar
sub_11edc -> sub_1204cchanged0.997/0/01 call, 1 mem, 1 const, 1 textsimilar
sub_7b08 -> sub_7a9cchanged0.921/2/31 branch, 2 memsimilar
sub_115f0 -> sub_11788changed0.976/16/14 call, 1 callee, 1 mem, 7 const, 2 textsimilar
sub_fef4 -> sub_1005cchanged0.986/3/13 call, 3 jump, 2 mem, 2 constsimilar
sub_73f0 -> sub_73c8changed0.794/21/41 branch, 23 mem, 24 constsimilar
sub_fe74 -> sub_ffd0changed0.893/0/01 call, 2 mem, 1 constsimilar
sub_8044 -> sub_8378changed0.432/86/29 branch, 10 call, 2 jump, 15 mem, 5 textsame references
sub_8d70 -> sub_8edcchanged0.821/0/12 memsimilar
sub_6a94 -> sub_6b14changed0.890/0/17 constsimilar
sub_a34c -> sub_a4bcchanged0.980/8/29 mem, 7 constsimilar
  • sub_6ca8: new
  • sub_806c: new
  • sub_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

BuildRowsRow of the fix
armsr/armv8, AArch64241
armsr/armv7, A32291
malta/be, MIPS32 big endian251
malta/le, MIPS32 little endian251

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.