- overflow wifi_pass, corrupt the BLE client's mainloop entry
opcode 0x05 (wifi password) gets its own 500-byte buffer starting at BSS offset 502 from wifi_ssid. from the start of wifi_pass, mainloop_list[7] (the entry for the BLE client's fd) sits 582 bytes away. corrupt ML[7] with a fake struct, disconnect BLE, and the disconnect handler walks mainloop_list, hits ML[7], calls destroy(user_data) through our controlled data. clean kill.
except the math doesn't work. macOS BLE caps each write at roughly 55 bytes of PSK payload. realistically you get maybe 10 writes per connection, so ~550 bytes total budget. ML[7] is at 582. 32 bytes short. ML[2] (offset 542) and ML[3] (offset 550) are reachable, but those are stderr and a log file, fds that never get removed from the epoll set during disconnect. the disconnect path won't touch them. dead end.
- send SIGTERM after corrupting mainloop_list
new angle: corrupt ML[2] via the overflow, then externally send SIGTERM to btgatt-server. the signal handler sets epoll_terminate = 1, the event loop exits, the cleanup loop runs, ML[2] fires through our fake struct.
problem: btgatt-server doesn't use a normal signal handler for SIGTERM. it uses signalfd, which delivers signals as readable events on a file descriptor instead of interrupting the process. that fd only gets polled inside mainloop_run's epoll_wait call. after the BLE client disconnects, the process returns from mainloop_run and blocks on accept(), waiting for the next connection. accept() doesn't poll the signalfd. SIGTERM arrives, gets queued in the signalfd buffer, and just... sits there unread. the cleanup loop never fires. dead end.
- target ML[5], the BLE listener socket
the listener fd (the one that accepts new connections) lives at mainloop_list[5]. when a new BLE client connects, this fd goes readable and the event loop dispatches through ML[5]'s callback. corrupt ML[5] with a fake struct and the next connection's accept event calls system() instead of the real handler.
ML[5] is 566 bytes from the start of wifi_pass. 16 bytes past the 550-byte write budget. tried bumping chunk size to 55 bytes to squeeze more data per write, but that pushes the outer BLE frame (after AES-GCM encryption, nonce, tag, checksum) past the 97-byte ATT MTU. the BLE stack rejects the write before it even reaches the robot. dead end.
- use wifi_ssid instead
switch from opcode 0x05 (password) to opcode 0x04 (SSID). the SSID buffer starts at BSS offset 0, so it's closer to mainloop_list. ML[5] is 572 bytes from wifi_ssid, within range across 11 chunks. but: mainloop_init() zeros the entire mainloop_list array at the start of each BLE connection. corrupt ML[5] during connection 1, the corruption gets wiped by mainloop_init() before connection 2's event loop starts. gone before the listener fd ever generates an event.
this is where the real constraint clicked. the corruption and the trigger have to happen in the same connection. mainloop_init() clears ML on every new connection, but it doesn't touch the wifi buffers. overflow data in wifi_ssid survives across connections. the ML entries don't. so whatever you corrupt in ML and whatever event you use to trigger it both need to happen within a single mainloop_run call.