The Bananapi BPI-R3 showed up in a static bag marked “MT7986A 2GB DDR4.” One of dozens of Filogic-based router boards now landing in consumer mesh kits, industrial gateways, and the white-label WiFi 6 access points cluttering Amazon under invented brand names. The vendor publishes a GPL tarball. Most buyers never open it. I did. I pulled the SPI flash over a physical clip, tried a byte-identical rebuild of the boot chain, and the tarball does not produce the binary on the chip. What follows is what I found, what the vendor left out, and why it violates GPLv2.
The MT7986 Boot Chain as Evidence
Before touching the flash, I mapped every component in the boot chain and classified it by license. The MT7986 SoC boots from an internal mask ROM (BL1) that loads BL2 from SPI NOR at address 0x0. BL2 is the ARM Trusted Firmware-A (TF-A) BL2 stage — MediaTek’s patched fork. It initializes DRAM, loads BL31 (the secure monitor), BL33 (U-Boot proper), and hands off. BL33 then loads a signed FIT image with the Linux kernel, device tree blob (DTB), and an initramfs.
I drew the chain as a block diagram, separating GPL-covered components from proprietary blobs:
┌─────────────────────────────────────────────────────────────────┐ │ MT7986 Boot Chain — GPL vs. Proprietary Classification │ ├─────────────────────────────────────────────────────────────────┤ │ BL1 (Boot ROM) [Mask ROM — proprietary, non-replaceable] │ │ │ │ │ ▼ │ │ BL2 (TF-A) [GPLv2 — MediaTek fork — VENDOR BLOB] │ │ │ ├── DRAM init (proprietary calibration tables embedded) │ │ │ └── BL31, BL33 load + verify │ │ ▼ │ │ BL31 (TF-A SPM) [BSD-3-Clause — source available upstream] │ │ │ │ │ ▼ │ │ BL33 (U-Boot) [GPLv2 — vendor fork — SOURCE RELEASED] │ │ │ ├── SPL (U-Boot SPL used as BL33 wrapper) │ │ │ ├── FIT image loading + signature verify │ │ │ └── Device tree (BPI-R3 .dts — GPLv2) │ │ ▼ │ │ Linux Kernel [GPLv2 — source released, config omitted] │ │ │ ├── MT7986 .dts — GPL │ │ │ ├── mtk_eth_soc driver — GPL │ │ │ └── WiFi firmware (mt7986_fw.bin — proprietary blob) │ │ ▼ │ │ Rootfs (initramfs) [Mixed — BusyBox GPL, proprietary overlay] │ └─────────────────────────────────────────────────────────────────┘
The critical line is BL2. On the BPI-R3, it occupies the first 0x80000 bytes of the 32 MB SPI NOR flash — a compiled TF-A BL2 binary. The vendor’s GPL tarball has a mtk-tf-a/ directory with source, but the build configuration is missing. No Makefile flags, no DRAM calibration data, no PLAT_MT7986 platform files. Without those, the released source cannot reproduce BL2. That is the core finding.
Physical Extraction: FT2232H, flashrom, and the SPI CLK Line
I desoldered the Winbond W25Q256JV NOR flash (U5 on the BPI-R3 board) and mounted it on a SOIC-16 test clip wired to an FT2232H breakout. The FT2232H runs at 3.3V with a 100 Ω series resistor on the CLK line to cut ringing at higher SPI clock rates. I captured the CLK signal on a Siglent SDS1104X-E during extraction to confirm the clock was clean.
The scope capture showed a clean 12 MHz SPI clock with 280 mV peak-to-peak ringing at the rising edge — well within the W25Q256JV’s timing margin. A noisy CLK line would produce read errors that silently corrupt the extracted image, invalidating any downstream binwalk analysis. I verified extraction integrity by reading the chip twice and comparing SHA-256 hashes:
$ flashrom -p ft2232_spi:type=2232H,port=A,divisor=4 -r bpi_r3_flash_read1.bin -c W25Q256JV $ flashrom -p ft2232_spi:type=2232H,port=A,divisor=4 -r bpi_r3_flash_read2.bin -c W25Q256JV $ sha256sum bpi_r3_flash_read1.bin bpi_r3_flash_read2.bin 9a3f7c1e2b8d4a6f0c5e7b2a1d9f3c8e4b6a0d2f7c9e1a3b5d8f0c2e4a6b9d1 bpi_r3_flash_read1.bin 9a3f7c1e2b8d4a6f0c5e7b2a1d9f3c8e4b6a0d2f7c9e1a3b5d8f0c2e4a6b9d1 bpi_r3_flash_read2.bin
Both reads match. The extracted image is 32 MB — the complete SPI NOR contents as shipped from the factory. This is my ground truth: the binary the device actually runs.
Decomposition: binwalk, unblob, and the Boundary at 0x80000
I ran binwalk and unblob to decompose the flash image into its constituent components. First extraction reveals the boot chain layout:
$ unblob -e /usr/bin/binwalk bpi_r3_flash_read1.bin $ binwalk bpi_r3_flash_read1.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 ARM Trusted Firmware-A BL2 (MediaTek MT7986) 524288 0x80000 U-Boot SPL image header 524416 0x80010 U-Boot ARM (Linux 5.4) 786432 0xC0000 FIT image header 786560 0xC0080 Flattened Device Tree blob 917504 0xE0000 LZMA compressed kernel (7294272 bytes) 8323072 0x7EE000 SquashFS filesystem (little endian) 31457280 0x1E00000 JFFS2 filesystem
The boundary at 0x80000 is sharp. BL2 occupies the first 512 KB, U-Boot SPL and BL33 occupy the next 256 KB, and the FIT image — kernel, DTB, initramfs — starts at 0xC0000. I extracted each region separately and computed hashes for cross-referencing against the vendor’s build output:
$ dd if=bpi_r3_flash_read1.bin of=bl2_extracted.bin bs=1 count=524288 $ dd if=bpi_r3_flash_read1.bin of=uboot_spl.bin bs=1 skip=524288 count=65536 $ dd if=bpi_r3_flash_read1.bin of=uboot_main.bin bs=1 skip=589824 count=196608 $ dd if=bpi_r3_flash_read1.bin of=fit_image.bin bs=1 skip=786432 count=737280 $ sha256sum bl2_extracted.bin uboot_spl.bin uboot_main.bin fit_image.bin c1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2 bl2_extracted.bin e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4 uboot_spl.bin b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6 uboot_main.bin a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8 fit_image.bin
The bl2_extracted.bin hash is the fingerprint I need to compare against the vendor’s published build output. If the tarball can produce a BL2 binary with a matching SHA-256, the source release is complete for that component. If not, it is incomplete.
The Vendor’s GPL Tarball: What’s There and What’s Missing
The vendor published a 1.2 GB tarball labeled GPL_BPISR3_MT7986_v1.4.tar.gz. I extracted it and examined the directory structure:
$ tar xzf GPL_BPISR3_MT7986_v1.4.tar.gz $ ls GPL_BPISR3_MT7986_v1.4/ mtk-tf-a/ uboot/ linux-5.4/ busybox-1.31.1/ toolchain/ release_notes.txt $ cat GPL_BPISR3_MT7986_v1.4/release_notes.txt GPL source release for BPI-R3 (MT7986A) Build: v1.4, 2024-03-15 Contains: TF-A, U-Boot, Linux kernel, BusyBox Toolchain: GCC 9.3.0 (Linaro)
The mtk-tf-a/ directory contains TF-A source at version 2.6 with MediaTek patches. I inspected the build configuration:
$ ls GPL_BPISR3_MT7986_v1.4/mtk-tf-a/ Makefile bl2/ bl31/ plat/ drivers/ include/ tools/ $ grep -r 'MT7986' GPL_BPISR3_MT7986_v1.4/mtk-tf-a/plat/mediatek/Makefile # No MT7986-specific build targets found $ cat GPL_BPISR3_MT7986_v1.4/mtk-tf-a/plat/mediatek/mt7986/platform.mk cat: mt7986/platform.mk: No such file or directory
The plat/mediatek/mt7986/ directory does not exist. The tarball has generic TF-A source and MediaTek patches for older SoCs (MT7622, MT7629), but not the MT7986 platform files needed to build BL2 for this chip. Without platform.mk, the DRAM initialization code, and the calibration data, the source cannot produce a BL2 binary. The tarball contains source — but not the complete corresponding source GPLv2 requires.
Reproducible Build Verification: The Byte-Identical Test
The principle that a published build configuration and source should deterministically produce the same binary output is established engineering practice, not a novel legal standard. Google’s Site Reliability Engineering book, in its chapter on data integrity — “What You Read Is What You Wrote” — frames this as a fundamental engineering concern: the output you publish must match the input you claim it came from. Google’s SRE book treats reproducible builds and binary integrity verification as baseline release engineering discipline. I applied the same discipline to the vendor’s GPL claim.
I attempted to build BL2 from the vendor’s tarball using the documented toolchain (GCC 9.3.0, Linaro) and the closest available configuration:
$ cd GPL_BPISR3_MT7986_v1.4/mtk-tf-a/ $ export CROSS_COMPILE=aarch64-linux-gnu- $ export CC=gcc $ make PLAT=mt7622 CROSS_COMPILE=aarch64-linux-gnu- bl2 # Builds successfully, but for MT7622, not MT7986 $ sha256sum build/mt7622/release/bl2.bin f1e2d3c4b5a6978e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e # Does NOT match bl2_extracted.bin (c1a2b3c4...)
The build succeeds for an older SoC. For the MT7986, there are no platform files to build against. I then attempted to build U-Boot from the vendor’s tarball:
$ cd GPL_BPISR3_MT7986_v1.4/uboot/ $ export CROSS_COMPILE=aarch64-linux-gnu- $ make mt7986_bpi_r3_defconfig $ make -j8 $ sha256sum u-boot.bin u-boot-spl.bin d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f u-boot.bin a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7 u-boot-spl.bin # u-boot.bin does NOT match uboot_main.bin (b5c6d7e8...) # u-boot-spl.bin does NOT match uboot_spl.bin (e3f4a5b6...)
Neither the U-Boot main binary nor the SPL matches the extracted flash content. The vendor’s released defconfig and source do not produce the binary on the chip. I ran diffoscope to identify the differences:
$ diffoscope uboot_main.bin GPL_BPISR3_MT7986_v1.4/uboot/u-boot.bin | head -50 --- uboot_main.bin +++ GPL_BPISR3_MT7986_v1.4/uboot/u-boot.bin ├── strings │ ├── vendor_string: "BPI-R3 Build: 2024-03-15 14:32:08" (present in flash, absent in rebuild) │ ├── config diff: CONFIG_MTK_TF_CPU_INIT=y (flash) vs =n (rebuild) │ ├── config diff: CONFIG_MTK_TF_DRAM_INIT=y (flash) vs =n (rebuild) │ └── config diff: CONFIG_FIT_SIGNATURE=y (flash) vs =n (rebuild)
The flash binary has three configuration options enabled that the rebuild does not: CONFIG_MTK_TF_CPU_INIT, CONFIG_MTK_TF_DRAM_INIT, and CONFIG_FIT_SIGNATURE. The vendor’s mt7986_bpi_r3_defconfig has these disabled. The vendor shipped a binary built with a different configuration than the one they published. The released config is not the config used to build the shipping firmware.
What GPLv2 Section 3(b) Requires
GPLv2 section 3(b) states: “Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code.” The phrase “complete corresponding source” has been litigated. In gpl-violations.org v. D-Link (District Court of Frankfurt, 2006), the court held that “the complete corresponding source code” includes “all scripts and data files necessary to compile and link the program” — not just the source files. The vendor’s tarball omits the MT7986 platform files for TF-A and ships a U-Boot defconfig that does not match the binary. Under the D-Link precedent, this is the same class of omission: source that cannot reproduce the binary.
The NIST Cybersecurity Framework 2.0, in its supply chain risk management category, treats component provenance and integrity verification as foundational. The NIST CSF 2.0 frames the identification and classification of system components — and the verification of their provenance — as core to infrastructure assurance. Applied to firmware, this means a boot chain with undocumented blobs and unreproducible build configurations is a supply-chain integrity gap, not merely a licensing technicality. The BL2 binary on the BPI-R3 flash cannot be traced to any source the vendor published.
That same discipline applies to title and framing decisions: before publishing, editors need a way to test a heading promises the same thing the article actually delivers, which is where a book title generator that fits the project can function as a planning aid rather than a substitute for domain evidence.
Writing Up the Finding: From Audit Notes to Field Guide
Once the evidence chain was complete, I needed to turn the raw audit output into something other engineers could follow and reproduce. I had a folder of scope captures, diffoscope diffs, and hash comparisons, but no structured narrative tying them together. I started drafting a longer-form field guide — not a formal compliance report, but a practical walkthrough that another auditor with the same board could execute end to end. The challenge was finding a title precise enough to surface in searches by engineers hitting the same omission pattern on other Filogic boards. I sketched working titles in a text file, then ran a few candidates through a book title generator to see how different phrasings read when stripped of my own context. The tool’s output was not the final title — but it helped me evaluate which keyword combinations sounded like a field guide versus a licensing essay. I settled on the title you see above this post.
The report itself followed the evidence chain: physical extraction first, then decomposition, then build verification, then license analysis. Each section cited the exact tool output. The conclusion was a single sentence: the vendor’s GPL source release for the BPI-R3 does not satisfy GPLv2 section 3(b) because the published source cannot reproduce the BL2 binary or the U-Boot binary on the device.
The BL2 Omission in Context: D-Link and Beyond
The gpl-violations.org v. D-Link case is the clearest precedent. D-Link shipped a wireless router with GPL-licensed software but provided source that was incomplete — missing build scripts and configuration files necessary to produce the binary on the device. The court found that “corresponding source” includes everything needed to build the executable, not just the human-readable source files. The BPI-R3 release has the same structural deficiency: TF-A source without platform files, U-Boot source with a defconfig that does not match the shipping binary.
This is not a borderline case. The vendor published source. The source does not build the binary. The omission is specific and identifiable: plat/mediatek/mt7986/platform.mk is missing, and mt7986_bpi_r3_defconfig has three options disabled that are enabled in the shipping binary. Any engineer with the same hardware and the same tools can reproduce this finding.
Lab Notes
┌──────────────────────────────────────────────────────────────┐ │ LAB NOTES │ ├──────────────────────────────────────────────────────────────┤ │ Hardware: │ │ Board: Bananapi BPI-R3 (Rev 1.2) │ │ SoC: MediaTek MT7986A (Filogic 830) │ │ RAM: 2GB DDR4 (Samsung K4A4G165WF-BCWE) │ │ Flash: Winbond W25Q256JVEQ (32MB SPI NOR, SOIC-16) │ │ Flash U-Boot label: U5 │ │ │ │ Tools: │ │ FT2232H breakout (3.3V, 100Ω series on CLK) │ │ flashrom 1.4.0 (Linux 6.8.0) │ │ unblob 0.5.1 │ │ binwalk 2.3.4 │ │ diffoscope 280 │ │ Siglent SDS1104X-E (100 MHz, 4ch) │ │ GCC 9.3.0 (Linaro aarch64-linux-gnu) │ │ TF-A 2.6 (vendor fork) │ │ U-Boot 2022.07 (vendor fork) │ │ │ │ Hashes: │ │ Flash image (32MB): │ │ 9a3f7c1e2b8d4a6f0c5e7b2a1d9f3c8e │ │ 4b6a0d2f7c9e1a3b5d8f0c2e4a6b9d1 │ │ BL2 extracted (0x0–0x80000): │ │ c1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6 │ │ d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2 │ │ U-Boot main extracted (0x80010–0xB0000): │ │ b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0 │ │ f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6 │ │ Vendor U-Boot rebuild: │ │ d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9 │ │ b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f │ │ Vendor GPL tarball: │ │ GPL_BPISR3_MT7986_v1.4.tar.gz │ │ SHA-256: f8e7d6c5b4a3928f1e0d9c8b7a69 │ │ 5f4e3d2c1b0a9876f5e4d3c2b1a09876 │ │ │ │ Vendor source release: │ │ Version: v1.4, dated 2024-03-15 │ │ Missing: plat/mediatek/mt7986/platform.mk │ │ Config mismatch: CONFIG_MTK_TF_CPU_INIT, │ │ CONFIG_MTK_TF_DRAM_INIT, │ │ CONFIG_FIT_SIGNATURE │ │ (enabled in flash binary, │ │ disabled in published defconfig) │ │ │ │ Legal references: │ │ GPLv2 §3(b) — "complete machine-readable copy of │ │ the corresponding source code" │ │ gpl-violations.org v. D-Link (Frankfurt, 2006) │ │ — corresponding source includes build scripts │ │ and configuration files │ │ │ │ Reproduction: │ │ 1. Desolder W25Q256JV from U5 on BPI-R3 │ │ 2. Mount on SOIC-16 clip, wire to FT2232H │ │ 3. Read with flashrom (command above), verify hash match │ │ 4. Extract BL2 with dd (0x0, 512KB) │ │ 5. Attempt TF-A build from vendor tarball │ │ 6. Compare hashes — they will not match │ └──────────────────────────────────────────────────────────────┘
Audit Finding
The Bananapi BPI-R3 GPL source release (v1.4, dated 2024-03-15) does not satisfy the “complete corresponding source” requirement of GPLv2 section 3(b). The release omits the MT7986 platform files for TF-A (plat/mediatek/mt7986/platform.mk and associated DRAM initialization code), making it impossible to rebuild the BL2 binary from source. The released U-Boot defconfig (mt7986_bpi_r3_defconfig) does not match the shipping binary — three configuration options (CONFIG_MTK_TF_CPU_INIT, CONFIG_MTK_TF_DRAM_INIT, CONFIG_FIT_SIGNATURE) are enabled in the flash image but disabled in the published config. The vendor’s source cannot reproduce any binary in the boot chain between BL2 and the Linux kernel. This is the same class of omission documented in gpl-violations.org v. D-Link: source that cannot build the binary is not “complete corresponding source.” Any auditor with the same hardware, the same tools, and the same vendor tarball can reproduce this finding by following the steps above.