11 KiB
Provenance of flowrs-transport
Everything in this crate is written for flowRS and licensed MIT OR Apache-2.0. Per-file origins
are listed in tools/runtime-licence.tsv, which tools/runtime-licence-audit.sh checks. This
file records the clean-room rewrite below in more detail than one register row can.
The ESP8266 ROM erase size: src/esptool/esp8266_rom.rs (clean room, 2026-10-27)
Why it was rewritten
The ESP8266 mask-ROM loader erases more flash than FLASH_BEGIN asks for, so a host must hand it
a different number from the image size. flowRS used to carry that rule as
crates/flowrs-boards/src/esp8266_rom.rs. That file was a transcription of
ESP8266ROM.get_erase_size in esptool.py, which is GPL-2.0-or-later. flowRS's porting policy
lets a GPL tool be used as an oracle, never as a source. Commit e4a336ba removed the file. The
module here replaces it. Nobody who had read the GPL code or its transcription wrote it.
How it was written
The implementer was an AI agent (Claude Opus 5.5) working on branch work/cleanroom. It
started from a specification written by a coordinator who had read the old file. The
specification held no code: the function's signature and purpose, and the seven assertions of
the old tests. Under the clean-room rules, the implementer did not open the old file, its git
history, esptool.py's source, or esptool's documentation. It used only the sources below.
| Source | Licence | What was taken |
|---|---|---|
| The coordinator's specification | — | Signature erase_size(offset: u32, size: u32) -> u32, the purpose, and seven pinned values |
ESP8266_RTOS_SDK, commit 858c7c2e: components/spi_flash/include/spi_flash.h |
Apache-2.0 | SPI_FLASH_SEC_SIZE is 4096 |
ESP8266_RTOS_SDK: components/esp8266/include/esp8266/rom_functions.h, components/esp8266/ld/esp8266.rom.ld, components/bootloader/subproject/main/esp8266.bootloader.rom.ld |
Apache-2.0 | The ROM's flash descriptor carries block_size and sector_size; the ROM exports SPIEraseSector |
ESP8266_RTOS_SDK: components/bootloader_support/src/bootloader_init.c |
Apache-2.0 | The ROM flash descriptor is configured with 64 KiB blocks and 4 KiB sectors (in the file's ESP32 branch; it confirms the descriptor's convention, not an ESP8266 value) |
ESP8266_RTOS_SDK: components/spi_flash/src/spi_flash.c, spi_flash_raw.c, include/esp_spi_flash.h, components/esp8266/source/chip_boot.c |
Apache-2.0 | Searched for erase and size constants; nothing beyond the above |
ESP8266_NONOS_SDK, commit 8e138654: ld/eagle.rom.addr.v6.ld, include/spi_flash.h |
ESPRESSIF MIT License (MIT terms, limited to use on Espressif's ESP8266) | The ROM has SPIEraseArea, SPIEraseBlock, SPIEraseSector and SPIEraseChip; its SpiFlashChip carries block_size and sector_size. Facts only; no code is in flowRS |
| esptool.py 5.6.0 and 4.8.1 from PyPI, installed in throwaway virtual environments on the build host | GPL-2.0-or-later | Run as a black-box oracle through tools/esp8266_erase_oracle.py: inputs and outputs only, with tracebacks off so no source line could print. The method's interface, (self, offset, size), was read with inspect.signature |
src/esptool.rs in this crate (the version after commit b1b3f743) |
MIT OR Apache-2.0 | ESP32-family ROMs and the flasher stub take the plain image size; FLASH_SECTOR_SIZE |
Espressif does not document the miscount. One web search (ESP8266 ROM SPIEraseArea sector block erase espressif, with docs.espressif.com excluded) returned a summary that paraphrased
an esptool issue and a Cesanta blog post: an erase that crosses a sixteen-sector block boundary
erases extra head sectors. The implementer did not open those pages. The model below was fitted
to the oracle's outputs, not to that summary.
The rule, derived from the oracle
The implementer tabulated the oracle's output for every start sector within a block (and a few in later blocks) against image sizes of 0 to 40 sectors. It also probed byte-granular sizes and unaligned offsets. The table fits one model exactly.
Count in 4 KiB sectors. Let start be the sector that holds the offset, head the sectors from
start to the end of its 64 KiB block (1 to 16), and handed the sectors FLASH_BEGIN asks for.
The ROM then erases min(2 × handed, handed + head) sectors. To cover an image of n sectors,
hand it the least handed for which that reaches n, which is max(⌈n / 2⌉, n − head). A
partial sector counts as a whole one, and an unaligned offset counts as the start of its sector.
The outputs repeat every sixteen sectors, which confirms the 64 KiB block.
Verification
- Sweep: 52,348,517 inputs, 0 disagreements. Each of esptool.py 5.6.0 and 4.8.1 was piped
into
examples/esp8266_erase_check.rs, which calls flowRS's rule. The inputs were:- every 4 KiB-aligned offset from 0 to 16 MiB (4,097 offsets) against size 0 and every size one below, on, and one above each 4 KiB multiple up to 16 MiB (12,289 sizes);
- 22 × 22 corner values up to 2^32 − 1;
- one million seeded random inputs anywhere in the 32-bit space;
- one million seeded random inputs inside 16 MiB.
- Pinned vectors:
tests/data/esp8266_erase_oracle.tsvholds 396 oracle vectors (18 offsets by 22 sizes), generated bytools/esp8266_erase_oracle.py fixture. The testtests/esp8266_rom_oracle.rschecks every one. - Unit tests in the module: the seven specified values. They also prove that the result is
the least whole-sector count whose modelled erase covers the image, that the ROM over-erases by
one sector only for an odd image no longer than twice
head, and that the largest inputs do not overflow. - Mutation check:
tools/mutate-esp8266-rom.sh.
What the implementer was exposed to, stated plainly
-
The old file, through a search mistake. While looking for the rule's callers, the implementer ran a recursive grep and tried to filter the old file out of the results. The filter expected
./-prefixed paths, which this shell's grep does not print, so it failed. As a result, 15 matching lines of the old file were shown, as follows:- three lines of its provenance header. They said the file was GPL-derived, and that the
flasher stub's erase size is the plain size (a fact
src/esptool.rsalso states); - the first line of the function's doc comment, and its signature (already in the specification);
- the test's name, and a test comment naming a sector-size constant of 0x1000;
- the seven test assertions (already in the specification);
- one comment line inside the function body. Cut off mid-sentence, it said the number sixteen is a literal in esptool.py's code.
No other line of the function body was shown. Every fact in those lines is in the specification, in
src/esptool.rs, or in Espressif's SDKs (4 KiB sectors, 64 KiB blocks, so sixteen sectors to a block). The implementer then deleted the file without opening it and excluded it from every later search. - three lines of its provenance header. They said the file was GPL-derived, and that the
flasher stub's erase size is the plain size (a fact
-
Training data. The implementer is a language model. Its training data may have included esptool.py's public source, and it cannot certify otherwise. In this session the rule came from the oracle's outputs and is expressed differently: a model of the ROM (
rom_erased_sectors) and the closed-form least cover of that model,max(⌈n / 2⌉, n − head), which the tests check against the model. esptool.py was run, not read.
Re-cited to the primary specifications: src/avr.rs and src/dfu.rs (2026-10-27)
The licence audit found that these files cited GPL tools as the place their protocol facts were read: avrdude (GPL-2.0-or-later) for the AVR bootloader protocols, and WebDFU (ISC, itself ported from dfu-util, GPL-2.0-or-later) for DFU. Facts are not copyrightable, but the porting policy wants them cited to the specifications that publish them. Each constant and layout was checked against the documents below only, never against the GPL tools. Every value matched. Each builder emits one byte sequence per command, in the order the specification's command table gives. That table dictates the structure; nothing in it follows a GPL tool's code. The comments now cite the specifications.
| Source | Licence | Checked in this crate |
|---|---|---|
Atmel AVR061, STK500 Communication Protocol (2525B, 04/03), and the command.h Atmel published with it (AVR061.zip, 2002) |
Atmel/Microchip documentation; facts only | avr::stk500v1: Sync_CRC_EOP 0x20; Resp_STK_OK 0x10, FAILED 0x11, INSYNC 0x14, NOSYNC 0x15; commands 0x30, 0x41, 0x50, 0x51, 0x52, 0x55, 0x64, 0x74, 0x75; Parm_STK_SW_MAJOR 0x81. Also the address low byte first, the page size high byte first, and the F memory type |
Atmel AVR068, STK500 Communication Protocol (2591C, 06/06), and its command.h (AVR068.zip, 2005) |
Atmel/Microchip documentation; facts only | avr::stk500v2. Table 3-1 gives the frame: MESSAGE_START 0x1B, sequence, size MSB first, TOKEN 0x0E, body, and an XOR checksum over every byte. The commands are 0x01, 0x06, 0x10 to 0x14 and 0x1B; STATUS_CMD_OK is 0x00 and STATUS_CMD_FAILED 0xC0; the answer body is [answer id, status, …] |
Atmel AVR109, Self Programming (1644G, 06/04), Table 2, and its reference bootloader main.c (AVR109.zip) |
Atmel/Microchip documentation and sample code; facts only | avr::avr109: the command characters and the carriage-return acknowledgement. main.c shows addresses and block sizes read high byte first, and flash addresses counted in words |
| USB-IF, Device Class Specification for Device Firmware Upgrade, revision 1.1 (2004) | USB-IF specification; facts only | dfu: bmRequestType 0x21/0xA1 (Table 3.1) and request codes 0 to 6 (Table 3.2). The block number goes in wValue for DNLOAD (section 6.1.1) and UPLOAD (section 6.2.1). Section 6.1.2 gives the 6-byte GETSTATUS reply with a 3-byte bwPollTimeout, the status codes 0x00 to 0x0F and the states 0 to 10 |
| ST AN3156, USB DFU protocol used in the STM32 bootloader | ST documentation | Cited as DfuSe's specification. Not read in this session: st.com did not answer requests from the build network, and no mirror was reachable. Someone should confirm the DfuSe values against AN3156 itself |
ST's STM32 USB device library, DFU class (STMicroelectronics/stm32-mw-usb-device, Class/DFU/Inc/usbd_dfu.h and Class/DFU/Src/usbd_dfu.c) |
SLA0044; facts only, no code in flowRS | dfu DfuSe: the commands ride on DNLOAD with wBlockNum 0 (GET_COMMANDS 0x00, SET_ADDRESS_POINTER 0x21, ERASE 0x41) and carry a 32-bit little-endian address. Data blocks start at 2, at (wBlockNum − 2) × transfer size past the address pointer. The DFU status and state values agree with DFU 1.1 |
The programmer ids in avr.rs's table (arduino, urclock, wiring, avr109, …) are the
names boards use in Arduino's upload.protocol, which are avrdude's programmer ids. They are
named for interoperability; nothing else from avrdude is used.