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_iawrote an address past the end of the reply buffer. - CVE-2026-53918:
dhcpv6_ia_handle_IAskept a pointer to a lease it had freed. - CVE-2026-53922:
dhcpv6_ia_handle_IAstook 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
| FUNCTION | CHANGE | SCORE | +/-/~ | TOUCHED | MATCHED BY |
|---|---|---|---|---|---|
sub_13498 -> sub_11ff0 | changed | 0.60 | 65/129/8 | 41 branch, 5 call, 11 jump, 50 mem, 1 const | same references |
sub_11e3c -> sub_123a0 | changed | 0.98 | 53/46/8 | 8 branch, 6 call, 10 jump, 31 mem, 1 const, 14 text | similar |
sub_9d34 -> sub_9dd8 | changed | 0.98 | 23/11/9 | 4 branch, 18 mem, 1 const | similar |
sub_fadc -> sub_fd30 | changed | 0.93 | 21/0/1 | 2 branch, 2 call, 1 jump, 4 mem | similar |
sub_e12c -> sub_e338 | changed | 0.97 | 21/15/11 | 9 branch, 1 jump, 10 mem, 1 text | similar |
sub_d70c -> sub_d8ec | changed | 0.98 | 21/10/4 | 2 call, 20 mem, 6 text | similar |
sub_7230 -> sub_72c4 | changed | 0.99 | 19/14/9 | 2 branch, 2 call, 1 jump, 19 mem, 2 text | similar |
sub_b868 -> sub_bc6c | changed | 0.83 | 12/4/0 | 1 branch, 6 mem, 1 const | similar |
sub_6ee4 -> sub_6f74 | changed | 0.99 | 2/0/0 | 1 branch | similar |
sub_f6a8 -> sub_f8e8 | changed | 0.97 | 6/0/0 | 2 branch, 2 mem, 1 const | similar |
sub_c300 -> sub_c4cc | changed | 0.97 | 5/4/0 | 8 mem, 2 const | similar |
sub_f02c -> sub_f25c | changed | 0.91 | 4/0/0 | 1 call, 2 mem | similar |
sub_c78c -> sub_c95c | changed | 0.99 | 4/1/1 | 1 branch, 1 mem, 1 const | similar |
sub_ac2c -> sub_ad00 | changed | 0.99 | 3/1/0 | 1 branch, 2 mem, 1 const | similar |
sub_93fc -> sub_94a0 | changed values | 1.00 | - | 2 text | identical shape |
sub_d358 -> sub_d538 | changed values | 1.00 | - | 1 text | identical shape |
sub_be84: newsub_13a18: new
168 matched (152 unchanged, 16 changed), 0 gone, 2 new
The texts each function points at name the rows:
| Row | Function | Texts |
|---|---|---|
sub_13498 | the relay of router advertisements | "Forward a RA on %s" |
sub_11e3c | the neighbour and route tables | "hop_limit", "rfc9096: ..." |
sub_9d34 | handle_client_request | "Got a DHCPv6-request on %s" |
sub_fadc | ndp_setup_interface | "/proc/sys/net/ipv6/conf/%s/proxy_ndp" |
sub_e12c | dhcpv6_ia_handle_IAs, with build_ia | "clamping requested PD ...", "IA_PD" |
sub_d70c | dhcpv6_ia_write_statefile | "broken\x20", the lease line |
sub_7230 | config_parse_interface | "leasetime" |
sub_f6a8 | handle_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
| Build | Rows | Row of the fix |
|---|---|---|
armsr/armv8, AArch64 | 18 | 5 |
armsr/armv7, A32 | 20 | 1 |
malta/be, MIPS16e big endian | 21 | 1 |
malta/le, MIPS16e little endian | 21 | 1 |
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
sstripleaves 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, asstrcmp@plt.