diskiller's domain

252 Megabytes in a 64 Gigabyte Trenchcoat

2026-09-15

I bought two 64 GB microSD cards on AliExpress for USD 9. Both of them. Nine dollars for the pair. I would like to tell you I was conducting research, but the honest version is that they were in the cart next to some camera modules and I have never once shown restraint at a checkout page.

They are, of course, fake. Everyone knows this story. You run f3probe, it tells you the card is a counterfeit holding a fraction of what it claims, you file a refund, and you move on with your life.

I did all of that. The refund even went through, which is its own small comedy: I pasted the f3probe output into the "tell us more" box and the seller folded instantly.

Then I kept the cards, because a 240 MB card that owes you nothing is the single best thing to point a destructive test at, and because the number kept bothering me.

The number that bothered me

Three different measurements, three different answers, all clustered suspiciously:

f3probe --destructive     252.00 MiB   516096 sectors
f3write/f3read, card 1    237.97 MiB   487360 sectors
f3write/f3read, card 2    236.30 MiB   483936 sectors

Against an advertised 62.5 GB. The interesting question is not "is it fake", which was settled in about ninety seconds. The interesting question is what is actually down there, and why three tests disagree about where the edge is.

So I wrote a small C program, pointed it at /dev/mmcblk0 on a Raspberry Pi with absolutely no regard for the card's feelings, and started mapping.

The hypothesis everybody has first

The classic counterfeit trick is address truncation. The controller reports a huge capacity, but physically only has a small die, so it throws away the high address bits:

physical_lba = logical_lba & 0x7ffff;

Write your holiday photos to the "60 GB" mark and they land on top of your tax returns at the 200 MB mark. It is a beautifully stupid design and it is genuinely common.

And 516096 sectors is so close to 219 that the hypothesis practically writes itself. I was ready to be delighted.

The tool

The program writes one self-describing 512-byte sector at each address I care about. Each probe contains a magic number, the run id, the sector number it was written to, a sequence number, and a payload generated by splitmix64 seeded from the run id and the sector number. Nothing has to be stored to check it later, and a sector that merely claims to be sector N gets caught out when its payload does not match sector N.

Two details matter more than the rest of the program combined.

Every read and write uses O_DIRECT. Without it you are measuring the Linux page cache, and every result you get is a confident lie about a card you never touched.

The second is that acknowledgement, persistence and survival are three different things, so the program tests them separately: write a probe and fdatasync, re-read every earlier probe, then close the device, drop the buffer cache, reopen and read everything again, then do a third read-only pass after the card has been fully reset. A controller can acknowledge a write, survive a flush, and still lose the data the moment it is power cycled.

Re-reading every previous probe after every single write is O(n2), which at a few dozen probes is free, and it tells you exactly which write destroyed which earlier probe rather than leaving you to infer it.

I also pointed it at a device-mapper device built to alias on purpose, four logical ranges mapped onto one physical extent, because a detector nobody has ever pointed at a known positive is not a detector, it is a random number generator with good PR.

It does not alias. At all.

I probed sector 8192 plus 2k for every k from 0 to 26, which tests every single address bit the device claims to have.

Zero collisions. Not one. Data written below the real capacity is never disturbed by anything written above it, no matter how far above. The elegant, stupid bit-masking hypothesis is simply wrong.

What happens instead is that writes past the real end are accepted, fdatasync returns success, the kernel is happy, everyone is happy, and the data is gone by the next read. F3 calls this class "limbo", which is a much better name than it needed to be.

The real geometry, exact to the sector

Sector 516095 stores data. Sector 516096 does not. There is no fuzzy boundary, no gradual degradation, no probabilistic edge. It is a cliff, and it is in the same place on both cards.

advertised   131072000 sectors   62.5 GB    (the CSD lies: C_SIZE = 127999)
real            516096 sectors   252.00 MiB
overstated                       254x

That real number is not arbitrary, which is the first genuinely satisfying moment in this whole exercise:

2^19 sectors      524288   256 MiB    the die
minus               8192     4 MiB    == preferred_erase_size, exactly
equals            516096   252 MiB    what actually works

So there really is a 256 MiB part in there, with precisely one erase group held back for flash translation metadata, wearing a 64 GB costume. Whoever built this did not hack together a fake from nothing. They took a real, functioning, perfectly reasonable 256 MiB card and changed one field in the CSD register.

The earlier f3write/f3read numbers of 236 to 238 MiB were lower not because the boundary is fuzzy, but because sustained sequential writing hits it messily. f3probe --destructive reported 516096 and was correct to the sector. Credit where it is due.

Now the part I did not expect

Reads past the real capacity do not fail. They do not return zeroes. They do not return 0xFF, which is what erased flash actually looks like. They return this:

0000000 8b 97 dd 6e 14 1d 66 3c 2a 08 11 99 b5 82 aa cb
0000016 56 22 ff d3 c9 a8 44 81 f7 bd 33 24 68 37 88 76
0000032 ae 76 23 46 31 fc 98 14 0f e9 ef b1 90 63 54 e3
0000048 73 c3 01 fb ec 49 ba a9 d2 5c cd 0c 4d d6 76 5e

Noise. Which by itself is unremarkable, flash returns garbage all the time. So I started poking at the garbage, and the garbage turned out to be the most interesting thing on the card.

It is stable. Byte for byte identical across repeated reads, indefinitely.

It is distinct for every sector. I sampled a full megabyte, 2048 consecutive sectors: 2048 different contents, no repeat anywhere, no period up to 512 KiB.

It is statistically ideal. Bit density 0.5000. All 256 byte values present, near-flat histogram. If you handed me this data with no context I would tell you it came out of /dev/urandom.

It is not a copy of anything on the card. I dumped the entire real 252 MiB and searched it. The first sixteen bytes of a phantom sector appear nowhere in it.

It is address-keyed, not command-keyed. Read one sector alone or 2048 in a single command, you get the same bytes either way.

It ignores writes. Write random data to a phantom sector, read it back, get the original noise, unchanged.

And it survives a full controller reset. I unbound and rebound the SD host controller, which re-initialises the card from CMD0 as if it had been physically reinserted. The device node disappeared and came back. Every phantom sector returned bit-identical data.

At this point the obvious suspect is a cheap linear feedback shift register, which is what you would reach for if you wanted plausible-looking noise in a few gates. So I ran Berlekamp-Massey over the bit stream, which recovers the shortest LFSR that can produce a given sequence.

first 1024 bits -> linear complexity 512
first 2048 bits -> linear complexity 1024
first 4096 bits -> linear complexity 2048

Exactly n/2 every time, which is the signature of true random data and the specific answer Berlekamp-Massey gives when there is no LFSR to find. Whatever is generating this is not cheap.

The card swap

At which point I remembered I had bought two of these.

I saved fingerprints from the first card, swapped in the second one, and read the same addresses before writing anything at all to it. Different card. Different CID. Different serial number.

1 MiB of phantom starting at sector 516096:

  card 0x00018e35  sha256 98ae5fae7e45851202cd405dd634ae9ae17b44dc424ce3524ef84a4f37a462c8
  card 0x00018e56  sha256 98ae5fae7e45851202cd405dd634ae9ae17b44dc424ce3524ef84a4f37a462c8

  cmp: identical, all 1048576 bytes

Two physically separate pieces of silicon, sitting in a drawer in different anti-static bags, emitting the same million bytes at the same addresses.

So the generator takes no per-card seed. It is a pure function of the sector number and a constant baked into the firmware.

And it is definitely generated rather than stored, which is not a guess. I sampled 400 sectors at random across the entire 62.3 GiB phantom range and got 400 distinct contents with zero repeats. A 256 MiB die cannot store 62.3 GiB of unique data. There is nothing being read back from anywhere. The controller is manufacturing it on demand.

So what is it?

Almost certainly the NAND scrambler, running with nothing underneath it.

Flash controllers scramble data on the way in and descramble it on the way out, keyed by physical address. This is not security, it is physics: long runs of identical bit patterns in a NAND page cause program disturb and read disturb, so you whiten the data first and unwhiten it on the way back. Every serious controller does this.

Now ask what happens if the controller is told to read a page that was never mapped, gets back whatever the die feels like returning, and descrambles it anyway. You get the scrambler keystream itself: deterministic, address-dependent, statistically perfect, and completely untethered from any data that has ever existed.

Which means this is probably not malice. It is a side effect. Somewhere there is a perfectly competent flash controller doing exactly what it was designed to do, in a situation its designers never intended, producing the most convincing lie in the entire product.

I believed that firmly for about four hours. Then the card died, and complicated it. See the postscript.

Why this is worse than zeroes

A fake card that returns zeroes past the boundary is a fake card that announces itself. Every heuristic catches it. Entropy checks catch it, hex dumps catch it, a human scrolling past it catches it.

This card's phantom region does not look empty. It looks like data. It looks like encrypted data, or compressed data, or a disk image, and it looks like that for 62 gigabytes.

Anything that detects counterfeits by looking for suspicious zeroes or repetition is defeated completely. The only thing that works is the thing F3 has been doing all along: write your own known pattern, read it back, and check it. There is no shortcut, and the popular shortcuts are exactly the ones this card walks straight through.

It also means any forensic tool that images one of these cards produces 62 GB of perfectly plausible, entirely fictional evidence. I find that funnier than I probably should.

An uncomfortable discovery about F3 itself

While writing this up I went back and ran f3probe repeatedly, because I wanted to quote it accurately. In destructive mode it is rock solid:

destructive 1:  252.00 MB (516096 blocks)
destructive 2:  252.00 MB (516096 blocks)
destructive 3:  252.00 MB (516096 blocks)

Correct to the sector, every time. Without --destructive, on the same card, in the same session:

run 1:   59.50 MB (  121856 blocks)
run 2:  256.99 MB (  526320 blocks)
run 3:    2.00 GB ( 4196224 blocks)
run 4:    2.00 GB ( 4196224 blocks)
run 5:    2.00 GB ( 4196224 blocks)
... seven consecutive runs of 2.00 GB

The safe, non-destructive mode is the one most people reach for first, and on this card it is wrong by up to 8x in the dangerous direction. Follow its advice and you run f3fix --last-sec=4196223, which builds you a 2 GB partition on top of 252 MiB of real storage, and now you have a card that looks fixed and still silently eats your data.

Use f3probe --destructive. On a card you have already written off, which is every card you are running f3probe on, the non-destructive mode is protecting data that does not exist while getting the answer wrong.

I want to be fair to F3 here, because it is an excellent piece of software and its validation logic is essentially identical to the one I independently arrived at: tag each block with its own address, fill the rest with a keyed pseudo-random stream, and verify both on read. That design is exactly right and it is why destructive mode is never fooled by the phantom noise. Something in the non-destructive path, which has to save and restore every block it touches, gets confused on this card in a way I have not finished root-causing.

It reproduces in under two seconds, which is the good news. I am writing it up for the F3 maintainer with the full data.

The accidental fingerprint

Here is the part that might actually be useful to somebody.

Because the stream depends only on the sector number and a firmware constant, and not on which card you read it from, the phantom data identifies the controller. Any other working counterfeit that emits these exact bytes is the same silicon, whatever capacity it claims, whatever brand is printed on the label, whoever sold it. The "working" qualifier is load bearing, for reasons the postscript gets to.

sha256 of sector 516096
  090a539a3a6d27be1f4011fc5ad325b8dce85e7092b29d0c74e59fa658ca6d8d

first 16 bytes
  8b 97 dd 6e 14 1d 66 3c 2a 08 11 99 b5 82 aa cb

If you have a fake card in a drawer, read sector 516096 with O_DIRECT and hash it. I would genuinely like to know whether this is one firmware or one industry.

The remaining ambiguity I could not resolve: both of my cards have the same real capacity, so I cannot tell whether the stream is keyed on the absolute sector number or on the offset past the end of real storage. A counterfeit with a different real capacity settles that in about thirty seconds, and I do not have one. Yet. The cart page is right there.

Loose ends

If you want to actually use one of these, F3 will build you a partition table that stops at the truth:

sudo f3fix --last-sec=516095 /dev/mmcblk0

That gives you an honest 252 MiB card, which is genuinely useful. A flash dump is 8 MB. A router firmware image is 30. I have SPI chips on the bench whose entire contents would rattle around in there.

To reset an SD card without getting up, on a Raspberry Pi 5, unbind and rebind the host controller. There is no remove attribute under /sys/class/mmc_host/, which is annoying, but the platform driver will let go if you ask it directly:

D=/sys/bus/platform/drivers/sdhci-brcmstb
echo 1000fff000.mmc | sudo tee $D/unbind
echo 1000fff000.mmc | sudo tee $D/bind

And if you ever need a block device that aliases on purpose, to test a tool that is supposed to detect aliasing, device-mapper will build you one in three lines. Several linear targets pointing at a single extent is a controller that masks address bits.

Postscript: the patient did not survive

Card 0x00018e56 died on the bench, after something like two dozen destructive probe runs in a single day. In fairness to it, I had spent that day writing to every address it claimed to have, repeatedly, on purpose. This is not a durability finding. This is what happens when you point a tool called "destructive" at a nine dollar counterfeit for six hours.

The way it died is the interesting part, because it is thematically perfect. The card spent its entire working life telling me it had stored things it had not stored. Its final act was to generalise that policy:

block 0        write ACKed, fdatasync OK, stores nothing
block 8192     write ACKed, fdatasync OK, stores nothing
block 262144   write ACKed, fdatasync OK, stores nothing
block 516095   write ACKed, fdatasync OK, stores nothing

Every address, including every address inside the real 252 MiB. It still enumerates. It still reports its CID and serial. It accepts every write without complaint. The kernel logs nothing. A full controller reset does not recover it. Physically removing and reinserting it does not recover it. It is a 62.5 GB card that is now honestly zero bytes, having spent its life being dishonestly 62.5 GB.

And then it made me revise the article, because I checked the phantom stream one more time:

sector 516096, healthy:  090a539a3a6d27be1f4011fc5ad325b8...  high-entropy noise
sector 516096, dead:     every byte 0xFF

16 MiB read from the dead card, bytes that are not 0xFF: 0

The whole device, all 62.5 GB of claimed address space, now returns erased flash. The noise generator is gone.

Which weakens my descrambler theory rather than confirming it. If the phantom stream were simply the descrambler running over erased pages, then a card that is now plainly returning erased-looking data should still be turning it into noise. It is not. It is handing back raw 0xFF.

So the honest version is narrower than what I wrote above. The phantom stream is a property of a functioning controller on this model, not something unconditionally baked into the silicon. Something in the read path that was synthesising that data is no longer running, and whether that path was the descrambler, an ECC engine, or something else entirely, I cannot now distinguish with this specimen.

Everything measured while both cards were alive stands: the stream is deterministic per address, statistically flat, generated rather than stored, survives a controller reset, and is byte identical across two physically separate cards. I have one of those cards left and I am no longer allowed to hurt it.

One last thing

The product name in the CID register, the string the manufacturer burned into every one of these cards at the factory, permanently, is:

name:     asdfg
manfid:   0x000005
oemid:    0x000c
date:     01/2026

Someone built a flash controller sophisticated enough to synthesise cryptographically flat pseudo-random data on demand across a 62 gigabyte address space that does not exist, and then named the product by putting their hand on the keyboard and pushing.

Nine dollars. Best nine dollars I have spent all year.