Skip to content

sdcard: report what the card says about itself, and its remaining life - #221

Merged
widgetii merged 3 commits into
masterfrom
sdcard/vendor-health-over-cmd56
Sep 22, 2026
Merged

widgetii merged 3 commits into
masterfrom
sdcard/vendor-health-over-cmd56

Conversation

@widgetii

Copy link
Copy Markdown
Member

There is no SMART on SD. The CID and CSD say what a card was sold as and
nothing about its condition, so "is this card worn out" has had no answer on a
camera. A few lines — industrial and surveillance ones — do implement a vendor
health register, read with CMD56 (GEN_CMD), carrying a percentage of rated
life used.

ipctool sdcard reads the identity out of /sys and, where the card is one of
those, the register too:

---
sdcard:
  device: mmcblk0
  name: WX32G
  manfid: 0x000003
  oemid: 0x5344
  serial: 0xe3a829fd
  date: 03/2025
  cid: 035344575833324780e3a829fd0193bb
  health:
    vendor: SanDisk / Western Digital
    life_used_percent: 1
    note: percentage of rated life the card reports as used; the rating itself is in no register
    signature: DW250316
    manufacturer: Western Digital

--json and --raw (the 512-byte register as hex) are there too.

Look at the last line. The CID has no brand field, and its manufacturer id
0x03 is registered to SanDisk — which Western Digital has owned since 2016,
so a WD Purple is indistinguishable from a SanDisk by the CID alone. The
register is the only place the card says what is printed on it.

Two things it deliberately will not do

It does not probe a card whose manufacturer id is not in the table. CMD56
with an argument a card does not implement is not free. Measured on the board
below: the data phase times out (-110), the card then raises its ERROR status
bit, and the next command on the host fails once (-13,
himci_cmd_done: The status of the card is abnormal) before the card clears
it. It self-heals — three follow-up CMD13s and a dd all passed — so it is
harmless to a diagnostic run by hand, but it is not something to hand a camera
that is writing video.

It does not decode a vendor it has not been read against. #833's survey
names SanDisk Industrial, WD Purple, Kingston Industrial, Apacer and Longsys,
each with its own argument and layout. I have one card. Shipping a plausible
guess at the other four would print a life figure nobody measured, which is
worse than printing nothing — so an unknown card gets a sentence saying the
register was not read and why, rather than a silent gap. The table is one
struct per vendor; adding one is a few lines plus a card to check it on.

Measured

WD Purple QD101 32 GB in an hi3516av300 (HiSilicon himci, kernel 4.9.37),
with the camera recording at 4 Mbit throughout:

  • the read costs ~5 ms including fork and exec
  • 50 consecutive reads, 50 successes; 10 more through the shipping musl
    binary, no failures
  • the recorder saw 0 dropped fragments, 0 write errors, 0 sync errors, and
    records_fsync_us_max did not move

The register embeds the card's own CID at offset 0x195 and it matches /sys
byte for byte. That is what says the reply belongs to this card rather than
being a stale buffer, and it is worth knowing the check exists.

Notes for review

  • The ioctl needs the whole-device node. The kernel refuses MMC_IOC_CMD
    on a partition with EPERM, to keep one partition's commands out of its
    siblings — so this opens /dev/mmcblk0, never p1.
  • linux/mmc/ioctl.h is not in every toolchain's sysroot, so the request is
    spelled out locally. The ABI is stable.
  • It is a subcommand rather than a section of the default report, on purpose:
    the report runs on every bare ipctool, and issuing an SD command there
    would be a behaviour change for every camera rather than something asked for.
  • No arch-specific code, so no new __arm__/__mips__/__aarch64__ guards.
  • No unit test. The decodable logic is a table scan and a memcmp; the rest
    is an ioctl. I could expose layout_for() and the signature check to pin
    "never probe an unknown vendor", which is the one rule worth pinning — say
    the word and I will add it.

Built and tested

arm32-musl, arm32-gnueabi and arm64-musl all build clean with no warnings from
the new file. cYAML_test, reginfo_test, longse_test and
tools/test_pipeline.sh all pass. clang-format reports no replacements.
mips32 not built locally (no toolchain on this machine) — CI covers it.

Related

A latent bug in the HiSilicon himci driver found while establishing that the
passthrough is safe — one error path that never completes the MMC request,
which wedges the host — is OpenIPC/linux#57. It is not required for this to
work; nothing here triggers it.

There is no SMART on SD. The CID and CSD say what the card was sold as and
nothing about its condition, which is why "is this card worn out" has had no
answer on a camera.

