Skip to content

SpamAP on M5 Stick S3 #2618

Description

@denny4-user

Bug report for Bruce maintainers — Beacon/AP spam broken since 1.15; root cause is -flto

This file is a deliverable (a bug-report write-up), not an implementation plan.
The fix is already applied and confirmed in the downstream fork; nothing needs to
be implemented here. The text below is meant to be sent to the upstream developers
(relates to issue #2608).


TL;DR

Beacon / AP spam (WiFi → Beacon Spam) injects frames that a monitor-mode sniffer
can see, but phones and laptops never list the spoofed SSIDs. It works on 1.14
and is broken on 1.15 / beta / current dev. The beacon code is byte-for-byte
identical between 1.14 and dev, so it is not a beacon-code bug. The regression
is caused by the -flto build flag that dev added after 1.14 (1.14 does not use
LTO). Disabling -flto fixes it with zero source changes. Deterministic.

Relates to: #2608


Affected

  • Reproduced on M5Stack Stick S3 (m5stack-sticks3, ESP32-S3, 8 MB, OPI PSRAM).
  • Beacon spam not working #2608 reports it firmware-wide (also LilyGo T-Embed CC1101 Plus). A build-flag root
    cause is consistent with "affects every board".
  • Works: 1.14 (released 2026-02-01). Broken: 1.15 (2026-05-25), beta, dev.

Symptom

  • WiFi → Beacon Spam (Random / Funny / Rickroll / Custom): the device transmits the
    beacons (visible on a monitor-mode capture), but no client lists the spoofed APs.
  • Key tell: it works while a USB serial monitor is attached and reading CDC;
    it fails standalone — on battery, and even on a plain USB charger with no data
    host. (This is what sent us down the wrong path for a long time.)

What it is NOT

  • Not the frame contents. prepareBeaconPacket() and the template are unchanged
    from 1.14; the emitted frame is a well-formed WPA2 beacon.
  • Not the WiFi setup. During the failure: WiFi.getMode() == WIFI_MODE_APSTA,
    the WIFI_ATK_NAME softAP is up, and esp_wifi_get_channel() matches the requested
    hopping channel.
  • Not power. Fails on a stable 5 V USB charger (no host) exactly like on battery.
  • Not modem sleep. esp_wifi_set_ps(WIFI_PS_NONE) did not help.
  • Not ESP_ERR_NO_MEM per se — that is a downstream symptom (see below).

Diagnostic evidence

We instrumented beaconSpamList() / beaconSpamSingle() to log, once per second, the
esp_wifi_80211_tx() return code, requested vs actual channel, WiFi mode and softAP
SSID.

  • Working case (serial host attached), typical line:
    mode=3 reqCh=5 actCh=5 txOK≈600/s txFAIL≈15–25% lastErr=0x101(ESP_ERR_NO_MEM) apSSID=BruceAttack
    → APs appear on the phone.
  • ESP_ERR_NO_MEM (WiFi TX buffer pool momentarily exhausted under fast injection) is
    present at the same rate in both the working and failing cases, so it is not
    the discriminator. It is a consequence of the driver not draining its TX queue, not
    the cause of client-side invisibility.
  • Decisive observation: adding a once-per-second on-screen status draw (an
    SPI/TFT transaction inside the injection loop) made it work standalone; removing
    it broke it again. Symmetrically, the serial logging's once-per-second printf (a
    blocking USB-CDC write when a host is attached) is exactly what made the
    "works with serial" case work. ⇒ the injection loop needs a periodic blocking
    pause
    for the queued frames to actually be aired.
  • Dead end worth documenting: retrying esp_wifi_80211_tx() on ESP_ERR_NO_MEM
    made it strictly worse — a 1 ms yield per congested frame slows the per-SSID
    cycle so much that clients (which dwell only ~100–300 ms per channel during a passive
    scan) catch too few SSIDs to list any. Do not "fix" the NO_MEM by retrying.

Root cause: -flto

  • The only relevant build difference between 1.14 and dev is that dev's
    platformio.ini adds -flto (plus -ffunction-sections / -fdata-sections);
    1.14 does not use LTO.
  • Bisect result: with the beacon code left byte-identical to 1.14 and -flto
    disabled
    , beacon spam works standalone (battery and USB charger, no serial host,
    no on-screen "crutch"). Re-enabling -flto reproduces the failure. This is
    deterministic and requires no source changes.

Mechanism (hypothesis — the bisect result above is certain, this part is inferred)

With LTO, the src/-side injection loop is inlined/optimized aggressively enough that
the Arduino loopTask (core 1) starves the WiFi driver task: the driver never gets a
window to push the queued management frames onto the air. The existing per-packet
vTaskDelay(1) is evidently insufficient under LTO. Any coarser periodic pause — a
blocking USB-CDC write, or an SPI display update — yields long enough for the driver to
flush its TX queue, which is why both "crutches" and an attached serial monitor masked
the bug. Without LTO the loop's timing matches 1.14 and the frames are aired normally.

Fix used downstream

Disable -flto in platformio.ini; beacon code unchanged from 1.14.
Cost: firmware grows from ~81 % → ~91 % of the 8 MB OTA partition on m5stack-sticks3
(LTO was buying ~10 % flash). RAM unaffected.

Suggested upstream fix (better than dropping LTO globally)

Since dropping LTO globally costs flash (and a little perf), a targeted fix is likely
preferable. Options, in rough order of preference:

  1. Add an explicit, LTO-proof pace/yield in the beacon injection loop — a small
    periodic vTaskDelay/taskYIELD at a coarse cadence (once per SSID-list pass,
    or every N frames), not per-frame and not gated on NO_MEM (per-frame
    pacing slows the SSID cycle and hides the SSIDs from scanners — see the dead end
    above).
  2. Ensure the WiFi driver/TX task gets CPU (task priority / core placement) so an
    LTO-tightened loop can't starve it.
  3. Keep LTO globally but exclude the injection code — either an optimize/noinline
    attribute on the beacon-inject function(s), or disable LTO for the wifi_atks
    translation unit only.

Reproduction

  1. Build dev (or use an affected 1.15/beta release) for m5stack-sticks3.
  2. Flash, then unplug from any computer — run on battery or a dumb USB charger
    (no serial host).
  3. WiFi → Beacon Spam → Random. Scan on a phone: no spoofed APs (a monitor-mode
    sniffer still shows the frames on the air).
  4. Rebuild with -flto removed from platformio.ini build_flags; reflash; repeat
    2–3 → the spoofed APs now appear. No source changes required.

Environment

  • Target: m5stack-sticks3 (ESP32-S3, 8 MB, OPI PSRAM), ARDUINO_RUNNING_CORE=1,
    CONFIG_ASYNC_TCP_RUNNING_CORE=1, FreeRTOS HZ 1000.
  • Toolchain (identical for 1.14 source and dev — so this is not a framework-version
    difference): pioarduino platform-espressif32 55.03.36 (Arduino 3.3.6 / IDF 5.5),
    framework libs bruce_esp32-arduino-libs-20260123.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions