Winbond NAND ECC algorithm
Why are we here
A while back I found myself having an irritating problem. I was reverse engineering an IP camera and wanted to modify its filesystem. Easy - you just desolder the BGA NAND, read it out, patch and write it back - right?
Well, not when there’s a difference in the ECC depending on whether a page has been written at all compared to if it has been written back with the same byte pattern. That is, the ECC for a page full of 0xff is 0xff if it has never been flashed before but 0x00 if it has. This is due to the on-chip ECC engine and not something the Flashcat software is able to avoid.
So, faced with the ability to either flash the data with ECC calulations off (“corrupt filesystem”) or with ECC calculations on (“corrupt filesystem”) I had to figure out a third solution.
Calculate the ECC myself - because then I can make sure it’s only calculated on the sectors that should have it and not the “uninitialized” ones.
The ECC algorithm for Winbond W25N01GV is not public. The only information that exists shows it to consist of 6 bytes for every 512 byte sector in a full 2112 byte page:
| Component | Count | Size per Item | Total Size | Description |
|---|---|---|---|---|
Sector Data |
4 |
512 bytes |
2048 bytes |
Main data storage area |
Spare Area |
4 |
16 bytes |
64 bytes |
Out-of-band (OOB) data |
| Total | - | - | 2112 bytes | Complete page size |
The makeup of a 16 byte Spare area looks like this:
| Byte Offset | Size | Field Name | Description | Typical Value |
|---|---|---|---|---|
0-1 |
2b |
Bad Block Marker |
Indicates bad/defective block |
0xFFFF |
2-3 |
2b |
User Data II |
Custom user data field |
0xFFFF |
4-7 |
4b |
User Data I |
Custom user data field |
0xFFFFFFFF |
8-13 |
6b |
Sector ECC |
ECC for 512-byte sector |
Computed |
14-15 |
2b |
Spare ECC |
ECC for spare bytes 4-13 |
Computed |
Additionally, Winbond writes the following in their documention:
As the ECC engine is based on Hamming code, the ECC status bits are applicable for 1 bit ECC correction and 2 bit ECC detection.
This should be solvable, right? Well, it turns out that 6 bytes for a 512 byte sector isn’t a common Hamming code schema and none of the public Hamming ECC implementations I looked at came even close. I thus turned to making a brief stab at it myself, but after an easy win where I observed that it’s actually a 3 byte ECC over 256 byte sub-sectors it seemed to be quite a task to reverse it any further.
So I booted up Qwen3.6-35B-A3B-GGUF (UD_Q4_K_XL quant) - and additionally built a llama.cpp with MTP support from an as-of-yet not merged PR together with an MTP version of the model. This is a model that runs quite fast on my 16GB 5060Ti system, which was needed because the next thing I did was to harness the LLM in a loop - with the “simple” task of … reverse engineering this ECC algorithm. I gave it what I already knew, and watched it get to work.
Don’t put cloud models you’re paying per token in a loop like this, kids!
Roughly 24 hours later I had the whole ECC scheme reversed. The LLM went wildly off track at times, but not more than that I only needed to nudge it slightly in the right direction with a knowledge file for it to refer to after each compaction event. Thanks to the harness (Opencode+DCP+Superpowers) it constantly verified its “thinking” through writing Python programs, which meant that there were practically no hallucination issues when it made hypotheses and verified them.
Oh, yeah. I’m assuming you ended up at this blog post because you have also been searching for the algorithm. Here it is:
Main Sector ECC Algorithm
The ECC is based on Hamming code with additional parity correction. For each 256-byte sub-sector:
Step 1: Compute Hamming Syndrome
syn = 0
for byte_pos in range(256):
bv = sub_sector[byte_pos]
for i in range(8):
if bv & (1 << i):
syn ^= (byte_pos * 8 + i)
The syndrome is the XOR of all bit positions where a 1 bit occurs. For a 256-byte sector, this produces an 11-bit syndrome (8 bits for byte position + 3 bits for bit position within byte).
Step 2: Count Total Bits
total_bits = sum(bin(b).count('1') for b in sub_sector)
Count the total number of 1 bits in the sub-sector.
Step 3: Parity Correction
parity = 0xff if total_bits % 2 == 1 else 0
This is an overall parity byte: 0xFF if odd number of bits, 0x00 if even.
Step 4: Compute ECC Byte 0
byte0 = (syn & 0xff) ^ parity
The lower 8 bits of the syndrome, XORed with the parity.
Step 5: Compute ECC Byte 1
syn08 = syn & 0xff # Lower 8 bits of syndrome
syn811 = (syn >> 8) & 0x07 # Upper 3 bits of syndrome (bits 8-10)
# Compute intermediate value g
g = ((syn08 & 0x0f) << 12) | (syn811 << 4) | (syn08 >> 4)
# Apply total_bits parity correction
extra_13 = g ^ ((total_bits & 1) << 11)
# Byte 1 combines upper bits of extra_13 with XOR of syn811 and parity
byte1 = ((extra_13 >> 8) & 0xff) | (syn811 ^ (0x07 if total_bits % 2 == 1 else 0))
Step 6: Compute ECC Byte 2
byte2 = extra_13 & 0xff
The lower 8 bits of the intermediate value.
Spare ECC Algorithm
Now, before we can end our investigation there’s one more piece to the puzzle - the two byte ECC for Spare that sits at the very end of every Spare. Or, it takes up two bytes but the two bytes are always the same which means it’s only a 1 byte “ECC”. And if we squint a bit we can see that over the full NAND dump I have there are actually only 16 unique values - likely because in our case the User Data I bytes are always 0xff.
This is how that’s calculated:
The Spare ECC follows the same Hamming code principle as the Main Sector ECC, but over only 10 input bytes.
Input
- 10 bytes =
spare[4:14]= User Data I (4 bytes) + Sector ECC (6 bytes)
Output
- 2 bytes =
spare[14:16]where both bytes are identical
Step 1: Compute Hamming Syndrome
syn = 0
for byte_pos in range(10):
bv = input_bytes[byte_pos]
for i in range(8):
if bv & (1 << i):
syn ^= (byte_pos * 8 + i)
The syndrome is the XOR of all bit positions where a 1 bit occurs. For 10 input bytes, this produces a syndrome value that captures the position of all set bits.
Step 2: Count Total Bits
total_bits = sum(popcount(b) for b in input_bytes)
Count the total number of 1 bits in the 10 input bytes.
Step 3: Parity Correction
parity = 0xff if total_bits % 2 == 1 else 0
This is an overall parity byte: 0xFF if odd number of bits, 0x00 if even.
Step 4: Compute ECC Byte
ecc_byte = (syn & 0xff) ^ parity
The lower 8 bits of the syndrome, XORed with the parity byte.
Step 5: Return Both Bytes (Identical)
return bytes([ecc_byte, ecc_byte])
Both output bytes are set to the same computed ECC byte for redundancy.
… and now we’re done.
Enjoy.