A few lines do implement one: a vendor register read with CMD56 (GEN_CMD),
carrying a percentage of rated life used. `ipctool sdcard` reads the identity
out of /sys and, where the card is one of those, the register too:

    ---
    sdcard:
      device: mmcblk0
      name: WX32G
      manfid: 0x000003
      oemid: 0x5344
      cid: 035344575833324780e3a829fd0193bb
      health:
        vendor: SanDisk / Western Digital
        life_used_percent: 1
        signature: DW250316
        manufacturer: Western Digital

Note the last line. The CID has no brand field, and its manufacturer id 0x03
is registered to SanDisk -- which Western Digital has owned since 2016, so a
WD Purple is indistinguishable from a SanDisk by the CID alone. The register
is the only place the card says what is printed on it.

TWO THINGS IT DELIBERATELY WILL NOT DO.

It does not probe a card whose manufacturer id is not in the table. CMD56 with
an argument a card does not implement is not free: measured on the board
below, the data phase times out, the card raises its ERROR status bit, and the
NEXT command on the host fails once before the card clears it. Harmless to a
diagnostic run by hand; not something to hand a camera that is writing video.

And it does not decode a vendor it has not been read against. Shipping a
plausible guess at another vendor's layout would print a life figure nobody
measured, which is worse than printing nothing -- so an unknown card gets a
sentence saying the register was not read and why, rather than a silent gap.

Measured on a WD Purple QD101 32 GB in an hi3516av300 (HiSilicon himci, kernel
4.9.37), with the camera recording throughout: the read costs about 5 ms
including fork and exec, 50 consecutive reads all succeeded, and the recorder
saw no dropped fragments and no write or sync errors. The register embeds the
card's own CID, which matches /sys byte for byte -- that is what says the reply
belongs to this card rather than being a stale buffer.

The ioctl needs the whole-device node: the kernel refuses MMC_IOC_CMD on a
partition with EPERM, to keep one partition's commands out of its siblings.
linux/mmc/ioctl.h is not in every toolchain's sysroot so the request is spelled
out locally; the ABI is stable.

Builds clean for arm32-musl, arm32-gnueabi and arm64-musl; cYAML, reginfo and
longse tests pass, as does tools/test_pipeline.sh.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add SD card identity and vendor health reporting

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds an sdcard command reporting card identity and vendor-provided wear data.
• Restricts CMD56 probing to verified vendors and validates register signatures before decoding.
• Supports YAML, JSON, and optional raw health-register output.
Diagram

graph TD
  A["ipctool sdcard"] --> B["Read sysfs"] --> C{"Vendor verified?"}
  C -- "yes" --> D["CMD56 ioctl"] --> E{"Signature valid?"}
  E -- "yes" --> F["Decode health"] --> G["YAML or JSON"]
  C -- "no" --> G
  E -- "no" --> G
Loading
High-Level Assessment

The allowlisted, table-driven CMD56 implementation is appropriate because unsupported probes can temporarily disrupt card I/O and vendor layouts are not standardized. Broader speculative probing and external utility delegation were reasonably avoided due to safety, verification, portability, and embedded-footprint concerns.

Files changed (4) +316 / -0

Enhancement (3) +315 / -0
main.cRoute the sdcard CLI subcommand +3/-0

Route the sdcard CLI subcommand

• Includes the SD card command interface and dispatches 'ipctool sdcard' before the common option parser.

src/main.c

sdcard.cImplement SD identity and vendor health reporting +306/-0

Implement SD identity and vendor health reporting

• Discovers an MMC whole-device node, reads identity attributes from sysfs, and conditionally retrieves verified vendor health data through CMD56. Valid responses expose wear percentage and manufacturer details in YAML or JSON, with optional raw hex output and explicit unsupported or failure messages.

src/sdcard.c

sdcard.hDeclare the sdcard command entry point +6/-0

Declare the sdcard command entry point

• Introduces the public declaration used by the main command dispatcher.

src/sdcard.h

Other (1) +1 / -0
CMakeLists.txtCompile the SD card command implementation +1/-0

Compile the SD card command implementation

• Adds 'src/sdcard.c' to the ipctool source list so the new subcommand is included across supported builds.

CMakeLists.txt

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Some cards make later commands fail ✓ Resolved 🐞 Bug ☼ Reliability
Description
layout_for authorizes CMD56 for every card whose manufacturer ID is 0x03, although the file
states that only particular industrial and surveillance product lines implement the register. An
unsupported SanDisk product with that shared ID therefore receives the probe that the same file
documents can set the card error bit and make the next host command fail.
Code

src/sdcard.c[97]

+    {0x03, "SanDisk / Western Digital", 0x00000001, {"DS", "DW"}, 8},
Evidence
The introductory comments limit these registers to selected industrial and surveillance lines and
document that an unsupported command can break the next host command, but the sole table entry and
lookup gate only on the manufacturer ID. The selected layout is then passed directly to
read_health before any response validation is possible.

src/sdcard.c[7-10]
src/sdcard.c[18-23]
src/sdcard.c[91-104]
src/sdcard.c[243-260]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Every card with manufacturer ID `0x03` currently receives the vendor CMD56 probe, including product lines for which that command has not been verified and may disrupt the next command.
## Fix Focus Areas
- src/sdcard.c[91-104]
- src/sdcard.c[243-266]
## Recommended Fix
Require enough sysfs identity attributes to match an explicitly verified product family before calling `read_health`. If a card cannot be positively matched, return the existing not-verified health message without issuing CMD56.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. The wrong storage card is inspected ✓ Resolved 🐞 Bug ≡ Correctness
Description
find_card treats the existence of either the type or cid attribute as a match and returns the
first numbered MMC block device without checking that its reported type is SD. On a system where
embedded MMC precedes a removable card, the command reports the embedded device and may apply the
selected vendor probe to it instead of inspecting the SD card.
Code

src/sdcard.c[R132-135]

