How much of OpenWrt actually ships with stack canaries?
Stack canaries are a cheap defense against stack buffer overflows, and it is easy to assume small router firmware skips them to save space. I wanted a number instead of an assumption, so I measured it directly on OpenWrt 23.05 across three architectures.
The result
OpenWrt builds stack protection in by default. It is not close to zero, and the coverage is steady across very different targets.
Every executable in all three images was measurable, so none of these numbers hide behind an unknown bucket. Position independent executables told a different story (roughly a third), which is worth its own look, but canaries are clearly the default.
The interesting part is what opts out
The binaries without canaries are not scattered at random. The same set turns up on all three architectures. Mapping each one back to the package that shipped it makes the pattern obvious:
| Package | Binaries without canaries |
|---|---|
| procd | askfirst, udevtrigger, ujail, utrace |
| fstools | mount_root, jffs2reset |
| ubus / uci / ubox | ubus, uci, validate_data |
| logd | logd |
| nftables | nft |
These are OpenWrt's own core system packages, the init and message bus layer that runs on every device. The variable that predicts whether a binary has a canary is the package it came from, not the CPU it was built for. That reframes the finding. It is not a platform limitation and not a toolchain-wide default being off. It is a small, fixed set of first party packages that miss a mitigation the rest of the system enables, consistently, on every build. That is the kind of gap that is fixable upstream.
The measurement bug, which is the real lesson
sstrip, which removes ELF section headers. The scanner read the canary symbol through those section headers, so with them gone it saw no symbols and recorded every binary as having no canary. A clean looking zero that was pure instrument error.
The fix was to stop depending on section headers and read the dynamic symbol table straight from the program headers, which survive stripping. The result also became three valued: present, absent, or unknown, so a binary whose symbols genuinely cannot be read is reported as unknown rather than as a false absent. Reading a symbol table two different ways and getting two different answers is exactly why a scanner needs to prove what it claims and admit what it could not measure.
All of the numbers here were produced by the corrected tooling, and each finding stores the bytes it was derived from so the verdict re-checks offline. The tool is Sentinel.