One bit in RAM: fixing a Phison SSD controller in "SATAFIRM S11" state
A 120 GB SSD reported itself as SATAFIRM S11 and served nothing but zeros. Every guide on the internet said to reflash it, which would have destroyed the data. The entire fault was one bit in a RAM flag, set at every bo…
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
A 120 GB SSD reported itself as SATAFIRM S11 and served nothing but zeros. Every guide on the internet said to reflash it, which would have destroyed the data. The entire fault was one bit in a RAM flag, set at every boot because of a single page on the NAND.
Phison PS3111-S11 · firmware SBFM21.1 · 2026
The drive was a Silicon Power M56, 120 GB, M.2 SATA, with a Phison PS3111-S11 controller and no DRAM. It had been working, and then it had not. What the host saw was this:
Model Number: SATAFIRM S11 Firmware Revision: SBFM21W1
That is not the drive's model. It is not even a real firmware version. dd read 120 GB of zeros from it without a single I/O error. The partition table was gone, the filesystem was gone, and smartctl made the machine hang during boot at a consistent 98.64 seconds, with hardreset failed and ata3.00: disable device.
Search for SATAFIRM S11 and the answer you get, everywhere, is: download Phison's MPTool, pick the matching flash-ID configuration, and reinitialise the controller. This works. It also wipes the flash translation layer and, with it, every byte of your data, permanently. The forum posts say so in passing, usually after the person has already done it.
I wanted the data. So instead of reflashing, I spent a while finding out what SATAFIRM S11 actually is.
It is this: a byte at address 0x5C0CB5F0 in the controller's RAM. If that byte is non-zero, the firmware's IDENTIFY handler reports SATAFIRM S11 instead of the real model. The byte was 0x04 — one bit set — and the bit was set at every boot by the firmware's own startup scan, which had found one bad page in the system area and remembered it. Erasing the block that held that page ended the condition permanently. The drive came back as SPCC M.2 SSD / SBFM21.1, mounted its NTFS volume, and a full image taken from it boots Windows to the desktop.
Getting there took two serious wrong turns and a 105 GB raw dump that I did not end up needing. The wrong turns are the interesting part, so they are in here too.
None of this would have been possible without Psychite by trulycrisp, and the accompanying writeup Drive Firmware Security – Phison S11. That project worked out the genuinely hard part: the vendor-unique command (VUC) transport that gets you past the standard ATA command set and into the controller, the command set itself, and the firmware container format.
With Psychite you can, over an ordinary SATA cable, on a drive that is refusing to serve data:
That is a debugger attached to a dead disk. Everything below is just using it.
My first instinct was the obvious one — forget the controller, read the flash directly and rebuild the volume from the raw pages. The OOB bytes on each page carry the LBA of the data in it, so in principle you can sort 8768 blocks of pages into a disk image and mount the result.
I dumped the whole chip. 105 GB, zero read errors, which was already informative: nothing was physically wrong with this NAND. Then I spent a long time failing to turn it into a filesystem, for two reasons that both turned out to be my own.
The chip has four chip-enables. Psychite selects one by the bit position within the controller's ce_bitmap, which gives 0, 4, 8, 12. Two of those four values silently do the wrong thing: they re-read CE0 and CE1 at page+2 instead of selecting CE1 and CE3. The OOB carries a CE id, so this is checkable, and once checked the real encoding falls out:
ce_raw = (((address >> 2) & 1) << 3) | ((address >> 3) & 1) # 0->0, 4->8, 8->1, 12->9
The read mode was wrong, and this one hid the fault
This controller uses some blocks in pSLC mode (one bit per cell, 512 pages per block) and the rest in MLC (two bits, 1024 pages). You have to ask for the right one. A pSLC block read in MLC mode does not return garbage — it returns what looks like an erased block.
So my dump quietly classified 4662 of 8768 blocks as empty. They were not empty. They held 38 GB of user data, the entire firmware system area, and — as it turned out — the one page that was causing the whole problem. The same mistake also produced a phantom symptom I believed for days: "the controller hangs when you read system blocks." It does not. It hangs when you read a pSLC system block in MLC mode.
The fix for both was the same, and it is the single most useful habit I took from this: the OOB contains a physical page index. Read a block, check that the page index in the spare bytes matches the order you read them in. If it does not, your addressing or your mode is wrong, and everything you conclude from here is fiction.
Meanwhile I pulled the firmware and opened it in Ghidra. The PS3111-S11 runs a little-endian Xtensa core; Ghidra handles it. The one thing you must do is load the sections at their runtime addresses, or every absolute reference is meaningless:
From there it is a short walk. FUN_60004c4c builds the IDENTIFY response. It reports the real model and firmware version unless the byte at 0x5C0CB5F0 is non-zero, in which case it substitutes the SATAFIRM S11 / SBFM21W1 strings.
The byte is not a flag. It is a bitfield of reasons, and each bit has a different setter:
This is the part I would most like other people to know, because it changes what you do next. SATAFIRM S11 is not one failure mode. It is at least four, and the controller will tell you which one you have, over read-only commands, in about a second. Every variable that scan writes stays in RAM and can be read without writing anything:
$ sudo safe_dump.py /dev/sgX diag-protect IDENTIFY model='SATAFIRM S11' firmware='SBFM21W1' 0x5c0cb5f0 [1] 04 protect flag: bit 2, and nothing else 0x5c0ceaac [1] 03 error unit at firmware block 3 0x5c0ce6da [1] 05 system unit header found 0x5c0ce6db [1] 07 block map found 0x5c0ce9ab [1] 08 scan covers firmware blocks 0..7
Exactly one reason bit. And both pieces of FTL metadata — the system unit header and the block map — were found, which means the translation layer was intact. The drive knew where all my data was. It had simply decided not to tell me.
You can write zero to 0x5C0CB5F0. It takes one command, and IDENTIFY changes to the real model immediately, with no reset. For about a minute this feels like the fix.
It is not. dd still reads zeros. The flag is the symptom; by the time you clear it, every boot-time decision gated on bit 2 has already been taken. Nor does a reset help by itself — it reloads the firmware from NAND, re-runs the scan, finds the same page, and sets the same bit. The only sequence that works is: remove the cause on the NAND, then reset so that init runs again and reaches a different conclusion.
I also tried to be clever and failed twice, which is worth recording:
FUN_60016c4c is the system-area mount routine. It walks blocks in ascending order on each CE, reads page 0, and classifies it by the first 32-bit word of the OOB:
value = *(u32 *)(0x50001200 + slot * 0x100);
if ((~value & 0xFFD00000) == 0) { /* ERROR_UNIT: a MASK test, not equality */ *(u8 )0x5C0CEAAC = block; / remember where */ *(u8 )0x5C0CB5F0 |= 4; / protect mode / break; } if (value == 0xFFA00000) { / SYSTEM_UNIT_HEADER / } if (value == 0x3FB10200) { / BLOCK_MAP */ }
Two details in there matter more than they look.
The error-unit test is a mask, not a comparison. (~value & 0xFFD00000) == 0 means "every bit of 0xFFD00000 is present." 0xFFFFFFFF satisfies it too — so on a read path where an erased page presents as all-ones, an erased block would trip the check. On this drive erased pages read back as 0x00000000 through the VUC path, which is the only reason the obvious repair works at all. If you do this and the erased page comes back 0xffffffff, stop: you have just made the trigger permanent.
The scan does not abort on finding it. It sets the bit and keeps going, and returns success as long as it finds a header and a block map. That is why a drive can be in protect mode with a perfectly healthy FTL. Bit 2 alone is enough to lock you out of data that the controller can still address.
And one piece of arithmetic that cost me several days: firmware block numbers are physical blocks ÷ 4. When the firmware says "block 3", the block you have to erase is physical block 12. A firmware block is four internal units, and the OOB CE-id field confirms the grouping. Nothing says so anywhere; you find it by noticing that a RAM variable and your dump disagree by a factor of four.
Having finally found it — 53 written pages out of 512, one block on each of CE0 and CE1 — I dumped it. It is a crash log, and it is unusually talkative. The sub-type is tagged in the second OOB word:
The 101 descriptor records hold a fixed command (0x00440021), an index climbing from 0x1b to 0x7f, and two linear ramps with steps of 0x40 and 2. Fixed command, parameters stepping regularly, 101 times: that is a read-retry sweep. The firmware was retrying a read with shifted reference voltages. It gave up after 101 attempts, wrote a crash log, dumped its own code section into it, and left behind the page that would greet it at every subsequent boot.
The 0x666666 pages paid for themselves twice. First, they match my decrypted firmware image byte for byte, which validates the extraction — and, more usefully, settles a question. Commercial data-recovery vendors describe the PS3111-S11 as applying XOR scrambling to NAND writes. Whatever happens at the cell level, the VUC read path returns plaintext: here is 176 KB of known content, read back through that path, unscrambled. There is no XOR key to extract on this path. (Nearly everything findable about this controller's data format comes from one vendor's marketing pages, which contradict each other — a key derived from model and chip ID in one place, a two-layer "dynamic XOR" in another.)
Second, the variable region of those pages is a snapshot of controller state at the instant of the panic:
0x6005CD00 = 05 ff ff ff ff ff | 07 ff
System unit header at block 5, block map at block 7. The mount had succeeded when the failure hit. Whatever went wrong, it was not a corrupt system area.
Running in parallel with all this was my attempt to rebuild the volume from the raw dump. It produced the most instructive failure of the project, and it had nothing to do with the hardware.
A large fraction of frames, read as the 4 KB of their declared LBA, contained high-entropy data with a recurring 8-byte prefix. Obviously encrypted, then. So I went looking for the encoding. Everything in this table was measured:
I also built two "fingerprints" of the mystery format. Both were artefacts, and I am recording them so nobody reuses them:
Here is what was actually going on. Bit 0 of byte 8 of an OOB frame entry marks a partial frame — a frame that does not hold the full 4 KB of its declared LBA. Across 263 sampled versions, a frame with that bit set never held the complete 4 KB. Reading those frames as if they did means comparing unrelated bytes. There was no cipher. I was computing entropy on misalignment.
The hypothesis survived as long as it did because of how I was testing it. Each negative result — not XOR, not a reordering, not compressed — got read as "then it is a more complicated encoding" rather than as evidence against the premise. A chain of exclusions that narrows forever without ever questioning the set it started from.
What broke it was not a cleverer analysis. It was that the drive got fixed, and became an oracle I could query. I took 60 LBAs from the dump, read them from the repaired device, and compared them against every version of them in the dump:
The 16 matches land at offset 0 with no shift, which proves the LBA decoding was right all along. Correlating OOB byte 8 against those three outcomes produced the partial-frame flag in an afternoon, and with it the explanation for every failed test above. The placement rule followed:
n = run length of the bits equal to bit 0 of byte 9, starting at bit 0 shift = 8 - n frame[0 .. n*512) == device sectors [LBA + shi
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。