Skip to content

Write /system in pieces where adb shell cannot carry it, and add two diagnostics - #107

Merged
bboe merged 4 commits into
mainfrom
dot-firmware-report
Oct 9, 2026
Merged

bboe merged 4 commits into
mainfrom
dot-firmware-report

Conversation

@bboe

@bboe bboe commented Oct 8, 2026

Copy link
Copy Markdown
Owner

A Windows host cannot root a Dot: its adb ends a stream at the first 0x1a, so the system image stops after 238 bytes and the failure reads as a bad eMMC. The write now goes over in pieces where that happens. Two commands collect what an issue needs.

This is what a reporter saw, three times, with a healthy card and cable:

[ 9/13] writing Fire OS 5.5.5.4's /system     (~100 s) ⠇ 6s

/dev/block/platform/mtk-msdc.0/by-name/system_a did not arrive; waiting for the
Dot to reconnect to try again.

ERROR: /dev/block/platform/mtk-msdc.0/by-name/system_a was not written intact
after 3 tries; the last: its md5 read back did not match. The cached image was
discarded, so the next run rebuilds it

and this is the same stream, same Dot, same recovery, minutes apart:

Windows 11, adb 36.0.1   gunzip: gzclose: Illegal seek
                         0+1 records out, 12 bytes, rc 0, md5 mismatch
macOS, adb 37.0.0        64 MiB through gunzip | dd, md5 matches

🤖 Generated with Claude Code

@bboe
bboe force-pushed the dot-firmware-report branch 9 times, most recently from 7f713cc to 2b9e164 Compare October 9, 2026 01:48
bboe and others added 3 commits October 8, 2026 18:49
An issue report carried the failing step and nothing about the Dot. This
collects the host, the state, the build, the eMMC, the partition table with
each name's node, and the kernel's own mmc lines.

Only a recovery answers all of that, so it starts one. A Dot already in a
recovery is left alone: the one a failed run left is the safe place to be,
and a first version cost a reporter theirs.

system_a is the reason it exists. Fire OS 5's image is 805,257,216 bytes
against a 805,306,368-byte partition, so the write has 48 KiB of headroom
and nothing checks it. Build 8149's report showed only a mismatched md5.

Wear comes from sysfs where a part reports it, and the erase count only from
the kernel log, which reaches back about 3,000 seconds, so uptime is printed
beside it. cid and serial are left unread, and GUIDs are masked.

Each device-side answer is read behind a marker of its own, because adb cuts
the tail: a table that does not arrive whole is printed as it came rather
than parsed into claims about which names point nowhere. A failed system
write discards the image and keeps the record of its size, which is the
number a report is run for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A failed install blames an md5, and the card, the cable and the host are all
candidates. This writes one pattern over two paths that share no hardware:
into /tmp, which is RAM, for adb and the cable with no eMMC in it; and from
the Dot to its own cache, for the card with no USB in it.

It writes over cache, which an install formats anyway, so a part-written
system_a stays as evidence. The pattern is not zeros, because a card that
writes nothing passes a zero test. It is every byte value followed by a word,
so nothing a transport mangles goes unnoticed, and its odd length makes a
slice written at the wrong offset hash differently. The USB leg leaves its
verified file in RAM and the eMMC leg writes that same file, so the legs
carry identical bytes and the second is skipped when the first proved
nothing.

cache is reformatted first and the pattern goes behind the new filesystem's
own blocks. So the writes are raw, like the /system write they stand in for,
nothing is mounted while they land, and cache holds a whole-partition
filesystem at every moment: a run stopped part way leaves a Dot that still
boots with its cache the size it should be. A Dot whose cache carries no
filesystem does not boot -- fs_mgr_mount_all fails in init's on fs, so class
main never starts and the Dot answers adb as root with no framework.

The layout is pinned, not inherited, because that boundary has to be an input.
-J size=4 -N 8192 with -O ^sparse_super,^resize_inode -E packed_meta_blocks=1
puts every metadata block inside the first 6.11 MiB of a 784 MiB cache, read
out of the superblock and the journal inode, against a 16 MiB margin. Left to
its defaults mke2fs puts a 16 MiB journal at 12.33-28.33 MiB, and e2fsck is no
guard there: after writing from 16 MiB it passed all five passes on a
filesystem missing 12.3 MiB of that journal, because a clean filesystem's
journal body is never checked.

A test that cannot run must not read as a card that failed. So the digest
comes back behind a marker and dd's stderr is kept; a refused umount, a
target that is not a block device and a format that did not happen each say
so, because the read-back names the node it wrote and a file in TWRP's RAM
/dev would match itself; and each skip says which of the three reasons it
was. conv=notrunc is toybox dd only, so it asks which dd is there, and the
whole command is one line with no quotes or Windows loses it.

Verified on bryce in TWRP 3.7.0_9-bboe2: 128 MiB over USB at 20 MiB/s, then
768 MiB written and read back at 18 MiB/s, both digests matching. Separately,
overwriting 16-784 MiB of the pinned filesystem left e2fsck clean, mounted,
carried a 4 MiB file through the journal, unmounted, checked clean again, and
read that file back unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Between clearing boot0's header and writing the preloader, a stopped run
leaves a Dot that shows no light and starts nothing. Only the stock restore
said so, and a reporter stopped at the system image, rebooted, and read the
dark ring as a brick.

_die now says it whenever a run that writes meets the marker, Ctrl-C
included, which is the likeliest way to stop inside that window. It says what
to do: the Dot waits in its bootrom and a rerun on the same computer picks it
up by itself. No button gesture reached fastboot from there when it was
tried.

It is a run that writes, not any error, because the marker is one file for
every Dot: --report and --write-test write nothing and must not speak for a
marker another Dot left behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bboe
bboe force-pushed the dot-firmware-report branch 2 times, most recently from 82af88e to 68a5789 Compare October 9, 2026 02:00
A Windows host cannot root a Dot, and has not since 1c71a24 put the /system
write on adb shell's stdin: that client reads stdin in text mode, where 0x1a
ends the file. The image holds 1,421,626 of them and the first is at offset
238, so 238 of 385,745,930 bytes arrive and gunzip writes 12. A pipeline
carries its last command's status, so dd exits 0 and only the md5 complains,
which reads as a failing card. 1c71a24 dropped exec-in because this TWRP
truncates it, and said "Windows is untried".

The image is now built as 128 MiB slices, each its own gzip member. Where
stdin carries a stream they go out back to back, a valid multi-member gzip
and what macOS has always done; where it does not, each slice is pushed to
/tmp and unpacked at its own offset, so nothing is staged on the eMMC and a
recovery's 240 MiB tmpfs is room enough. The route is chosen by sending 1 KiB
holding 0x1a and hashing it on the Dot, not by naming an operating system.

Slicing costs 23.4 s against 15.9 s for 256 MiB, and only where it is needed.
Measured and rejected: gzip cannot be told to avoid a byte; escaping needs a
decoder on the Dot, where base64 runs at 1.2 MB/s; a forwarded socket accepts
with nothing listening, and a reverse one is not honoured; exec-in returns
with 4.7 MB in flight and reports no status.

Each route is accepted on its own word and nothing else. gunzip touches a
file when it fails, dd's status ends the pipeline instead of being discarded,
and adb's status is read after the child is waited for, not before, where it
is always None. Silence is a failure: adb shell exits 0 whatever happened
remotely, so every one of those would otherwise have named the card.

gunzip reports a damaged gzip but not a missing one. toybox 0.7.6 exits 1 on
a stream cut at 0x1a and on a corrupted body, so the marker appears for both.
On input with no gzip magic it exits 0 and behaves like cat, passing the bytes
to dd to write. So each slice is checked for the gzip magic on the host before
the cache is used, where it also catches a build that did not finish.

Verified on bryce. A whole v1-bboe install from a Windows host took the
sliced route by its own probe and wrote /system in 108 s, md5 verified on the
card -- the first since 1c71a24. The stream route was then driven against the
same Dot: a good 16 MiB stream matched, a gzip cut at 0x1a was named, a dd
that could not write returned its own message, and a vanished Dot returned
adb's. Corrupt slices are refused before any of that runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bboe
bboe force-pushed the dot-firmware-report branch from 68a5789 to 55d09ec Compare October 9, 2026 02:12
@bboe
bboe merged commit 55d09ec into main Oct 9, 2026
5 checks passed
@bboe
bboe deleted the dot-firmware-report branch October 9, 2026 02:17
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