FiiO X1 NAND dump: 128 MB over USB, one block at a time
Contents
Two programs set out to copy the whole flash chip of a FiiO X1, a $99 music player built around an Ingenic JZ4760B MIPS chip, so its firmware could be studied off the device. The first was 4,384 bytes of bare-metal C that would run from the player's SD card, read every NAND page and write it straight back to the card, with an estimated finish in a few minutes. The second was a shell script that asks Rockbox's hwstub debug stub for one block at a time over USB.
I read the first one line by line before it went near the hardware, and again before its second try. It never ran. The second one finished the job while I slept.
Five bugs before a single instruction executed#
The C program was built to ride the door from Part 5: a file named factory.x1 on the SD card, which X-Boot, the player's second-stage bootloader (the SPL), loads to RAM and jumps into without a signature check. It carried its own SD card driver, a NAND page reader, UART logging, and an LCD progress bar that found the framebuffer by reading the LCD controller's own DMA descriptor registers.
Before copying it to a card, I read it through once more. That read found four bugs, and a second pass after the LCD code went in found a fifth. Not one of them would have produced an error message.
Wrong status bits. The SD driver polls the MSC (SD/MMC controller) status register to learn when a command finishes. Of the eleven MSC_STAT_* constants in my code, two matched the JZ4760 header. MSC_STAT_END_CMD_RES was bit 2 in my code and is bit 11 in the header; MSC_STAT_DATA_TRAN_DONE was bit 15 and is bit 12. A coin would have done better than two in eleven. With "command done" pointing at bit 2, the driver was really reading a CRC-error bit, so it could report success on a CRC error or spin forever on a good response.
A stolen pin. A standard sd_gpio_init() routes the MSC0 pins on port A. On the JZ4760, PA20 is the MSC0 clock in that mapping, and on this board PA20 is also NAND R/B#, the flash chip's ready/busy line. Configuring the SD clock would have taken the one signal every NAND read waits for. The SPL had already configured the SD pins before handing over (on port C, as far as I could tell), so the fix was to delete the function.
32-bit reads on a 16-bit bus. The NAND data port at 0xBA000000 is 16 bits wide, and the IPL, the first-stage loader that already works on this hardware, reads it 16 bits at a time. My reader used 32-bit loads. On some Ingenic parts that combines two bus reads; on others the upper half is undefined. The page would have come back garbled in a way that looks almost right. The fixed loop:
/* Read data + OOB as 16-bit words: (2048 + 64) / 2 = 1056 words */
for (i = 0; i < (PAGE_TOTAL / 2); i++)
buf16[i] = NAND_DATA16;
No stack. The entry point was a plain C function that never set $sp, so it ran on whatever stack pointer the SPL left behind, with local variables and deep call chains on top. The first fix marked _start as naked with the stack set in inline assembly. GCC's answer was one line:
nand_sd_dumper.c:542:1: warning: ‘naked’ attribute directive ignored [-Wattributes]
The MIPS port ignores the attribute and emits a prologue anyway. The entry point moved to its own assembly file:
_start:
li $sp, 0x80080000
j main_entry
nop
The one I'm not showing was a NAND ready/busy wait polling a register bit that, on the JZ4760, is a chip-select enable, which would have hung the first read. All of it was caught by reading, not running. Hold on to that; it is the only success this program gets.
"waiting sdcard"#
Build, scramble with Rockbox's packtools (the Makefile's last step), copy to the card as factory.x1:
../rockbox/utils/jz4760_tools/packtools --scramble -i nand_sd_dumper.bin -o nand_sd_dumper.x1
Part 5 had promised a door. I distrust a promise in proportion to how little it asks of me, and "put a file on an SD card" asks almost nothing. Boot the X1 into recovery (the SHORTCUTS key held during power-on). The screen lights up. The SPL starts. And it prints:
waiting sdcard
Forever. That was already the result of the first build, the one without the LCD code, on the card in its original format. The text was the SPL's own screen, and I read it as proof that the LCD was switched on and waiting for my code, which is how the second build came to carry a progress bar. Then the 4,384-byte build with the LCD code and the fifth fix, same card, same format: waiting sdcard. A normal power-on instead of recovery changed nothing.
The card itself was readable. With the dumper renamed to x1.fw, the SPL flashed some text, said update not found and booted normally. So it could bring up that card, mount its FAT and find a file on it by name. It found the file I had called x1.fw and rejected it, which is fair to a file with no firmware header. The file called factory.x1, which the SPL's own handler at offset 0x114CC opens on this recovery path, it never ran.
Then I made it worse. Repartitioned to 2GB with mkfs.fat -F 32: waiting sdcard, although a regular boot still saw the card. Restored to full size and reformatted, factory.x1 next to a valid x1.fw firmware image: waiting sdcard. The same with the x1.fw header date bumped: a short detour into a mode the power button couldn't end, then waiting sdcard.
After the reformat, not even a valid x1.fw got picked up. My notes have a theory for that half: the SPL's SD/FAT code expects BIOS Parameter Block values that the card's original formatter wrote and Linux's mkfs.fat does not. It explains why x1.fw stopped being found. It does not explain the first half of the evening, where a card the SPL could demonstrably read still did not get its factory.x1 run. The idea that lasted longest that night was that waiting sdcard waits for an x1.fw before it checks anything else, and by the time I tested it with both files on the card, my own reformat had spoiled the experiment. Why factory.x1 was ignored stayed open.
The door from Part 5 is real. I have its string, its handler and its offset. I do not have a single boot where it opened.
Seven and a half hours over USB#
Back to what already worked: hwstub over the chip's USB boot mode, the same tool that dumped the IPL and SPL in Part 3. Proven, slow, reliable.
The NAND is 128MB: 1,024 blocks of 64 pages, 2,048 data bytes per page. The dump script loops over blocks, skips any block file already on disk at full size, and starts one hwstub_shell per block. This is the Lua it feeds each one:
JZ.nand.setup(params)
HW.NEMC.NFC.write(3)
JZ.nand.reset()
-- Read 64 pages raw (no BCH, no PN) using read_page
local all_data = {}
local idx = 0
for p = 0, 63 do
local page = ${start_page} + p
local data = JZ.nand.read_page(0, 2, page, 2, 16, 2048)
for i = 0, 2047 do
all_data[idx] = data[i]
idx = idx + 1
end
end
JZ.nand.rom.write_to_file('${outfile}', all_data)
print('BLOCK_DONE')
A test block took 26 seconds. Times 1,024, that is 7 hours 24 minutes. The bottleneck is Lua copying bytes one table slot at a time over USB control transfers, which is about as fast as it sounds.
Start it. Go to bed. I know how the night went because the files do: block 0 was written at 21:56 during the test, block 1023 at 05:25 the next morning. The log holds 1,023 OK lines for the run and zero errors, and the concatenated image is exactly 134,217,728 bytes.
One thing the crude tool did not keep: the 64-byte spare area (OOB) of each page. read_page(..., 2048) asks for the data area and nothing else. The careful program would have saved all 2,112 bytes of every page. Nobody needed the OOB that night. That changes later.
What 128 megabytes looks like (first pass)#
My first map of the raw blocks, drawn while the dump was still running, at block 344:
| Blocks | Size | Contents |
|---|---|---|
| 0 | 128KB | IPL (8KB used, rest erased) |
| 1-15 | 1.9MB | Erased (IPL spare copies) |
| 16-21 | 768KB | SPL (X-Boot) |
| 22-63 | 5.3MB | Erased (SPL spare) |
| 64-214 | ~19MB | Firmware. Every 5th block reserved. |
| 215-1023 | ~101MB | Not checked, most of it not dumped yet; labelled userdata, assumed empty after a fresh firmware install |
Keep that last row in mind. It is the one Part 7 takes apart.
The firmware region spans 151 blocks, and 121 of them hold data. Every 5th block, 66, 71, 76 and so on up to 211, is erased: a reserve for bad-block replacement, managed by the FTL (flash translation layer) in the SPL. None of the reserves were in use.
The partition table#
The SPL carries a partition table at offset 0xAB200. Seven entries, each with an offset, a size, flags and a 20-byte name:
| Partition | Offset | Size |
|---|---|---|
| recovery | 0x02000000 | 1MB |
| boot | 0x02100000 | 5MB |
| system | 0x02600000 | 4MB |
| userdata | 0x02A00000 | 128MB |
| cache | 0x0AA00000 | 256MB |
| kpanic | 0x1AA00000 | 32MB |
| storage | 0x1CA00000 | 2MB |
Those offsets are virtual, translated by the FTL to physical blocks: about 460MB of address space on a 128MB chip.
The names, recovery, boot, system, userdata, cache, are Android's. The X1 runs µC/OS-II v2.92, a small real-time kernel with no Linux anywhere. My guess is that the layout came with Ingenic's Android-era reference code and nobody renamed it. My favourite entry is kpanic: 32MB reserved for kernel panic dumps, on a device with no kernel of the kind that writes them.
PNGs, in the clear#
binwalk over the 121 data blocks of the firmware region printed 4,416 lines of output: PNG image headers, each followed by its zlib streams, ending in a 320×240×32 bitmap. Icons and backgrounds, stored as plain PNG files with no encryption. The bitmap matches the screen: 320×240, the size the dumper's LCD code would have read from the controller's DESSIZE register if it had ever run.
The missing firmware#
The code itself, sys.bin, the 4.1MB µC/OS-II image that runs at 0x80001000, did not turn up where I looked. Block 64 starts with resources, not code, and the system partition's physical blocks depend on an FTL mapping I had not decoded.
I had two explanations. Either the code blocks were scrambled by the NAND controller's PN randomizer, which I assumed was on for everything from block 16 up, or the FTL had put system somewhere I had not identified. My notes from that night wrote the first one down as fact: PN randomizer on for blocks 16 and up, SPL and firmware alike. Part 7 is about how wrong that was.
What's left#
Every data byte the chip stores is on disk. The firmware code is in there somewhere, behind a mapping or a scrambler I had not beaten yet.
The 4,384-byte dumper is still in a source file: reviewed twice, correct as far as reading can tell, never executed. It would have been faster, and it would have kept the spare bytes. I keep it the way other people keep a gym membership. The shell loop that did the work took seven and a half hours and a night's sleep, and it is the one I will reach for again.
Filed under: reverse-engineering, mips, embedded, fiio, hardware
Read next#
- FiiO X1 NAND archaeology — the next part: finding
sys.binin this dump, and what the "erased" blocks really held. - FiiO X1 factory.x1: unsigned code execution from the SD card — the door this part's dumper was built for, and why it should have worked.
Sources#
- Rockbox hwstub NAND Lua scripts —
nand.luaread_page implementation - Rockbox packtools —
factory.x1scramble tool - ingenic-boot
jz4760.h—MSC_STAT_*bit definitions (END_CMD_RESbit 11,DATA_TRAN_DONEbit 12,CLK_ENbit 8) - NAND Flash 101: Introduction (Micron TN-29-19) — page/block geometry, bad block management
- FiiO X1 review (Headfonics, 2014-10-07) — launch price: "The FiiO X1 on the other hand at $99"