+        snprintf(name, sizeof(name), "mmcblk%d", i);
+        char probe[64];
+        if (sysfs_str(name, "type", probe, sizeof(probe)) ||
+            sysfs_str(name, "cid", probe, sizeof(probe))) {
Evidence
The discovery condition uses successful attribute reads rather than the value of type, and
immediately returns the first matching numbered device. That selected name controls both all
reported identity fields and the whole-device node passed to the ioctl.

src/sdcard.c[107-123]
src/sdcard.c[129-140]
src/sdcard.c[143-166]
src/sdcard.c[217-247]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Device discovery returns the first MMC block device with common sysfs attributes, so embedded MMC can be selected instead of the SD card requested by the command.
## Fix Focus Areas
- src/sdcard.c[107-141]
- src/sdcard.c[217-247]
## Recommended Fix
Read each candidate's `type` attribute and accept only a device whose normalized value is `SD`. Continue searching past MMC, SDIO, and malformed candidates before reporting that no SD card was found.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Higher-numbered cards appear absent ✓ Resolved 🐞 Bug ≡ Correctness
Description
find_card searches only mmcblk0 through mmcblk3 instead of enumerating the block devices
exposed by sysfs. A valid card assigned mmcblk4 or above is therefore never examined and causes
the command to print that no card was found.
Code

src/sdcard.c[R129-132]

+static bool find_card(char *dev, size_t cap) {
+    for (int i = 0; i < 4; i++) {
+        char name[8];
+        snprintf(name, sizeof(name), "mmcblk%d", i);
Evidence
The loop condition explicitly stops after index three, and the command maps its false result
directly to the no-card error path. No alternative enumeration or caller-provided device selection
exists.

src/sdcard.c[129-140]
src/sdcard.c[217-221]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The fixed four-device search misses valid cards whose kernel block-device index is four or greater.
## Fix Focus Areas
- src/sdcard.c[107-141]
## Recommended Fix
Enumerate `/sys/block` entries matching `mmcblk` followed by a numeric whole-device suffix, then inspect their type attributes. Do not impose an arbitrary maximum device index.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Wrong replies can become health data 🐞 Bug ≡ Correctness
Description
sig_matches validates only the first two bytes against DS or DW, while the documented embedded
card identifier at offset 0x195 is never compared with the sysfs identifier. Any successful
response carrying that short prefix is consequently decoded as the selected card's life value even
when it does not belong to that card.
Code

src/sdcard.c[R174-176]

+    for (size_t i = 0; i < sizeof(lay->sig) / sizeof(lay->sig[0]); i++)
+        if (lay->sig[i] && !memcmp(buf, lay->sig[i], strlen(lay->sig[i])))
+            return true;
Evidence
The layout comment identifies the embedded CID, but the layout carries no CID metadata and
sig_matches performs only a prefix comparison. The sysfs CID is added to output and then
overwritten in the shared buffer, so it is unavailable when a successful response is accepted and
decoded.

src/sdcard.c[91-97]
src/sdcard.c[172-178]
src/sdcard.c[229-245]
src/sdcard.c[260-272]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Health data is accepted using only a two-byte prefix even though the register contains an embedded card identifier intended to prove that the reply belongs to the selected card.
## Fix Focus Areas
- src/sdcard.c[83-98]
- src/sdcard.c[172-178]
- src/sdcard.c[229-266]
## Recommended Fix
Parse the 32 hexadecimal CID characters read from sysfs into 16 bytes and compare them with the register bytes at offset `0x195` after the signature check. Reject the response without decoding health data when parsing fails or the identifiers differ.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
5. Failed reads look like zeroed registers ✓ Resolved 🐞 Bug ≡ Correctness
Description
sdcard_cmd clears buf before the ioctl but serializes it whenever --raw is requested,
regardless of whether read_health succeeded. An ioctl failure therefore produces a 512-byte
all-zero raw field even though no register data was received.
Code

src/sdcard.c[R291-294]

+        if (raw) {
+            char hex[HEALTH_LEN * 2 + 1];
+            for (int i = 0; i < HEALTH_LEN; i++)
+                snprintf(hex + i * 2, 3, "%02x", buf[i]);
Evidence
The buffer is initialized entirely to zero, the ioctl failure branch does not populate it, and the
later raw block has no success condition. Thus the generated hexadecimal value is synthetic on every
failed ioctl.

src/sdcard.c[255-265]
src/sdcard.c[291-296]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The raw output path serializes the initialized buffer after failed reads, falsely presenting synthetic zero bytes as data returned by the card.
## Fix Focus Areas
- src/sdcard.c[255-295]
## Recommended Fix
Track whether the ioctl completed successfully and add the `raw` field only in that case. Preserve raw output for successful but unrecognized replies if diagnostic access to those bytes is intended.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can route each action level your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/sdcard.c
Comment thread src/sdcard.c Outdated
Comment thread src/sdcard.c Outdated
Comment thread src/sdcard.c
Comment on lines +174 to +176
for (size_t i = 0; i < sizeof(lay->sig) / sizeof(lay->sig[0]); i++)
if (lay->sig[i] && !memcmp(buf, lay->sig[i], strlen(lay->sig[i])))
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Wrong replies can become health data 🐞 Bug ≡ Correctness

sig_matches validates only the first two bytes against DS or DW, while the documented embedded
card identifier at offset 0x195 is never compared with the sysfs identifier. Any successful
response carrying that short prefix is consequently decoded as the selected card's life value even
when it does not belong to that card.
Agent Prompt
## Issue description
Health data is accepted using only a two-byte prefix even though the register contains an embedded card identifier intended to prove that the reply belongs to the selected card.

## Fix Focus Areas
- src/sdcard.c[83-98]
- src/sdcard.c[172-178]
- src/sdcard.c[229-266]

## Recommended Fix
Parse the 32 hexadecimal CID characters read from sysfs into 16 bytes and compare them with the register bytes at offset `0x195` after the signature check. Reject the response without decoding health data when parsing fails or the identifiers differ.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/sdcard.c Outdated
Five from review, and the first one matters most because it undoes a safety
claim the file made about itself.

**A manufacturer id is not a safety gate.** 0x03 is SanDisk's, and Western
Digital has shipped under it since buying them -- so it covers the WD Purple
that implements the health register AND every consumer SanDisk that does not,
which is most of the cards in these cameras. Gating the probe on it looked like
a rule and probed almost everything, which is exactly the one-poisoned-command
cost the file documents, spent on cards that were never going to answer.

So the identity is free and always printed, and the register is read only when
asked for with --health, with what that costs written into --help. What the
vendor table still decides is decoding: a layout nobody has read against a real
card is not guessed at.

**The card is found, not assumed.** find_card() counted mmcblk0..3 and took the
first device with a cid. It now walks /sys/block and insists on type "SD": the
numbering follows probe order so a card can land anywhere, and a board with
eMMC usually has it as mmcblk0 with the card behind it -- reporting the
soldered part, let alone aiming a vendor command at it, is the wrong answer to
"what is in the slot".

**The reply has to belong to the card.** Two signature bytes are not much of a
claim. The commit that added this said the embedded CID is what ties the 512
bytes to the card in the slot, and then did not check it. It does now, and
reports `cid_echoed`. Searched for rather than read at a fixed offset, and
reported rather than enforced: it was confirmed at 0x195 on one card, and a
layout that puts it elsewhere should not have its health refused on the
strength of one sample.

**A failed read is not a register of zeroes.** --raw serialised the buffer
whether or not anything came back, so an ioctl failure printed 512 bytes of
made-up register. It is emitted only after a read that produced one.

Checked on the WD Purple QD101 in an hi3516av300: the default prints identity
and sends the card nothing, --health returns the register with
`cid_echoed: true`, and --raw after a refused read prints no raw field.
cYAML, reginfo, longse and test_pipeline.sh pass; arm32-musl and arm64-musl
build clean; clang-format reports no replacements.
@widgetii

Copy link
Copy Markdown
Member Author

All five were right, and the first one undoes a claim the file made about
itself. Fixed in 4cab0b9.

1. Some cards make later commands fail. The sharpest of the five. 0x03 is
SanDisk's id and Western Digital has shipped under it since buying them, so it
covers the WD Purple that implements the register and every consumer SanDisk
that does not — which is most of the cards in these cameras. Gating on it read
like a safety rule and probed almost everything, spending the documented
one-poisoned-command cost on cards that were never going to answer.

A manufacturer id cannot decide this, so nothing pretends it can. The identity
is free and always printed; the register is read only on --health, with what
that costs written into --help. The vendor table now decides only decoding
a layout nobody has read against a real card is still not guessed at.

2 and 3. The wrong card, and cards above mmcblk3. Both real. find_card()
walks /sys/block instead of counting, and insists on type SD — numbering
follows probe order, and a board with eMMC usually has it as mmcblk0 with the
card behind it. Reporting the soldered part, let alone aiming a vendor command
at it, is the wrong answer to "what is in the slot".

4. Wrong replies can become health data. A fair hit, and the gap was between
the prose and the code: the commit message said the embedded CID is what ties
the 512 bytes to this card, and then never checked it. It does now and reports
cid_echoed. Searched for rather than read at 0x195, and reported rather than
enforced — it was confirmed at that offset on one card, and a layout that puts
it elsewhere should not have its health refused on the strength of one sample.

5. Failed reads look like zeroed registers. Correct. --raw serialised the
buffer whether or not anything came back, so a refused ioctl printed 512 bytes
of invented register. It is emitted only after a read that produced one.

Checked on the WD Purple QD101 in an hi3516av300:

$ ipctool sdcard                 # sends the card nothing
  health: "not asked for: pass --health to read the vendor register, ..."

$ ipctool sdcard --health
  health:
    vendor: SanDisk / Western Digital
    life_used_percent: 1
    cid_echoed: true

$ ipctool sdcard --raw | grep -c raw:
0

cYAML_test, reginfo_test, longse_test and tools/test_pipeline.sh pass;
arm32-musl and arm64-musl build clean with no warnings from the new file;
clang-format reports no replacements.

Follow-up to review finding 4. The corroboration was there but it was a
boolean at the bottom of the block, which is exactly the kind of thing a reader
skims past on the way to the number.

When the reply does not carry this card's CID, the note beside
life_used_percent now says so instead of describing what the figure means in
general.

Still not a gate, and deliberately. The signature is the vendor's own magic, so
a reply that matches IS this vendor's health register; the only thing in doubt
is whether it came from the card in the slot, and the ioctl was aimed at that
card's own node. Refusing to print a figure on that basis would lose a good
reading from any card whose layout puts its CID somewhere other than the one
offset this has been read against. Reporting beats refusing when the
uncertainty is about provenance rather than content -- and now it reports
loudly.
@widgetii

Copy link
Copy Markdown
Member Author

Finding 4 — I have improved it but deliberately not gated on it, so here is the
reasoning to accept or overrule.

The corroboration was already computed; it was a boolean at the bottom of the
block, which is the kind of thing a reader skims past on the way to the number.
The note beside life_used_percent now carries the verdict instead:

note: percentage of rated life reported, BUT this reply does not carry this
      card's CID, so it could not be confirmed as coming from the card in the
      slot

Why it still prints the figure. The signature is the vendor's own magic, so
a reply that matches is this vendor's health register — the content is not in
doubt. What the CID would confirm is provenance: that the register came from
the card in the slot. And the ioctl was aimed at that card's own whole-device
node, so there is no multiplexing that could return a different card's answer.

Against that, gating has a concrete cost: the offset was confirmed on exactly
one card. A WD or SanDisk line that places its CID elsewhere, or omits it, would
lose its health reading entirely — a false negative on a perfectly good card,
caused by generalising from one sample. That is the failure I would rather not
ship, and it is why the check is searched-for across the whole register rather
than read at 0x195.

So: report loudly, refuse nothing. If you would rather it hard-fail without the
CID echo, say so and it is a two-line change — but I would want a second card's
register read first, so the gate is built on more than one observation.

The other four are in 4cab0b9 and verified on the WD Purple QD101 in an
hi3516av300: the default sends the card nothing, --health returns the
register with cid_echoed: true, --raw after a refused read emits no raw
field, and find_card() now walks /sys/block insisting on type SD.

@widgetii
widgetii merged commit 85e270c into master Sep 22, 2026
5 checks passed
@widgetii
widgetii deleted the sdcard/vendor-health-over-cmd56 branch September 22, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant