Run against 15b0fb8 (main, working tree clean at the start). Three questions:
- Is what the project says about itself true?
- What is actually broken in the code?
- Does this repository read like a project a stranger would trust their only Walkman to?
Every claim below was executed, not inferred, unless it says otherwise. Where a defect's mechanism is certain but its trigger is unmeasured, the row says which is which — the distinction this project already makes between device-verified and device-unverified, applied to desk findings.
main went red TWICE today |
15b0fb8 (pushed direct, -Wformat-truncation) and b22202b (a dependabot PR merged over a failing check, cinder-ffi would not compile). Both fixed here. |
| The gate exists and is not required | CI ran on both and said no. Nothing on main requires a passing check — that is the root cause of both, and it is §D5. |
| 2 real defects found | Both in the guard/watchdog machinery added today; one could hang the app permanently. Fixed, with a self-test that fails on the old code. |
| 2 documentation claims were false | The README promised a Linux installer that had not shipped since v0.1.2 and could not have completed an install. Now published, and honest about where it stops. |
| 1 structural problem, half fixed | .git is 1.3 GB, 863 MB of it committed ARM binaries. dist/dev/ is untracked from here (74 of the 117 revisions); recovering the existing 863 MB needs a history rewrite, which is the maintainer's call — §D1. |
| Everything is green now | 429 Rust tests, 46 launcher-recovery cases, 9 guard self-tests, all harness scenarios, shellcheck clean, zero dependency vulnerabilities — and a full ARM cross-build at GLIBC ≤ 2.18, which is the gate no runner can run. |
cinder-home/src/main.cpp:325. The guard-recovery message was widened today from
"GUARD RECOVERED: %s" to a 209-byte string, and its buffer was widened from 128 to 192. So
every one of these lines was truncated mid-sentence:
error: 'snprintf' will always be truncated; specified size is 192,
but format string expands to at least 208 [-Werror,-Wformat-truncation]
This is not a lint nit. It is the single most important line the fault path prints — the one that
tells you Sony IPC has died for the boot and that audio and Bluetooth need a restart — and its tail
was being cut off. tools/host_syntax_check.sh caught it; the GitHub run for 15b0fb8 failed on it.
Fixed: buffer to 320, with a comment saying why the number is what it is. All 20 files parse.
A2. run_guarded wrapped work that is not Sony IPC, and a timeout there leaked a locked Rust mutex — FIXED
run_guarded exists for one failure mode: a Sony IPC call that never returns. Its recovery is a
siglongjmp out of the call, and — as of today's commit — permanently setting g_ipc_dead, on the
correct reasoning that the abandoned call left a half-built std::string inside a closed client.
Four call sites are not Sony IPC:
| Site | What it actually does |
|---|---|
main.cpp:612 |
cinder_db_open — SQLite over ~3,500 tracks. 25 s. Boot path. |
main.cpp:9192 |
reclaim_contents — system("cinder-msc off"), a mount, then cinder_db_open again. 30 s. |
main.cpp:785 |
statvfs. The comment says "guarded for parity", which is honest and is the smell. |
main.cpp:812, :846 |
system() shell-outs, dev channel only. |
Two of those take the Rust render lock. cinder_db_open holds
cell().lock().unwrap() — a std::sync::Mutex<Option<Render>> — across Db::open,
build_library, the playlist store, the liked list and the whole likes import
(lib.rs:4562-4630).
A siglongjmp past a MutexGuard does not run its destructor. The mutex stays locked, with no
owner, for the life of the process. The next frame calls into cinder-ffi, blocks on it, and never
comes back — so the per-frame watchdog fires, latches the bad-boot counter and _exit(42). On the
boot-path site that is a boot to stock, caused by a library that was merely slow.
Mechanism: certain. siglongjmp skipping Rust destructors is not in question, and the guard is
what performs it. Trigger: unmeasured. Nothing has timed cinder_db_open against a cold vfat
/contents under USB-MSC contention, which is exactly the condition site 9192 runs in. The 25 s and
30 s budgets may be comfortable — nobody has checked, and the file's own note says the art cache's
first build across a 304-album library is unmeasured.
Fixed by splitting the guard three ways, on the question a call site actually has to answer — not "might this be slow" but "if this call is abandoned mid-flight, what does it leave behind?"
| Kind | Leaves behind | Behaviour | Sites |
|---|---|---|---|
run_guarded (GUARD_IPC) |
a half-built container in a closed Sony client | unchanged: recover once, then g_ipc_dead |
102 |
run_guarded_local (GUARD_LOCAL) |
nothing | recover and carry on; never touches g_ipc_dead in either direction |
1 (statvfs) |
run_watchdog_only (GUARD_FATAL) |
a held Rust mutex; an unreaped child; system()'s swapped SIGINT/SIGQUIT |
no sigsetjmp at all — a hang falls through fault_handler's un-guarded path: name the call, latch, _exit(42), escape ladder |
4 |
GUARD_FATAL is the part worth dwelling on. Not unwinding looks like the weaker option and is the
stronger one: the old path ended in _exit(42) anyway, just two watchdog cycles later, with
g_ipc_dead set as a red herring and the wrong thread's stack in the log. A clean, immediate,
correctly-labelled exit into a ladder built to absorb exactly this is strictly better than a silent
freeze. fault_handler now prints the stuck call's label via write(2) before the fault dump.
The DB budget went 25 s → 45 s and the reclaim's 30 s → 60 s, because the consequence of overrunning
is now a hard exit rather than a skipped subsystem, and neither has ever been timed. The retry
ladders are unaffected: they are driven by the return code — a missing /contents makes
cinder_db_open return non-zero promptly — not by the timeout.
This is the rule already written down in this project as "an escape must depend on less than what it rescues", applied one level down: a guard must not be able to break more than the call it is guarding.
Tested. The harness cannot reach this — its clock is virtual and alarm() is wall-clock, so no
scenario can make a real recovery happen. It is pure logic, so it went into
cinder-home/tools/guard_selftest.cpp, which CI already runs. That file went from 4 tests to 9;
the 5 new ones produce 6 failures against the pre-split logic and none against the new.
The mirror of A2, and it follows from the same conflation. run_guarded returns -1 immediately
when g_ipc_dead is set. That flag is now permanent for the boot. So after any guard recovery —
one slow NextTrack is enough — the following can never run again:
/contentscan never be reclaimed (site 9192). Sony's stack unmounts the music volume when a cable appears; the reclaim that undoes this is refused. The user gets an empty library, grey album art and a scrobbler writing to an unlinked inode, for the rest of the boot, with no way back except a reboot.- Storage figures in Settings stop updating (site 785).
The user-visible outcome is "I plugged it into my PC and all my music vanished", reached from a transport timeout that the guard successfully recovered from.
Fixed by the same split (A2): only GUARD_IPC consults g_ipc_dead. Self-tests 6 and 7 pin
it — with Sony IPC dead, statvfs and the /contents reclaim both still run.
Two claims, both false:
- "There's a Linux build too." — no Linux artifact has been attached to a release since
v0.1.2(2026-08-20).release.ymlhas a singlewindows-latestbuild job. - "
release.yml… builds the Windows and Linux installers" — it does not.
And it could not usefully, as written: run_sony_updater() is #[cfg(not(windows))] → Err(Unsupported),
so finish_install stages the payload, prints ERROR: could not start the Sony updater, and
exit(1). A published Linux binary would report failure on every successful run.
Fixed by making the build honest and then publishing it, rather than by withdrawing the promise
— because ci.yml had been building and testing the Linux installer on every push the whole time.
The only things ever missing were an honest exit code and an upload step.
run_sony_updater()off Windows now returnsOk(())instead ofErr(Unsupported). The staging is the useful half and it always worked; only the handoff is impossible.print_next_steps()has a#[cfg(not(windows))]arm that says plainly this is as far as a non-Windows build can go, then gives the three manual steps that finish the job (eject cleanly, then Settings ▸ Device Settings ▸ Update on the player), and points at the Windows one-click build as the easier path.release.ymlgains abuild-linuxjob;releasenow needs both. The release body and the README describe exactly where it stops.
An installer that stops halfway and does not say so is worse than one that never ran. This one now says so.
A5. The dependency gate added in this pass immediately broke main — FIXED, and it is the best evidence in this document
Worth reading as a whole, because it demonstrates §D5 better than any argument could.
.github/dependabot.yml was added in this pass (§C3), committed, and pushed. Dependabot picked it
up within minutes and opened two PRs. Both were merged the same hour. The second, #8, bumped six
player crates — three of them across a semver-major boundary, straight through the device's own code
paths:
| Crate | From | To | Reaches |
|---|---|---|---|
zune-jpeg |
0.4.21 | 0.5.15 | album-art JPEG decode |
png |
0.17.16 | 0.18.1 | album-art PNG decode |
rusqlite |
0.32.1 | 0.40.2 | the media library — and its bundled SQLite C source |
fontdue / libc / minifb |
semver-compatible |
CI ran on that PR and went red. Both decoders changed their reader traits — zune-jpeg 0.5 takes
T: ZByteReaderTrait where 0.4 took &[u8], png 0.18 requires Read + Seek — and cinder-ffi
would not compile. Eight errors. It was merged anyway, and main did not build for the next hour.
Two distinct failures, and the second is the serious one:
- A red check did not stop a merge. That is §D5, and it is the same root cause as A1.
rusqliteis a change no gate in this repository can validate.cinder-home/build.shis the only thing on earth that checks this tree against glibc 2.23. The device is glibc 2.23 (2016);rusqlite'sbundledfeature compiles SQLite from vendored C; and build.sh's own header warns that C must be built against the 2.23 headers with LFS off or it emits*_time64/stat64/fcntl64references the device'sld-2.23refuses to resolve. No hosted runner does that link — deliberately; it needs a cross toolchain and a xenial armhf sysroot. So an automatedrusqlitebump can only fail on a maintainer's machine, or on the player.
Resolved, and the bump kept — because it was actually verified rather than assumed:
- The API breaks are fixed (
ZCursorfor zune-jpeg,io::Cursorfor png). One of them is a genuine improvement worth noting:png0.18'soutput_buffer_size()returnsOption<usize>because it now reports overflow instead of wrapping;?on it is a second net under the dimension gate, and 0.17 would have allocated on the wrapped value. cinder-home/build.sh stablewas run in full, which is the gate CI cannot be:OK: cinder-home GLIBC needs = GLIBC_2.4 2.9 2.12 2.15 2.17 2.18— every one under 2.23. The qemu construction preflight passed, the 46-case launcher matrix passed, anddist/stable/was restaged so the committed payload matches the source it was built from.
dependabot.yml was then rewritten so it can only propose what CI can judge: semver-major
updates are ignored for every crate the device links (rusqlite, png, zune-jpeg, fontdue,
embedded-graphics, libc), with the reasoning written into the file. Actions keep majors, because
there CI genuinely does judge the result — a broken action turns its own workflow red.
The lesson generalises past dependencies: an automated gate that proposes changes must be scoped to what the automated gates can check. This repository's most important gate runs on a laptop.
Run locally, in full, after the fixes in this pass:
| Gate | Result |
|---|---|
player test suite |
432 passed, 0 failed (323 cinder-ui + 82 cinder-ffi + 17 cinder-db + 10 integration) |
installer test suite |
7 passed |
Clippy correctness + suspicious |
0 errors |
tools/host_syntax_check.sh |
20 files, all parse |
| C++ self-tests | 7/7 |
tools/shell_check.sh |
47 scripts, bash -n + shellcheck 0.11.0 clean |
cinder-home/harness/run.sh |
all scenarios passed |
cinder-home/tools/test_launcher.sh |
46 passed, 0 failed |
cargo audit (new) |
0 vulnerabilities, both workspaces |
Not desk work. The NW-A55 was on adb, so the fixes were built for ARM, pushed and booted.
Before: dev-channel build from 13:22 (the pre-fix binary), bootcount 0, escape ladder intact
(launcher present, MAXBAD and cable_escape_off both live, no kill switch armed). The current
binary was copied to /data/cinder/backup/ first, so recovery is a push-back rather than a flash —
the existing cinder-home.prev from 2026-08-16 was left alone rather than overwritten.
After — dist/dev/cinder-home, 3,653,580 bytes, GLIBC_2.4 … 2.18, pushed and rebooted:
| Boot | completed, StopBootAnimation at 27.4 s, bootcount stayed 0 |
| Library | 2560 tracks, 256 albums, 166 artists — identical to the pre-reboot baseline, so rusqlite 0.40's bundled SQLite reads the media DB correctly on the device |
| Guard recoveries | 0 |
| Fatal signals / watchdogs | 0 |
| Art decode failures | 0 |
| Stability | 8½ minutes' uptime, clean |
The /contents reclaim ran, and this is the useful part. At 13.343 s:
usb-msc: /contents was unmounted by something other than us — reclaimed it
(cinder-msc off rc=0). Library and album art are back.
That is the exact call site moved to run_watchdog_only in §A2 — a system() mount followed by a
full cinder_db_open — executing on hardware, completing, and the library loading normally
afterwards with Sony IPC untouched. The cable being in for adb is what makes Sony's stack unmount
the volume, so the adb session reproduces the condition for free.
What the boot did not prove, and how it was covered instead. The log said
art cache: 256 cached, 0 to decode — the cache was built by the old binary, so the new decoders
never ran. Clearing it was the obvious next step and was not worth the device state, so the same
question was answered read-only: the actual embedded cover was pulled off the player with dd
(Aña — Anymore, 40,338 bytes, magic=FFD8FFE0) and decoded on the host through decode(). It
came back at the right dimensions with w*h*3 bytes.
That exposed the real gap: decode_jpeg had no test at all, and JPEG is what every cover in
this library actually is. Three were added with hand-built 8×8 fixtures embedded as byte literals —
RGB channel order, the grayscale out.len() == px fan-out branch (which real covers hit, and which
would otherwise be logged as an "unrecognized layout" and dropped), and a truncated file, which is
the realistic corruption when art is read from a FLAC at a recorded offset and length.
- The installer has zero dependencies.
installer/Cargo.lockcontains exactly one package: the installer itself. A signed-by-nobody.exethat people run against their only Walkman, with no supply chain at all, is a genuinely strong position and is now protected by a CI gate (§C3). - The setuid helpers take fixed verbs, never paths.
cinder-poweracceptsoff/restart;cinder-mscaccepts five literals;cinder-clockacceptssetplus a digit string it validates character by character. There is no path or format string reachable from an unprivileged caller into a root process. For twelvechmod 4755installs this is the right design and it is held consistently. - Six TODO markers in ~60,000 lines, five of them one cluster in
ldac-bridgenaming vtable indices that are genuinely not yet recovered. There is no hidden backlog in the comments.
GitHub's own community profile reported code_of_conduct, issue_template and
pull_request_template missing. Added:
CODE_OF_CONDUCT.md— Contributor Covenant 2.1, with one project-specific clause: understating the risk of a build or a procedure is a conduct problem here, not merely a technical disagreement. Enforcement routes to the private security advisory form, the only private channel this repo has..github/ISSUE_TEMPLATE/bug_report.yml— asks for channel, model, Sony firmware version, the log path, and whether the escape ladder brought the device back. That last field makes every crash report double as a test of the recovery mechanism..github/ISSUE_TEMPLATE/device_report.yml— for reporting aDEVICE_CHECKLIST.mditem run on real hardware, pass or fail. This project's bottleneck is device time from people who own the player; there was no route for a stranger to contribute one..github/ISSUE_TEMPLATE/feature_request.yml,config.yml(blank issues off; RECOVERY.md, SECURITY.md and Discussions surfaced at the point of filing)..github/PULL_REQUEST_TEMPLATE.md— with a blast-radius section that separates "UI, cannot brick anything" from "runs as root / boot path / Sony IPC / USB-MSC ordering", and the six local check commands as a checklist.
CHANGELOG.md, Keep a Changelog format, backfilled from tags v0.1.0–v0.1.5 and linked from the
README and the release body. Entries carry the project's own device-verified / device-unverified
marking, because for this codebase "what changed" and "was it ever run on hardware" are different
questions and the second one matters more.
122 crates reach the binary that runs as the device's Home app, and no gate looked at any of them.
Added an audit job to ci.yml running cargo audit over both workspaces, plus a weekly cron so
an advisory published against unchanged code is found by the calendar rather than by the next
unrelated commit.
Verified locally before wiring, per this repo's own rule about CI steps: 0 vulnerabilities, exit
0 on both. Two unmaintained-crate warnings (instant, ttf-parser, both transitive via fontdue)
are reported and do not fail the build, which is the right split — a vulnerability is a stop, an
unmaintained transitive dependency is a fact to know.
.github/dependabot.yml added: monthly, grouped, low PR limit. Monthly on purpose — this repo ships
binaries built by a maintainer and committed, so a dependency bump landing unattended is a bump
whose output nobody rebuilt.
docs/README.md says which document answers which question and, more usefully, which ones are
history. PRODUCTION_READINESS.md and FLASH_NEXT.md were accurate on 2026-07-28 and are now
read as current by anyone who opens them cold.
Added CI / release / downloads / licence / device badges, a Documentation section pointing at the new index, and a Contributing section that says plainly that the most useful contribution is a device report, because it is, and nothing said so.
D2 and D3 were fixed in this pass and are kept here with their outcomes, because the reasoning is the useful part. D1, D5 and D6 are open. D5 is the one that matters: it is the root cause of both of today's breakages, and it needs an authenticated maintainer — nothing in this pass could touch it.
.git total |
1.3 GB (418 MB in July — it has tripled) |
Blobs under cinder-home/dist/ |
863 MB, 66% of the repository |
Revisions of dist/dev/cinder-home |
38 |
Revisions of dist/dev/cinder-probe |
36 |
Revisions of dist/stable/* |
22 and 21 |
117 copies of ~3.5 MB ARM ELFs, every one of them rewritten in full because a stripped binary shares
no deltas with the last build. A git clone of a ~60,000-line project transfers 1.3 GB, and that is
the first thing anyone evaluating this repository experiences.
The binaries are tracked for a good reason — they are the flashable deliverable, the ARM toolchain
is not reproducible on a hosted runner, and tools/release.sh verifies the committed payload
byte-for-byte before it will tag. That reason justifies shipping them; it does not justify keeping
every intermediate build forever. Three options, cheapest first:
- Stop committing
dist/dev/. It is 74 of the 117 revisions and it is not what users install. Publish dev builds as CI artefacts or pre-release assets instead. Costs nothing, halves the bleeding, does not touch history. - Commit
dist/stable/only on release commits. A binary per tag, not per push — six objects a year instead of twenty. - Rewrite history (
git filter-repo) to drop supersededdist/blobs, keeping the ones tagged releases point at. Recovers most of the 863 MB. This rewrites every SHA, so it needs a force-push and it invalidates every existing clone and every commit hash quoted indocs/— of which there are many, deliberately. Only worth it alongside (1) and (2), or the growth resumes the next day.
Recommended: (1) now, (2) next release, and treat (3) as a separate decision made once the bleeding has stopped.
2026-09-11 — the 1.3 GB was one working copy's
.git, not the clone. GitHub reports the repository at 124 MB, and a fresh clone downloads about that; the.gitthat was measured held 1.09 GB of loose objects thatgit gchad never packed. The committed binaries are still most of the history, and option 3 is still how to drop them — now as one step of a rewrite that also takes Sony's files out, rehearsed inHISTORY_REWRITE.md.
Its header says "Last audited: 2026-07-28". STATUS.md carries entries through 2026-08-31.
Between them sit the FM tuner, the clock helper, Bluetooth, NFC, playlists, the on-screen keyboard,
liked-songs sync, the device settings page and six audits — none of which the roadmap knows about.
Its P1 list still describes Bluetooth as "P2, backend not wired".
The README calls it "Forward plan" and docs/README.md cannot fix this by describing it, because
unlike the dated audits a roadmap is supposed to be current. Either re-audit it, or demote it: put
the forward plan in DEVICE_CHECKLIST.md (which is current) and keep ROADMAP.md as a dated
snapshot like the audits, with the date in its filename.
battery_track.tsv (7.7 KB of session data) and cinder_screen_20260826_113143.png (468 KB, a
timestamped screenshot) were tracked at the top level. .gitignore already excluded rendered screen
previews on the grounds that they are regenerable; these two slipped past.
Fixed: both moved to artifacts/session/ (ignored wholesale) and untracked, with
/battery_track.tsv and /cinder_screen_*.png added to .gitignore so they cannot drift back. The
repository root is now thirteen files, every one a document or a manifest.
This is the most important item in this document, and it is the one that cannot be fixed from a shell. It needs an authenticated maintainer.
Two red commits reached main on 2026-09-01:
| Commit | How | What CI said |
|---|---|---|
15b0fb8 |
pushed directly to main |
failure at 12:25 UTC (§A1) |
b22202b |
dependabot PR #8 merged | failure on the PR run for 92fd6b2 (§A5) |
The gate worked both times. It ran, it was correct, and it was not required. Everything else in this audit — 429 tests, the launcher matrix, the harness, the self-tests, the advisory scan — is only worth what the merge rule makes it worth, and right now that is nothing.
The fix is one API call, and it also makes the dependabot.yml caution in §A5 mean something,
because a red PR merged is a red main either way:
gh api -X PUT repos/superwilso/Cinder/branches/main/protection \
--input - <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": [
"player (host tests)",
"installer (linux)",
"installer (windows)",
"C/C++ syntax + self-tests + shell + harness + launcher",
"dependency advisories",
"dist payload is complete"
]
},
"enforce_admins": false,
"required_pull_request_reviews": null,
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false
}
JSONenforce_admins: false and required_pull_request_reviews: null are deliberate for a
single-maintainer project: this should stop a red merge, not require a second person who does not
exist. allow_force_pushes: false is worth having regardless — and note it would have to be lifted
deliberately for the history rewrite in §D1, which is the right amount of friction for that.
Check what is set today with:
gh api repos/superwilso/Cinder/branches/main/protectionTopics are nw-a30, nw-a40, nw-a50, walkman, walkmanone — every one about the device, none about
what this repository is, so it does not surface for anyone searching the technology:
gh api -X PUT repos/superwilso/Cinder/topics -f names='["walkman","nw-a50","nw-a40","nw-a30",
"walkmanone","rust","reverse-engineering","firmware","embedded","custom-firmware","ldac",
"bluetooth","music-player","digital-audio-player","hi-res-audio"]'
gh api -X PATCH repos/superwilso/Cinder \
-f homepage='https://github.com/superwilso/Cinder/releases/latest'Wiki is off and Discussions is on, which is the right way round for a project whose documentation is this good in the tree.
Every check in Part B was executed on this machine at the tree described. GitHub state was read from
the public API (/repos, /releases, /actions/runs, /pulls, /community/profile) — gh is not
authenticated here, so nothing was written to GitHub in this pass: no issue, release, merge or
setting was changed, and D5 and D6 are open for exactly that reason. The ARM figures in Part B come
from a full cinder-home/build.sh stable run on this machine, which has the cross toolchain and the
xenial armhf sysroot. Repository sizes come from git rev-list --objects --all piped through
git cat-file --batch-check, not from estimates.