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-53921: odhcpd, OpenWrt 24.10.7 to 24.10.8

OpenWrt 24.10.8 takes 34 patches to odhcpd, its DHCP and router advertisement server (OpenWrt commit 67c5b71). Four are for CVEs, and three of those are in the code that answers the IA_NA and IA_PD options of a DHCPv6 client:

  • CVE-2026-53921: build_ia wrote an address past the end of the reply buffer.
  • CVE-2026-53918: dhcpv6_ia_handle_IAs kept a pointer to a lease it had freed.
  • CVE-2026-53922: dhcpv6_ia_handle_IAs took a length less than the header of an IA, and the size underflowed.
  • CVE-2026-53920: a cut-short IA option disclosed stack memory.

GCC inlines build_ia into dhcpv6_ia_handle_IAs, so one function holds the first three. The other patches check realloc, bound the nesting of relayed messages, check the lengths of options, and escape host names in the lease file.

OpenWrt runs sstrip on what it ships, so only the segments of the file are left. On MIPS the binary is MIPS16e and exports dhcpv6_ia_handle_IAs, so the report names it there.

Get the binaries

$ for v in 24.10.7 24.10.8; 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 usr/sbin/odhcpd)
  done

Compare

$ hereticus --format markdown 24.10.7/usr/sbin/odhcpd 24.10.8/usr/sbin/odhcpd
FUNCTIONCHANGESCORE+/-/~TOUCHEDMATCHED BY
sub_13498 -> sub_11ff0changed0.6065/129/841 branch, 5 call, 11 jump, 50 mem, 1 constsame references
sub_11e3c -> sub_123a0changed0.9853/46/88 branch, 6 call, 10 jump, 31 mem, 1 const, 14 textsimilar
sub_9d34 -> sub_9dd8changed0.9823/11/94 branch, 18 mem, 1 constsimilar
sub_fadc -> sub_fd30changed0.9321/0/12 branch, 2 call, 1 jump, 4 memsimilar
sub_e12c -> sub_e338changed0.9721/15/119 branch, 1 jump, 10 mem, 1 textsimilar
sub_d70c -> sub_d8ecchanged0.9821/10/42 call, 20 mem, 6 textsimilar
sub_7230 -> sub_72c4changed0.9919/14/92 branch, 2 call, 1 jump, 19 mem, 2 textsimilar
sub_b868 -> sub_bc6cchanged0.8312/4/01 branch, 6 mem, 1 constsimilar
sub_6ee4 -> sub_6f74changed0.992/0/01 branchsimilar
sub_f6a8 -> sub_f8e8changed0.976/0/02 branch, 2 mem, 1 constsimilar
sub_c300 -> sub_c4ccchanged0.975/4/08 mem, 2 constsimilar
sub_f02c -> sub_f25cchanged0.914/0/01 call, 2 memsimilar
sub_c78c -> sub_c95cchanged0.994/1/11 branch, 1 mem, 1 constsimilar
sub_ac2c -> sub_ad00changed0.993/1/01 branch, 2 mem, 1 constsimilar
sub_93fc -> sub_94a0changed values1.00-2 textidentical shape
sub_d358 -> sub_d538changed values1.00-1 textidentical shape
  • sub_be84: new
  • sub_13a18: new

168 matched (152 unchanged, 16 changed), 0 gone, 2 new

The texts each function points at name the rows:

RowFunctionTexts
sub_13498the relay of router advertisements"Forward a RA on %s"
sub_11e3cthe neighbour and route tables"hop_limit", "rfc9096: ..."
sub_9d34handle_client_request"Got a DHCPv6-request on %s"
sub_fadcndp_setup_interface"/proc/sys/net/ipv6/conf/%s/proxy_ndp"
sub_e12cdhcpv6_ia_handle_IAs, with build_ia"clamping requested PD ...", "IA_PD"
sub_d70cdhcpv6_ia_write_statefile"broken\x20", the lease line
sub_7230config_parse_interface"leasetime"
sub_f6a8handle_solicit"Got a NS for %s on %s"

The fix is on row 5. The four rows above it are larger patches of the same update: the relay of router advertisements alone takes 65 instructions and drops 129.

The other architectures

BuildRowsRow of the fix
armsr/armv8, AArch64185
armsr/armv7, A32201
malta/be, MIPS16e big endian211
malta/le, MIPS16e little endian211

hereticus-cli/tests/truth_cves.rs locks these ranks, and fixtures/cve_binaries.py fetches and pins the binaries.

What this case found

The first reports of this update held two kinds of noise, both fixed:

  • On A32, 18 PLT stubs read as changed. Two new imports moved every slot of the global offset table, and each stub loads its slot at an offset from itself. Two stubs of one import now read as unchanged.
  • On MIPS16e, the lazy binding stubs of MIPS32, which sstrip leaves no section to find, decoded as MIPS16e and ran together: 14 read as gone and 18 as new. They are now read as MIPS32, 16 bytes each, and named for the dynamic symbol each resolves, as strcmp@plt.