Skip to content

Esoteric: DFU push of a tiny app firmware fails on second iteration (Espruino SDK12, nRF52832) #2708

@thandal

Description

@thandal

On nRF52832 with Espruino's SDK12 DFU bootloader, DFU-pushing a sub-page-sized application (≲4 KB) twice in succession produces a corrupted bank_1 staging region. The first push works; the second push completes successfully according to the Web IDE, and the device reboots but immediately re-enters DfuTarg and stays there — the new app's reset vector lands at a corrupted address and HardFaults on boot.

The corrupted bytes at the bank_1 staging address (0x20000 when bank_0 is a 1-page app) are a strict bit-subset of the expected .bin bytes — NVMC's 1→0-only programming applied to two writes at the same flash address.

To Reproduce:

  1. Flash an MDBT42Q with stock Espruino: SoftDevice S132 v3.1.0 + Espruino SDK12 bootloader + Espruino app firmware (~276 KB).
  2. Build a ~1.9 KB application that fits in one flash page. Package as a signed DFU zip:
    nrfutil pkg generate --application-version 0xff --hw-version 52 --sd-req 0x8C,0x91 ...
    
  3. Enter DFU mode (BTN1-hold + power-cycle on the MDBT42Q breakout). Push the zip via Web IDE. Succeeds.
  4. Re-enter DFU the same way, push the same zip again. Tool reports success.
  5. Device is stuck in DfuTarg. LED stuck on (corrupted reset vector faults immediately).

SWD-read at 0x20000 to confirm:

expected (.bin first 32 bytes):
  20010000 0001F629 0001F651 0001F653 0001F655 0001F657 0001F659 00000000

observed after failed push #2:
  00000000 0001F628 0000B000 0000B400 0000F654 00002000 00004610 00000000

For every word, observed & expected == observed — the NVMC AND-of-two-writes signature.

One possible mechanism:

dfu_req_handling.c receives DFU data into a 4-slot cyclic buffer (m_data_buf[4][256]) and queues each filled slot to fstorage via nrf_dfu_flash_store(), passing a pointer to the slot rather than a copy. On a small transfer that fits in fewer chunks than fstorage can drain in time, the producer wraps the buffer index and overwrites a slot whose earlier store is still in flight. This is one plausible path to two writes landing at the same flash address with different intended payloads.

A targeted fix would copy each chunk into a dedicated single-owner buffer before calling nrf_dfu_flash_store(), or otherwise gate buffer reuse on m_flash_operations_pending draining. SDK15's nrf_dfu_req_handler.c rewrite addresses this class architecturally.

Related / adjacent reports

Workaround

Pad the application past ~70 KB (not sure the exact size necessary) before packaging the DFU zip. Larger transfers don't exhibit the bug. Use a forced .rodata array referenced via a runtime-indexed read so the compiler/linker can't strip it:

const uint8_t pad[80 * 1024] = { 0xAA };  // first byte non-zero forces .rodata, not .bss

// in main():
pad_keepalive = pad[NRF_FICR->DEVICEID[0] % sizeof(pad)];  // runtime index defeats constant folding

Three back-to-back DFU pushes of an >80 KB app empirically work; the same code without padding fails 100% on the second push.

Environment

  • Chip: nRF52832 (MDBT42Q module)
  • SoftDevice: S132 v3.1.0 (FWID 0x8C, also 0x91)
  • SDK: nRF5 SDK 12
  • Bootloader source: Espruino/targets/nrf5x_dfu/sdk12/
  • Tools: nrfutil ≥ 8.x, Espruino Web IDE for DFU uploads

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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