Repository navigation
Sera Finding #5 #2647
Description
Description
ZD-BLUEZ-MESH-PBADV-001 — Mesh PB-ADV SAR START: unchecked copy size -> attacker-controlled heap OOB WRITE (mesh/pb-adv.c:430) + silent in-chunk corruption of session->user_data
AI-exclusive finding · BlueZ 5.85 (fixture) · upstream 5.87-era source checked 2026-10-09
TL;DR
pb_adv_packet() (mesh/pb-adv.c:300) never validates len (no >= 7 guard
before pkt += 7; len -= 7), and the SAR START branch checks the DECLARED
length but not the COPY length:
session->exp_len = l_get_be16(pkt);
if (session->exp_len > sizeof(session->sar)) return; /* <= 80 checked */
...
memcpy(session->sar, pkt + 3, len - 3); /* len-3 NEVER checked */session->sar is 80 bytes inside a 168-byte heap object (l_new via
pb_adv_reg). Copy size = L - 10 where L = advertising-packet length.
Measured (real pb-adv.o + real pb_adv_reg acceptor flow, ASAN+UBSAN):
| L (AD len) | copy | result |
|---|---|---|
| 31 (legacy adv max) | 21 | control: clean, rc=0 |
| 100 | 90 | survives — silent in-chunk corruption (uuid region) |
| 112 | 102 | silent: user_data=0x41414141 delivered to close_cb |
| 116 | 106 | silent: user_data=0x4141414141414141 delivered to close_cb |
| 117 | 107 | ASAN heap-buffer-overflow WRITE, 0 bytes past the 168-B chunk |
| 255 | 245 | WRITE of size 245 at mesh/pb-adv.c:430 |
Attacker controls: write distance (L-90 bytes past sar), write content
(full packet payload), and whether the write stays inside the object (silent)
or crosses into the next heap allocation.
1. Metadata
| Field | Value |
|---|---|
| Title | PB-ADV SAR START memcpy len-3 unchecked -> CWE-787 heap OOB |
| write; silent in-chunk corruption injects user_data pointer | |
| Component | BlueZ mesh/pb-adv.c pb_adv_packet :300 (entry), :430 |
(write); mesh.c prov_rx :132 (no length guard); filter = |
|
AD-type byte only (prov_filter[]={BT_AD_MESH_PROV}) |
|
| Affected | BlueZ 5.85 fixture; same code present on upstream master |
| (checked 2026-10-09) | |
| Fixed in | NONE |
| CVE | NONE assigned (checked NVD/OSV 2026-10-09) |
| CWE | CWE-787 (heap OOB write); entries = CWE-119/20 family; |
| in-chunk variant = CWE-131/corruption of object fields | |
| Severity | 8.1 HIGH (conditional class) — CVSS:3.1/AV:A/AC:H/PR:N/ |
| UI:N/S:U/C:H/I:H/A:H — AV:A (advertising, no pairing) but | |
| AC:H: overflow needs an AD >90 B, i.e. EXTENDED advertising | |
| delivery (BLE5); legacy 31-B ads cannot overflow (control). | |
| Proven write; RCE NOT established (see H7 note). | |
| Reporter | seraphine AI agent |
| First hit | 2026-10-09 |
| Build/flags | gcc -O1 -g -fno-omit-frame-pointer -fsanitize=address, |
undefined; real mesh/pb-adv.c (-Ilib), libell 0.83-1 |
2. Reachability (honest, per-path)
- Unguarded chain (source-proven): advertising AD type 0x29 → mesh-io
filter (type match only, no length) →mesh.c:prov_rx(forwardsdata,len
with no check) →pb_adv_packet(nolen>=7, no copy bound). - Overflow precondition:
L > 90. Legacy advertising caps at 31 B → no
overflow. Requires an extended-advertising delivery of 91..255 B to
prov_rx(BLE5 controllers / mesh-io paths that pass long ads). Labelled
AC:H for exactly this reason. If a deployment's mesh-io never delivers
long ads, that instance is not reachable — the CODE defect stands regardless. - Secondary candidate (not proven):
len < 7underflowslen -= 7
(uint16) →len-3wraps to ~64 KB → huge memcpy; the header reads
(pkt+1..+6) then come from adjacent heap, sotype/link_idare
nondeterministic — kept CANDIDATE, not claimed. - Open flow (all local, no radio): acceptor
pb_adv_reg(false, ...),
23-byte OPEN (uuid match), then the START packet — no daemon needed.
3. Steps to reproduce
bash /home/box/BTHunt/orch/build_pb.sh # real pb-adv.o, ASAN+UBSAN
bash /home/box/BTHunt/orch/pb/run_pb.sh # control + triggers
# silent tier:
/home/box/BTHunt/orch/pb/pb_harness corrupt 116 # user_data=0x4141414141414141
# write tier:
/home/box/BTHunt/orch/pb/pb_harness trigger 255 # ASAN WRITE at pb-adv.c:4304. Differential (authoritative, this box, 2026-10-09)
| input | result |
|---|---|
| control L=31 | rc=0, OPEN fired, no corruption |
| corrupt L=108 (window edge) | rc=0, user_data untouched (nil) |
| corrupt L=112 | rc=0, close_cb got 0x41414141 |
| corrupt L=116 | rc=0, close_cb got 0x4141414141414141 |
| trigger L=117 | ASAN WRITE 107, 0 B past 168-B chunk |
| trigger L=255 | ASAN WRITE 245 at mesh/pb-adv.c:430 |
5. Impact — and the H7 verdict (removed)
- PROVEN (H6): attacker-controlled heap OOB write (size, content, offset)
- silent object-field corruption that injects an arbitrary 8-byte value
intosession->user_data, which is then passed to provisioner callbacks.
- silent object-field corruption that injects an arbitrary 8-byte value
- H7 REMOVED (guarded): every callback examined dereferences
user_data
only AFTER a pointer-equality check against a file-static global
(acp_prov_close/open:if (rx_prov != prov) return;— prov-acceptor.c:108/162;
srv_close/open:if (prov != rpb_prov || ...) return;— remprv-server.c:167/102).
The compare short-circuits before any deref, so a corrupted pointer neither
derefs nor steers the subsequent access (which uses the global, not the
argument). Making the check PASS would additionally require leaking the
target's heap address. No arbitrary-deref / control-flow edge established;
adjacent-chunk object corruption (L>=117) is a candidate only. - Honest chain state: H6 write = terminal verified rung.
6. Fix (suggested)
if (len < 7) return; /* before pkt+=7; len-=7 */
...
size_t copy = (size_t)len - 3;
if (len < 3 || offset + copy > sizeof(session->sar)) return;
memcpy(session->sar, pkt + 3, copy);plus a defensive len >= 7 assert at every entry from mesh-core.
7. References
mesh/pb-adv.c:300-460(entry,len-=7:326, START copy :430, cont copy
:445 with the off-by-3 guardoffset+len-3vs copy oflen);
mesh/pb-adv.hpublicpb_adv_reg;
mesh/mesh.c:132-153prov_rx (no guard) +prov_filter[]={BT_AD_MESH_PROV};
mesh/prov-acceptor.c:108/162,mesh/remprv-server.c:102/167(callback guards)- struct layout:
pb_adv_session168 B,sar[80]mid-object,user_datalast - Sender honors
PB_ADV_MTU 24(pb-adv.c:62) — receiver never enforces an
equivalent: TX-side discipline does not protect RX. - Candidate sourced from wave1 remote-disclosure agent (GATT Server Callbacks/Events #3 in
wave1-remote-disclosure.md); execution here. - Harness:
/home/box/BTHunt/orch/pb/{pb_harness.c,run_pb.sh},
object/home/box/BTHunt/orch/pb/pb-adv.o
8. Method note
Bottom-up: real object file, real registration flow (pb_adv_reg → recorded
mesh_reg_prov_rx callback), acceptor OPEN established before the trigger;
one variable per run (L); control at legacy maximum; silent window found by
sweep (108 clean / 112 corrupt / 117 first ASAN) rather than asserted; H7
candidate tested by reading every registered callback's body before deciding
— removed on evidence, not kept on narrative.
To reproduce
No response
btmon trace
No response
Analysis focus
None
Privacy
-
Skip anonymization — Send the raw decoded trace to the LLM without scrubbing MAC addresses or device names.
-
I understand this trace will be processed by a third-party LLM API and I have the right to share this data.
Other logs
Versions
- BlueZ version:
- Kernel version:
- Problematic device:
Activity