firebreak takes a Dot from stock Fire OS 6 to rooted Fire OS 5.5.5.4. It polls
the Dot every 2 seconds, does the stage for the state it finds, and stops when
the Dot is in the state its optional target names. The table gives the stages
for the default, amonet-biscuit-v1.1.0-bboe; Targets has the
others, and Back to stock the stock target.
| state | how it is recognised | what the run does |
|---|---|---|
| stock-booted | adb get-state says unauthorized, or Fire OS 6 without root |
prints the fastboot gesture, and the + gesture for a Dot already on amonet v2.0.0 |
| stock-fastboot | unlock_status is false |
amonet v2.0.0 fastbrick |
| stock-fireos5-fastboot | unlock_status is false, and lk_build_desc is f379dba-20170906_000423, Fire OS 5's, which amonet v1.1.0 keeps: Fire OS 5's own fastboot, locked, on a stock Dot or one on amonet v1.1.0 |
the same |
| emos | no adb or fastboot, and one serial port with USB ID 1949:2007 |
/init recovery at emOS's serial console |
| amonet-v2.0.0-booted | Fire OS 6, and id or su -c id answers uid 0 |
adb reboot recovery, into v2.0.0's TWRP; ends an amonet-biscuit-v2.0.0 run |
| amonet-v2.0.0-twrp | unlocked, lk_build_desc is not amonet v1.1.0's, mtp in sys.usb.config |
makes room for amonet v1.1.0 in the table, then reboots so the kernel rereads it |
| amonet-v2.0.0-twrp-v1.1.0-table | amonet-v2.0.0-twrp, and sgdisk --print lists boot_a_x |
writes amonet v1.1.0's chain and Fire OS 5.5.5.4, preloader last |
| amonet-v2.0.0-fastboot | unlocked, lk_build_desc is not amonet v1.1.0's |
downgrade to amonet v1.1.0 through the bootrom |
| amonet-v1.1.0-fastboot | unlock_status is true, and lk_build_desc is f379dba-20170906_000423 |
amonet v1.1.0 TEE and TWRP 3.7.0_9-bboe2, then recovery |
| amonet-v1.1.0-twrp | recovery, amonet v1.1.0's lk_build_desc, another ro.twrp.version, mtp in sys.usb.config |
writes TWRP 3.7.0_9-bboe2 to recovery, reboots into it |
| amonet-v1.1.0-bboe-twrp | recovery, amonet v1.1.0's lk_build_desc, ro.twrp.version 3.7.0_9-bboe2, mtp in sys.usb.config |
Fire OS 5.5.5.4, boot and /system patches, Magisk 17.3 |
| amonet-v1.1.0-bboe-booted, amonet-v1.1.0-booted, rooted | sys.boot_completed is 1 and su -c id answers uid 0; recovery hashes as TWRP 3.7.0_9-bboe2, as v1.1.0's TWRP 3.2.3, or as neither |
the target's state: hides the updater and checks it; another: writes the target's TWRP to recovery |
- A probe can land part way through a boot. TWRP answers adb before it sets
ro.twrp.version, and while USB drops for MTP,adb shellanswers with adb's error text. Either would read as amonet v2.0.0's and start a downgrade. So anlk_build_descnot shaped likef379dba-20170906_000423, or a version not starting with a digit, reads asstarting. The downgrade checks the shape too. - An empty
lk_build_descwould read as amonet v2.0.0's and repeat the downgrade,boot0erase included. So an empty or timed-out fastboot read, and any poll that times out, reads asstartingtoo. - A rooted Fire OS 6 Dot is on amonet v2.0.0, because Fire OS 6 has no root
without the unlock. Its root is Magisk's
su, or anadbdthat runs as root and nosuat all, which the XDA thread'sboot-root.zipleaves. So the probe asks both. Before this state, the first read as rooted and the second as stock-booted, which asked for a gestureadbcan replace. - A Dot unlocked with amonet v2.0.0 that boots Fire OS 6 without root also
answers
unauthorized, so it reads as stock-booted. amonet v2.0.0 removes stock fastboot, and the action-button gesture on an amonet v2.0.0 Dot was never tried. So the message also names + at power-on, which starts v2.0.0's TWRP. - On a stock Dot with LK
63cb91b, 2026-10-12, the gesture on one USB port of a Mac laptop left a solid blue ring, and the Dot showed nothing on USB. On another port the same gesture gave a green ring and fastboot. The same day, from stock 8149, the gesture on the first port went green, so the cause is not known. The gesture's message says to try again or try another port when the ring stays solid blue. suanswers about 27 seconds into Fire OS 5's first boot, whiledumpsys packagestill answersCan't find service: package. So rooted also needssys.boot_completedto be 1.- Each state is acted on at most once per run, so a stale read cannot repeat a stage. The downgrade ends with amonet-v1.1.0-fastboot's stage, and counts as both.
- That stage needs amonet's fastboot, and the bootrom step does not always
leave the Dot in it. On a resumed run reported from Windows the step wrote
v1.1.0 and the Dot came back in v1's own locked fastboot: a solid green
ring,
unlock_statusfalse, andrestricted on locked hwfrom the firstfastboot flash, which is all a locked LK answers. So the stage readsunlock_statusfirst and returns where it isfalse, and a stage that returns that way counts none of the states it would otherwise pass. That leaves stock-fireos5-fastboot's own stage to unlock the Dot again, where the once-per-run rule would otherwise have the run wait out its 10 minutes on a state already counted. The chain then goes in from v2.0.0's TWRP, which is the route a rerun took to rooted Fire OS 5 in 7 min 45 s. - Why the Dot came back locked is not known. Its table already held
boot_a_x, so the surgery had finished, and the preloader and LK were v1.1.0's. What that leaves is the microloader in amonet's ownboot_aandboot_b, or the boot control block. - The script waits without limit for a Dot and for the fastboot gesture. Any other state unchanged for 10 minutes stops it, and the next run redoes that stage.
- Rooting formats userdata. A registered Dot loses its Wi-Fi and its Alexa registration, and finishes in setup mode.
- After boot0 is erased the Dot shows up only as the bootrom's serial
port, which no probe sees. So the script creates
boot0-erasedin its cache before the erase. It deletes the file once amonet logsReboot to unlocked fastboot, when boot0 is written, and whenever it sees the Dot booted or in fastboot, which only a preloader in boot0 reaches. TWRP does not count: it keeps running from RAM after the restore to stock clears boot0. - A run that finds the file and no Dot runs the bootrom step again, without
the erase. A run stopped inside amonet's payload leaves the Dot there, and
the payload never restarts. So the resume first sends every MediaTek port
the payload's reboot command,
0xf00dd00dthen0x3000; the Dot came back as its bootrom within 5 seconds. Sent to a live bootrom on a Dot, it did no harm: the handshake that followed went through. - The run then waits up to those 5 seconds for each port it poked to go, so it does not open a payload's port while the payload reboots.
- On macOS, a run stopped 12 seconds into amonet's
tzwrite resumed this way: amonet found the bootrom 4 seconds after the run started, and the root finished. - From Linux, with boot0 erased, the resume went on after 30 seconds, twice,
with no one touching the Dot. From macOS the bootrom gives one window per
power-on: after
adb rebootfrom TWRP it appeared 4 s later, stayed 40 s, and then showed nothing on USB for the 2.5 minutes watched. Only a replug opened another window. - So a resume started inside that window needs no replug: a Dot was replugged, and a resume started 8 s after its bootrom appeared took that port. The reset command had reached the bootrom first, and the handshake still went through. A resume started after the window closes asks for the replug.
- A run stopped while amonet sends its payload, before the payload runs,
leaves the bootrom inside its write command, waiting for the rest of the
payload's words. The reset command reaches it as data. amonet's handshake
sends
0xa0and waits for0x5fwith no limit, and the bootrom echoes a word only after 4 bytes, so 3 reads in 4 wait out amonet's 5-second timeout. Stopped 0.3 s into a 0.8 s send, about 12 KB were left: 12 hours of handshake. The run had disabled the watchdog, so only a power cycle clears it. - So the handshake gives up after 10 seconds without
0x5f, where a live bootrom answers in about 2 ms. The run then asks for a replug, waits for the port to go and come back, and tries again. Under--shortit stops instead: a replug without the short brings up the preloader, and the Enter that ends amonet's short prompt has already been sent, so a second short would get no countdown. Before this, the replug failed the run withDevice not configured, and a rerun inside the new window went on through the root. With the limit, on a Dot stopped the same way, the resume asked for the replug 10 s after it took the port, and the same run finished the root to amonet-biscuit-v1.1.0-bboe in 9 min 6 s. - The replug gets its own 10 minutes for the port to return, and the step
stops 30 minutes after it starts. Without that cap, a bootrom that answers
0x5fand nothing more is reopened every 10 seconds for good: a probe run reached 51 handshakes. On the erase route nothing else ends the run.
- Before a run that installs a target, firebreak names the device and refuses
a target made for another one, before it downloads anything.
reportprints the same, and does not refuse.write-testdoes not check: it writes onlycache, by name, on any device. - It counts every USB device from MediaTek (
0e8d) and Lab126 (1949), and from Google and HTC only the IDs an Echo uses: TWRP's18d1:d001and18d1:4ee2, and fastboot's0bb4:0c01. A phone in Android's genericmtp,adb,18d1:4ee2, still counts. - With
ANDROID_SERIALset, only the USB device with that serial counts. Without it, two Echo devices that name two different devices name none. - In fastboot it asks
getvar product, which needs no authorization:BISCUITorRADAR. - Elsewhere it reads the USB product string,
iProduct, which the host has even when adb answersunauthorized. Fire OS sets it toro.product.model, and TWRP to its own model. biscuit isAEOBCin Fire OS 5 and 6,Echo Dot (2nd Gen)in v2.0.0's TWRP and bboe2, andEcho Dot 2nd Genin v1.1.0's TWRP 3.2.3. radar isAEORDandEcho (2nd Gen). The vendor and product IDs are the same on both, so they cannot tell them apart. - macOS reads
ioreg -a -l -p IOUSB, Linux/sys/bus/usb/devices, and WindowsDEVPKEY_Device_BusReportedDeviceDescthrough PowerShell, which gives no manufacturer. Each readAEOBCfrom a Dot on Fire OS 6, the macOS one with adbunauthorized, whereadb devices -lgives no model. The read took about 0.05 s on macOS and 0.8 s on Windows. radar's strings are read from its images, not from a radar. - A device that matches nothing, or no device, is let through: the checks before each write still guard it. The bootrom and the preloader name no product, so a Dot that shows no light is left to the bootrom step's own check.
- Two devices can have a target of the same name:
stock, and the default when no target is named. Then the device found chooses which one runs. Until one is found, the run waits, as it waits for a Dot, and says once that an Echo that shows no light never names one. With--short, or withboot0-erasedin the cache, the Echo is in its bootrom, so the run stops at once and asks for--device. It never guesses: each unlock writes a preloader made for its own device, and nothing here has run one device's preloader on another. A target name that only one device has, or one--devicenames, is checked as above. - In fastboot, the report also prints
antirback_pl_version,antirback_tee_versionandantirback_lk_version. - The report shows only the first 4 characters of a serial number. Amazon's
start with a prefix that may name the device type:
G090on every biscuit seen here.
- EchoMuse's emOS replaces Fire OS's boot image with its own init. It runs no
adbd, so adb and fastboot see nothing. Its init offers a root shell on a
USB serial port instead:
1949:2007, namedEchoMuseandemOS, with the Dot's serial number. Measured on a Dot with emOS 0.10 on amonet v2.0.0. - That shell's
/init recoveryreboots into the bootloader's TWRP, and the port goes away within a second. From v2.0.0's TWRP, 20 s later, the root goes on as from amonet-v2.0.0-twrp.stocktakes the same path: from emOS 0.10 on amonet v1.1.0 with TWRP 3.7.0_9-bboe2 it restored 8146 in 3 min 3 s. - Finding the port by USB ID needs pyserial, so the probe runs a child that imports it. It runs only when adb lists no Dot.
- A port that will not open, as on Linux without the udev
ttyline, stops the run with the rules and the commands that add them. - A program that has the port open keeps firebreak out. On macOS, with
EchoMuse's wizard tab still open in Chrome after it installed emOS 0.10,
2026-10-12, pyserial's open failed with errno 16,
EBUSY. The run had printed Linux's udev commands, which do not apply. SoEBUSYreads as busy. The udev commands are Linux's only, so any failed open off Linux reads as busy too. Windows was not measured. The message names the wizard's tab. - The child opens the port and sends a newline. A prompt ending in
#gets/init recovery. One ending inpassword:stops the run: EchoMuse's dashboard sets that password, and the user types the command or clears it. Nothing that matches in 5 seconds stops the run too. - pyserial opens a port raw. A port left to echo, as a terminal opens one, feeds the shell its own output and every command returns 127.
- A Dot whose preloader runs but never starts LK shows no light and offers
no fastboot: only
0e8d:2000, the preloader, for about a second every 30 seconds. Its boot0 is intact, so only the eMMC test-point short reaches the bootrom.--shortruns the bootrom step for it, with no erase. - Neither the gesture nor
FACTFACTsent to the preloader reached fastboot. mtkclient's four ways to crash a preloader into the bootrom all failed on Amazon's: each came back as the preloader. Its register writes echoed but took no effect. - The bootrom step takes only a port it can identify as the bootrom,
0e8d:0003, vendor and product both, and skips a stock preloader's, which--shortreports as a missed short. amonet would otherwise take whichever port appears first. - Before it resolves its plan, the step reads the first 512 KiB of
lk_aandlk_bover the payload, so the wrong device hears that first. Each LK names its fastboot product as\0product\0NAME\0:BISCUITin every biscuit LK, stock Fire OS 5.5.0.3 to 8149 and amonet v1.1.0's and v2.0.0's, andRADARin every radar LK, each by 0x4c000. The step refuses an LK that names another product, and refuses when neither namesBISCUIT. One LK is enough, because the plan writeslk_a, thenlk_b, so a run cut short leaves the other whole. It looks only on a table whose CRCs pass; a torn table goes on to the plan, which refuses it. - Nothing else in this step can tell a biscuit from an Echo (2nd Generation):
its table has biscuit's 16 partitions and the same bootrom answers. Without
the check,
--shorton a radar that shows no light would write biscuit's amonet v1.1.0 chain, preloader included. This is read from the code and the LK images, not seen on a radar. - It reads ports from pyserial's USB list and opens only the bootrom's, where
amonet opened every port to see it. On Linux the preloader's
/dev/ttyACM*isdialout's, and the udev rule covers only0e8d:0003, so amonet's way would never see a missed short. - It remembers each port by name and USB ID, and checks every port on each
poll. The bootrom usually reuses the preloader's name: on Linux both are
/dev/ttyACM0, and macOS names a port by its USB location. A port whose ID cannot be read yet, or that will not open yet, is looked at again on the next poll. --shortapplies only to a Dot first seen with nothing on USB. Any other state turns it off, so a later reboot cannot start the short flow. That includesstarting, which covers a probe that timed out as well as a Dot half started; after a timeout the run asks for fastboot, and a rerun with--shortgoes on.- amonet takes any port that appears after it lists the ports at start. So
--shortasks for the short only once amonet logs that it is waiting, and sends no reset first: a Dot plugged in earlier would be in that list and never count as new. - The short must be off before the payload starts the eMMC, its first act. Nothing before that touches the eMMC, and the handshake disables the watchdog. So the run holds amonet at its "Remove the short" prompt, counts down 5 seconds, then lets it go on.
- amonet's first write clears boot0's header. Under
--shortits bootrom step createsboot0-erasedjust before each write, after amonet's first boot0 check, as the erase path creates it before the erase. A run that stops before that leaves boot0 intact and no file. A run that fails or is stopped after it is resumed by a plain rerun. - The bootrom step logs the error for a port it still cannot open after 1
second, once, and the run shows it at once. udev can set a new port's mode
a moment after it appears, so a first failure alone means nothing. A
missing udev rule or ModemManager would otherwise hold a
--shortrun silent for 10 minutes. - The payload starts the eMMC once. With the short still on, either it never
comes up and amonet times out waiting for it, or the bootrom step's first
read, the partition table's
55 AA, fails. Both stop the run before amonet writes anything, and a new short starts over.
- The simulator runs firebreak against a simulated Dot, and records every command for a diff.
--verboseprints eachadbandfastbootcommand and its exit status with the time, and each state the run sees. The 2-second polls are not printed.- In verbose mode, or when the output is not a terminal, a stage prints a line
when it starts and when it ends, with no running count. In verbose mode
both lines start with the time. Output that is not a terminal also gets
no download meter. Either would fill a log with
\rframes. - A running stage shows a Braille spinner where its mark will go, and a
finished stage prints ✅. Where the output's encoding cannot represent them,
the spinner is
|/-\and the mark isdone. On Windows that is output redirected to a file: since Python 3.6 a console reports UTF-8 whatever its code page. - A finished line is at most 79 columns, so it does not wrap on an 80-column terminal. That caps a stage label at 36 characters.
- Steps are numbered from 1 in each run, against the total that
Targets says how the run counts. The restore stage is 16 of
them, and 17 from a rooted Dot, with the reboot into recovery. A restore
write whose target already reads back the right md5 ends
skip, in place of its seconds. - A plan carried out from TWRP prints one line per stage, not one per write. A
root from stock printed 22 lines, 12 of them writes that took under a
second. The boot0 clear, the table and every write but TWRP and the
preloader share one line, such as
write amonet v1.1.0-bboe. TWRP and the preloader keep their own lines, the preloader because a stop after it cannot be undone. Other writes in a row that share a label share a line, such as the table's primary and backup. A shared line endsskiponly when every write in it was skipped. A root from stock now prints 11 lines.--verbosestill prints every command. The bootrom step still prints one line per write. - Each stage's estimate is an upper bound: the slowest time measured for it on any host, plus 5%, rounded up to the next 15 seconds, or the next 5 seconds under a minute, over roots on macOS, Linux and Windows 11. An estimate is only ever raised. It is in seconds, as the time beside it is. On 2026-10-12 Fire OS 5's first boot took 319 s on Linux against 240 s, and its /system 108 s on Windows 11 against 100 s. A Fire OS 6 slot took 119 s on Linux, 1 s under its 120 s. A Mac took 272 s, 41 s and 85 s. One case is left out: a first run on a computer that finds the Dot already in TWRP waits up to 35 s more for the system image.
- The estimates and the rings are from TWRP 3.2.3. One root on macOS with TWRP 3.7.0_9-bboe1 came in under every estimate. Its replace step took 37 s on a Dot.
The ring during firebreak stock 6302, from a rooted Dot not set up:
| ring | when |
|---|---|
| purple | rooted Fire OS 5, setup timed out (anim_OOBE_start_error) |
| off, about 12 s | adb reboot recovery |
| deep blue, about 10 s, a brighter segment turning in its last 3 | v1.1.0's TWRP starting, before adb answers |
| a blink off, then a cyan arc turning, about 3 s | TWRP up: adb wait-for-recovery returns |
| cyan, whole ring, steady | through the restore's steps |
| off, about 8 s | the reboot into stock |
| deep blue, about 50 s | stock Fire OS 6 booting |
| cyan and blue, turning | Fire OS 6 starting Alexa |
| orange | setup mode, about 90 s after the reboot |
The ring during a root from stock 8138, in daylight, 14 min 49 s in all. That root went through the bootrom; a root from stock now does not:
| ring | when |
|---|---|
| green | stock fastboot, from the button held at power-on |
| off 1 s, then yellow, shrinking to an arc over about 10 s | the fastbrick flashed |
| red-orange, about 7 s, then green, about 5 s, then off, about 5 s | the exploit, before TWRP |
| blue, about 9 s | v2.0.0's TWRP starting |
| a white flash, a blink off, a cyan arc | v2.0.0's TWRP up; the script reboots it at once |
| off, about 7 s | adb reboot bootloader |
| a white flash, then every color, turning, about 5 s | the exploit's unlocked fastboot |
| off, about 5 min | the bootrom step: boot0 erased, then amonet v1.1.0 |
| blue, about 10 s, then every color, turning, about 3 s | v1.1.0's fastboot, while tee2 and recovery are flashed |
| off, about 5 s | fastboot oem reboot-recovery |
| deep blue, about 13 s | v1.1.0's TWRP starting |
| blinks off, a cyan arc turning, about 5 s | adb answers, MTP not yet on |
| cyan, whole ring, steady | from MTP on, through the Fire OS push and install |
| off, green, off, green, off, about 5 s | the Fire OS install finishing |
| cyan, whole ring, steady | the boot image, /system and Magisk |
| off, about 6 s | the first reboot into Fire OS 5 |
| deep blue, about 35 s | Fire OS 5 booting |
| cyan and blue, turning, about 3 min | the first boot, until the setup app starts |
| orange | setup mode, about 1.5 minutes before the script finishes |
A run from stock 5041, at night, showed the same, except that the fastbrick ring read white.
- The ring turns orange before the boot is done. On a Dot, 2026-10-04, the
setup app started and the ring turned orange 30 s before
BOOT_COMPLETEDwent out. The firstsucame 13 s after that. The last step ended about a minute later, 322 s afteradb reboot. The end is an estimate from the Dot's uptime, because the step logged no time. The minute held Magisk's firstsu, the 2-second probes, andhide_updater'sdumpsys package.
- An issue report used to carry the failing step and nothing about the Dot.
reportcollects firebreak's version, the host, the state, the build, the eMMC, the partition table with each name's node, and the kernel's own mmc lines. The version matters becauseuvx firebreak@latestcan run a newer firebreak than the run that failed. - 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_ais 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.
cidandserialare left unread, and GUIDs and the Dot's serial are masked: the run setsANDROID_SERIALto the Dot it picks, and the mask reads it. - The kernel log's mmc lines are its last 60, and the card's first line is read on its own, so a log that still reaches the boot says so even after 60 later lines.
- Each answer the report parses into claims comes behind a marker of its own,
because adb cuts the tail:
sgdisk's table,/proc/partitions, theby-namelisting and the kernel log's two reads. One that does not arrive whole is printed as it came, or not matched at all, rather than parsed into claims about which names point nowhere. The other answers are printed as they came. A failed system write discards the image's first slice and keeps the record of its size, which is the number a report is run for. - The report reads the Dot's state without the root run's probe, which stops on a partition table it cannot read: a damaged table is the case a report is run for. Any recovery is left alone, and one that is not TWRP stops the report rather than being restarted. A Dot that runs Fire OS without root is not restarted either, because its recovery is Amazon's. An unlocked fastboot is restarted into its recovery, which holds Amazon's until a root run writes TWRP; when no adb recovery answers within 3 minutes, the report says that is the likely reason. The Dot is left in its recovery afterwards, and the report ends by saying so.
write-testseparates the card, the cable and the host, which a failed install's md5 cannot. It 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 owncache, for the card with no USB in it. The eMMC leg writes the file the USB leg verified, so both carry the same bytes, and it is skipped when the USB leg proved nothing.- It writes over
cache, which holds nothing the Dot needs, so a part-writtensystem_astays as evidence. The twoamonet-biscuit-v1.1.0targets do not formatcache, so the filesystem this test leaves is the one the Dot boots with. 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. cacheis formatted first, and small enough that its own blocks end well before the pattern starts, so the writes stay raw, like the/systemwrite they stand in for, nothing is mounted while they land, and a run stopped part way leaves a Dot that still boots. The layout is pinned rather than inherited, because the boundary has to be an input:-J size=4 -N 8192with-O ^sparse_super,^resize_inode -E packed_meta_blocks=1puts the primary superblock, the group descriptors, the bitmaps, the inode tables and the journal inside the first 6.11 MiB of a 784 MiB cache, measured, against a 16 MiB margin.^sparse_superalso puts a backup superblock in every block group, at 128, 256 and up to 768 MiB (mke2fs 1.47), and the pattern overwrites those. The filesystem still mounts on its primary, and the last format writes all of it again: a filesystem over all ofcachebut its last 1 MiB.- A write that does not answer in 30 minutes leaves
cacheas it is.ddmay still be writing on the Dot, and a format under it could leave a filesystem that reads as fresh and is not. The one already there ends before the pattern starts, so the Dot still boots, and a rerun formats it first. e2fsckwill not catch it if that boundary is wrong. With the default layout the journal is one extent at 12.33-28.33 MiB, and after overwriting from 16 MiBe2fsck -fnpassed all five passes on a filesystem missing 12.3 MiB of its 16 MiB journal: the journal's superblock survived, and its body is not checked on a clean filesystem. A mount, a write, an unmount and a seconde2fsckare what prove a journal works.- A test that cannot run must not read as a card that failed. So the digest
comes back behind a marker, the writing
dd's errors are kept, and a refusedumount, a target that is not a block device and a format that did not happen each say so: the read-back names the node it wrote, and a file in TWRP's RAM/devwould match itself. The reverse holds too: addthat cannot write the card ends with a marker of its own, which blames the card. Each skip says which of its four reasons it was. The whole command is one line with no quotes, or Windows loses it. - Measured 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.
- A Dot whose
cacheholds no filesystem roots fine and then cannot boot.fs_mgr_mount_allfails in init'son fs, sopost-fs-dataandclass_start mainnever run:zygote,netdandinstalldhave noinit.svc.*property at all, while everyclass coreservice is up. So the Dot answers adb as root,suworks,/systemis mounted and read correctly -- andsys.boot_completednever arrives. It reads like a bad/systemor a slow first boot. The reason is in dmesg, not logcat:e2fsck: Bad magic number in super-block, thenfs_mgr: Failed to mount an un-encryptable or wiped partition, thenfs_mgr_mount_all returned unexpected error 255. amonet-biscuit-v2.0.0wipes cache andstockformats it. The twoamonet-biscuit-v1.1.0targets format userdata only, so a cache damaged before a run survives it.write-test --shortruns the test through the bootrom instead, for a Dot that shows no light and so has no recovery. It reads the last 64 blocks ofcachethree times, then writes 1, 34 and 64 blocks there and reads each back, then writes the first read back. 34 is the partition table, the first bootrom write of a--shortrun; 64 is the most one command carries.- v2.0.0's payload answers a block write only when the eMMC took it, and the
same silence follows when the bytes never all arrive. So after a write with
no answer the test sends a 4-byte memory read,
0x5000. A payload that answers took the whole write and refused it, which blames the card; one that answers with the write's own acknowledgement was slow; one that stays silent is still waiting for bytes, or for the card. - After a write the card refused or answered late, the test puts
cache's blocks back. After one the payload lost track of, it cannot, and says to unplug the Dot: the pattern stays at the end ofcache, which holds nothing the Dot needs. - A late answer is followed by the ping's own 4 bytes. When they do not come, the ping filled the end of a write that lost bytes on USB, so that blames USB, not the card.
- It reads the table first and skips a table that is not intact, so a stale
cacheentry cannot point the writes at another partition. - Measured on a Dot from Windows 11, with boot0's header cleared so no short
was needed: three reads agreed, writes of 1, 34 and 64 blocks read back
unchanged, and
cacheread back as it was.
- Python 3.9 or later, because stock macOS's
/usr/bin/python3is 3.9.6. CI runs its--helpon that interpreter, andtests/test_python_floor.pychecks that the package uses no module, attribute or import 3.9.6 lacks. - The standard library only, so the script runs on stock macOS, Linux and Windows. adb and fastboot are the only external tools.
- A Python from the Microsoft Store writes under
%LOCALAPPDATA%into its ownPackages\PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0\LocalCachecopy, and the processes it starts do not see that copy. Measured on Windows 11 with Store Python 3.13.14, alone and under a uv environment: Python saw a file it wrote, and acmdit started in that folder did not, by a relative or an absolute path. The fastbrick'sfastbootloads its image from the cache, which fits one user's run on that Python: each of its 10 flashes exited 1 at once. The run stops when its Python, or the one its environment was made from, is underWindowsApps. It namesuvx --managed-python, or for a pyz, a python.orgpython.exeby its full path oruv run --managed-python --no-project python. It does not namepy: the launcher lists the Store Python too, andpy -3.13ran it on that host. - On a terminal,
ERROR:lines are red and warnings yellow.NO_COLOR,TERM=dumb, a pipe or a file turns that off. On Windows only Windows Terminal (WT_SESSION) gets color; the old console prints the escape codes as text. A blank line separates one kind of message from another. - adb must be 1.0.36 (platform-tools r24) or newer: 1.0.32 answers
wait-for-recoverywithunknown host serviceand passesshell -nto the device. Ubuntu 22.04 and 24.04 ship 1.0.41. fastboot must accept-S, as every release back to r19 does. Every run checksadb versionandfastboot --helpfor-S, the stock target included, because a route to stock can pass through fastboot. Old releases, run with no device, set these floors. - Downloads go to
$XDG_CACHE_HOME/firebreak(default~/.cache/firebreak), or%LOCALAPPDATA%\firebreakon Windows. The script moves what the oldoverdub-firmware,overdub-rootandoverdub-stockdirectories hold into it before it reads the cache, so an interrupted downgrade's marker survives. A name already in the new directory stays in the old. Each is checked against a pinned SHA-256 before use and on every run. The amonet trees unpacked from the two zips are not, and neither is the system image built from Fire OS's. - The system image's directory is named after the zip's hash, so a new zip
builds a new image. Its
md5file is written last and synced with the slices before the directory is renamed into place. It names the image's md5, its 4 KiB blocks and its slice count, and the script trusts a directory only when all three are there and every slice starts with the gzip magic. It rebuilds otherwise, so a directory from a firebreak that wrote onesystem.img.gzis rebuilt, and one from overdub'sdot_firmware.py, which writes the same slices, is used as it is. If the md5 read back still fails after the last try, the script deletes the first slice, which works even when another process holds the rest open. - Each stage names the downloads it uses. A run fetches those of the stages
on the route from the Dot's state to the target's goal, and no others. A
stockrun that finds TWRP fetches only the build. A run toamonet-biscuit-v2.0.0from v1.1.0's TWRP does not fetch Fire OS 5. A rooted Fire OS 5 Dot needs TWRP 3.7.0_9-bboe2 only when the target isamonet-biscuit-v1.1.0-bboe. tests/test_needs.pychecks each stage's downloads against its code, for every target. It reads the source of the stage and of each firebreak function it calls, and collects the downloads passed tofetchandunpack. It runs nothing. It decides anifonly when the condition has no call, such as a check on the target's goal; otherwise it takes both branches. A stage must name every download its code always makes, and nothing else. The one exception is v1.1.0's zip in the LK check, below, which a stage may name.- amonet v1.1.0's zip is a need only where the Dot still runs v1.1.0's LK.
On v2.0.0's TWRP the LK check matches v2.0.0's
lk.binfirst, so a run toamonet-biscuit-v2.0.0from stock fastboot does not fetch it. Werelk_ato match neither, the check would fetch it itself. - The run downloads nothing while it waits for a Dot it has not seen. That Dot can be another device, or can need nothing. A Dot that waits where the next state is known starts the downloads in a background thread: an unmodified Dot, booted, comes back in its own fastboot after the gesture, and amonet v2.0.0's fastboot comes back as its TWRP. So the downloads overlap the gesture.
boot-root.zipcomes through the browser, so the background thread never fetches it. The main thread fetches it before the other downloads, so the one step that needs the user comes first and does not land in the gesture's instructions.- The run names the device before it starts that thread, and again before the
first stage. A device plugged in after the run started is then refused
before the route's downloads. The probe of a rooted Fire OS 5 Dot still
fetches amonet v1.1.0 first: it compares
recoverywith v1.1.0's TWRP to name the state. For TWRP 3.7.0_9-bboe2 it compares against the pinned size and SHA-256, so it downloads nothing. - The main thread waits for that thread's locks half a second at a time. On Windows, Ctrl-C does not interrupt a blocking lock wait, so one long wait would ignore it until a 397 MB download ended.
- The downloads are at most about 476 MB. With the unpacked trees and the system image, the cache holds about 1.3 GB.
- Before each stage, the script waits for every download on the route and checks it. A bad download then stops the run with the Dot unchanged. A route can end before the goal: emOS reboots into a recovery the run names only when it sees it. The next stage then downloads the rest.
- The downloads run one at a time, in the route's order, so the first stage's come first: amonet v2.0.0's zip for the fastbrick. Fire OS is 397 MB of the 476, and one download filled the line at about 40 MB/s, so parallel downloads would only delay that zip.
- Nothing deletes a download, so a later run downloads nothing. A rooted run prints the cache's path and size and says it is safe to delete.
- The emOS probe and the bootrom step need pyserial, which a wheel or source
install declares as a dependency, pinned to the same version the download
holds. A test reads the pin out of
pyproject.tomland checks it names that wheel, so the two cannot drift. - A pyz carries no pyserial, so the run fetches the pinned pure wheel from
PyPI when the host has none and imports pyserial from that zip: on this
process's
sys.pathfor the bootrom, on the emOS child'sPYTHONPATH. A host with pyserial fetches nothing. - A run asks PyPI once whether a newer firebreak exists, through
update_checker, after it refuses root and before it starts adb, so a root
run cannot leave a root-owned
~/.cache. It prints a notice only when one does, with the way to upgrade: the latest release'sfirebreak.pyzfor a pyz, anduvx firebreak@latestfor any other run, as the README gives both. The command or link goes on a line of its own, outside the 79-column wrap, so a copied line holds all of it. - The check keeps each answer for an hour.
--no-update-checkskips it. A child never checks. - Each request has a 1 s timeout: with PyPI unreachable through a dead proxy, a run that stopped at the adb check took 1.2 s. The timeout does not cover DNS: with a resolver that never answers, the same run took 8.8 s. A failed check is also kept for an hour. The check catches every exception, so it cannot stop a run.
- The pyz bundles update_checker: its 2 modules and its
dist-info, which holds the BSD-2-Clause notice. The build installs the pinned dependencies frompyproject.tomlintobuild/bundleand zipsfirebreakandupdate_checker*from there. The bundled version is the pin, and a test checks the pyz holds it and no pyserial. - The pyz has no
dist-infofor firebreak, so firebreak's version is__version__infirebreak/__init__.py. hatchling reads the package's version from there too, and a test checks the installed one equals it. - The one child left is emOS's,
python -m firebreak _child emos, with the pyz orsite-packageson itsPYTHONPATH. A path to re-run would not do: the wheel's Windows launcher is an.exe. -mputs a child's working directory first on its import path, so a child runs in the cache or amonet'smodules, never where the run started. Afirebreakdirectory there would replace the package, and aserial.pythe pinned wheel. The child drops that entry before it does anything else. It checks the entry first: underPYTHONSAFEPATHPython adds no such directory.adb get-statereportsunauthorizedon stderr, so the script reads both streams.- Without
ANDROID_SERIAL, the run uses the one Dot on USB inadb devices -l, and ignore network adb: a rooted Dot with tcp/5555 open would makeadb get-stateanswermore than one device/emulator. The run picks again on every poll, because each reboot drops the Dot off USB. With two Dots on USB the run stops and asks forANDROID_SERIAL, which fastboot follows too. - An
ANDROID_SERIALof the formhost:portis refused: TWRP starts no Wi-Fi, and fastboot and the bootrom are USB-only. - Windows 11's adb prints no
usb:field, so there a line counts as USB unless its serial ishost:portoremulator-*.
- Binary from the host goes by
adb push, binary from the Dot byadb exec-out, andadb shellcarries only text whose shape is checked. The one exception is the/systemwrite, which puts the image onadb shell's stdin where a probe has shown that this host's adb carries 1 KiB holding 0x1a unchanged, and pushes it in pieces where it has not. - A Windows adb reads its own stdin in text mode, where 0x1a ends the file, so
adb shelltruncates a stream there. Every other byte survives: NUL, CR, LF and EOT all arrive. Fire OS 5's system image holds 1,421,626 of them and the first is at offset 238, so 238 of 385,745,930 bytes reach the Dot andgunzipwrites 12. - Nothing reports it. A pipeline carries its last command's status, so
gunzipfails,ddexits 0, and the only symptom is an md5 that does not match -- which reads as a failing eMMC. On Fire OS 6, which has nogunzipat all, the same stream also exited 0 having written nothing. So each such command ends in a word of its own,gunziptouches a file when it fails, anddd's own status ends the pipeline. The word is what the host accepts: silence is a failure, becauseadb shellcan exit 0 whatever happened remotely, and addthat cannot write the card would otherwise read the same as a stream that arrived. gunzipreports a damaged gzip but not a missing one. toybox 0.7.6 exits 1 on a stream cut at 0x1a (gzclose: Illegal seek) and on a corrupted body (gzread: incorrect data check), so the marker appears for both. On input with no gzip magic it exits 0 and behaves likecat: 17,000 bytes in, 17,000 straight out, whichddwould then write to the partition. So each slice is checked for the gzip magic on the host before the cache is used. A build that did not finish never reaches the cache: it is renamed into place only after itsmd5file is written.- A slice that arrived intact, by its md5 on the Dot, and still did not unpack can only be bad on the host. The sliced route deletes it and stops, so the next run rebuilds the image rather than failing the same way. The stream route cannot tell which slice failed, so it unpacks each on the host and deletes the ones that fail there too.
- Outbound is the same class.
adb shellon Windows expands LF to CRLF, so the partition table, its backup copy andmisc's boot control block come off the Dot byexec-out, and each read checks its own length. exec-inis not the way round it. It is binary-safe, but it returns before the Dot has finished -- 4.7 MB still in flight at 16 MiB, complete and identical 5 s later -- and reports no status at all:exec-in 'exit 7'answers 0. On amonet v2.0.0's TWRP a pipeline through it either fails at once or hangs with nothing started on the Dot.- Measured on one Dot from two hosts minutes apart, same recovery: Windows 11 with adb 36.0.1 against macOS with adb 37.0.0.
-
adb, fastboot and the bootrom's serial port need the Dot's USB devices open to the user. Ubuntu's
android-sdk-platform-tools-commonand android-udev-rules, which LineageOS points to, list other Lab126 products but not a booted Fire OS 5,1949:0110. Fire OS 6 is MediaTek's0e8d:201c, which Ubuntu's package marks as an adb device and a distribution without it leaves to root. Other guides for this Dot usesudo. These rules, in/etc/udev/rules.d/51-echo-dot.rules, cover every state, the serial port included:SUBSYSTEM=="usb", ATTR{idVendor}=="1949", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee2", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="d001", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="0c01", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", ATTR{idProduct}=="0003", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", ATTR{idProduct}=="201c", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="tty", ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="0003", MODE="0660", GROUP="plugdev", TAG+="uaccess" SUBSYSTEM=="tty", ATTRS{idVendor}=="1949", ATTRS{idProduct}=="2007", MODE="0660", GROUP="plugdev", TAG+="uaccess"Then
sudo udevadm trigger. A rooted Fire OS 5 is1949:0110(adb alone, set in its boot image; themtp,adbFire OS 5 otherwise runs is1949:0112), a rooted Fire OS 6 81460e8d:201c(adb alone, one interface), TWRP18d1:d001and18d1:4ee2, fastboot0bb4:0c01, the bootrom0e8d:0003, and emOS's serial console1949:2007. Theusbline for1949covers emOS's USB device but not its/dev/ttyACM*node, which isdialout's without thettyline. -
uaccesscovers a user at the machine; over SSH the user must be inplugdev. A new group reaches only new processes, and a running adb server keeps its old permissions: with the group added and the old server up,adb deviceslisted nothing. So afteradb kill-server,sg plugdev -cruns a script with the group, without a new login. -
The script refuses to run as root: the downloads would belong to root, and the rules make
sudoneedless. -
It checks at start that the process has
plugdev, even for a desktop user covered byuaccess, so the check is one question. A user not in the group gets the rules and the commands to add them, starting withgroupadd -f plugdevbecause Fedora and Arch have no such group. A user in/etc/groupwhose shell predates that getsadb kill-serverand thesgline. Thesgline repeats the command as run, interpreter, path and flags included, because./firebreak.pyzis wrong from another directory. -
Without the rules adb lists the Dot as
no permissions. When that lasts 5 seconds and nothing else listed is usable, the run stops and prints the same rules and commands. A shorter spell is udev still setting permissions. A phone the user cannot open, beside a Dot that answers, does not stop them.
- The fastbrick image is
fastbrick-20221007.imgwhenlk_build_descis63cb91b-20221007_072309or41fb3ce-20221007_151724, elsefastbrick.img. That one payload takes both bootloaders, where amonet v2.0.0's own map lists only the first. The second ships in 4310, 4313 and 4315, which the default image does not unlock. - A flash that returns means the exploit did not start, so it is retried, up to 10 times. A flash still running after 8 seconds means the exploit is running: the script stops fastboot and leaves the Dot to reach TWRP.
eMMC-ROorDevice mismatchmeans the payload refused, and the Dot is unchanged.
| target | ends in | stops at |
|---|---|---|
amonet-biscuit-v1.1.0-bboe, the default |
amonet v1.1.0, TWRP 3.7.0_9-bboe2, rooted Fire OS 5.5.5.4 | amonet-v1.1.0-bboe-booted |
amonet-biscuit-v1.1.0 |
the same, with v1.1.0's own TWRP 3.2.3 | amonet-v1.1.0-booted |
amonet-biscuit-v2.0.0 |
amonet v2.0.0's TWRP, Fire OS 6 8146 in both slots, boot-root.zip |
amonet-v2.0.0-booted |
stock [BUILD] |
Amazon's Fire OS 6 BUILD, 8149 when left out, the whole Dot erased |
stock-booted |
- Each poll picks the stage for the state found and the target, so a run goes from any state to any target, a rooted Dot included. Every state has at most one stage per target, so a table does what a search over the stages would.
- Each stage in the table says how many steps it prints and which state it
leaves the Dot in. Before each stage the run walks the table from the
current state to the target's and shows that total. A walk that meets a
state it cannot predict past, such as emOS, whose TWRP depends on the
chain under it, shows
?. A Dot that lands somewhere else is walked again from there, so the total changes only when the Dot surprised the run. Step numbers only go forward. amonet-biscuit-v1.1.0andamonet-biscuit-v1.1.0-bboewrite the same /system, boot image and Magisk. Onlyrecoverydiffers, and the write from v2.0.0's TWRP puts the target's own TWRP there. A rooted Dot moves between the two with that one write, from Fire OS throughsu.- Booted Fire OS 5 has no
md5sum. So the probe and the write readrecoveryback throughadb exec-outandsu, and hash it on the host. On a Dot, 13,953,024 bytes took 2.2 s. One read, at the larger image's length, answers for both TWRPs.
- In v2.0.0's TWRP it runs
twrp wipe cache,twrp wipe data, andtwrp installof the OTA twice, with a reboot into recovery between. Then it runstwrp installofboot-root.zipand reboots. - It pushes the OTA to
/sdcardbefore each install. A/sdcardon a/datathat did not mount is TWRP's RAM: the push and its read-back pass, and the reboot loses the file. - The XDA procedure flips the active slot with
bcbtoolbetween the two installs. The run does not: a TWRP install of an A/B OTA writes the inactive slot and makes it active, so the flip would make the second install write the same slot again. The run readsbcbtool get_activebefore and after each install, and stops unless it changed. boot-root.zippatches both slots. It is an XDA attachment, and XDA sends a script a JavaScript challenge in place of the file. So before it touches the Dot, the run opens it in the user's browser and waits up to 2 minutes for it in~/Downloads, matched by size and sha256de49cc88.... With no graphical browser, the run stops and says where to get the file.- Once the cache holds the file, the run deletes its copies from
~/Downloads. A new file with other bytes is deleted only when the browser recorded the attachment as its source. - Safari unpacks a zip into
~/Downloadsand moves the zip to the Trash. So on macOS the run also looks in~/.Trash. When it opened the browser and took the file from there, it also deletes a new directory that holds exactly the zip's files. - Fire OS 6 without root for a minute after the install means
boot-root.zipdid not take, and the run stops. A single such read during the first boot does not stop it. - From amonet v1.1.0 the run carries out amonet v2.0.0's plan from either amonet v1.1.0 TWRP, in place of its zip, the XDA route from amonet v1.1.0. A rooted Dot first goes to recovery. The plan resolves against the Dot's table before anything is written, so a table that does not fit is refused with nothing changed.
- boot0's header is cleared first, with
boot0-erased, as the zip copy did and the restore does. From there to the preloader, any stop lands in the bootrom, and the resume writes amonet v1.1.0 and goes on from its TWRP. A stock table under amonet v1.1.0's LK would not boot, so nothing is written before the clear. - Then the table goes back to stock, as the zip's
restore_gptdoes, by the TWRP route below. Onlyuserdataand the two appended partitions change, so every partition the plan writes keeps its node, and no reboot comes between. - Then the TEE, both LKs, kaeru and the TEE payload are written and read
back, and
miscis zeroed, as the zip does. v2.0.0's TWRP comes after them, so they share one line, and the preloader is written last. The plan's RPMB zero,FASTBOOT_PLEASEand reboots are left out, as for v1.1.0's chain from v2.0.0's TWRP; the run reboots into recovery itself. - The zip wrote the preloader first. A stop after that left a valid preloader in front of a mixed LK and TEE: no bootrom, and maybe no LK, so only the eMMC short reached it. The zip is no longer installed, and no patched copy of it is built.
- Cut on a Dot, with the patched zip this replaced: a reboot from the Dot
itself when the zip logged
Updating tz, after LK and before the preloader. The bootrom appeared 4 s later, the same run's resume took it with no one touching the Dot, wrote amonet v1.1.0, installed the zip again from bboe2, and went on to amonet v2.0.0:[1/8]to[13/13], 6 min 43 s in all. This TWRP has no/proc/sysrq-triggerand itsreboottakes no-f, so the cut was a plainreboot. The plan clears boot0 first and writes the preloader last, as that copy did. - A resume without an erase or
--shortthat sees the preloader,0e8d:2000, takes boot0 as intact: it stops the bootrom step, deletes the marker, and goes on polling. On a Dot rebooted from Fire OS 6 with the marker set by hand, the run said so 2 s later and ended at amonet-v2.0.0-booted, with nothing written. - An amonet v1.1.0 Dot started in fastboot reads as stock-fireos5-fastboot,
and the fastbrick leaves it in v2.0.0's TWRP on amonet v1.1.0's partition
table. The probe reads that as amonet-v2.0.0-twrp-v1.1.0-table, and for
amonet-biscuit-v2.0.0the run carries out the plan there first. The amonet v2.0.0 install itself stops ifboot_a_xis still there, or ifsgdiskprints nouserdata. - Fastboot cannot tell the two tables apart, and a stock Fire OS 5 Dot is the common case. So stock-fireos5-fastboot's stage plans for the stock table, as stock-fastboot's does. Where the probe then finds amonet v1.1.0's table, the step counter plans again from there.
- In v2.0.0's fastboot the run asks for the + button at power-on, which starts v2.0.0's TWRP, and waits for it without limit.
--shortgoes with any target. Its bootrom step writes amonet v1.1.0, and the run goes on from amonet v1.1.0's TWRP like any other.- Measured on a Dot, 2026-10-04, each with no one touching the Dot:
| from | to | took |
|---|---|---|
| amonet-v1.1.0-bboe-booted | amonet-biscuit-v1.1.0 | 9 s |
| amonet-v1.1.0-booted | amonet-biscuit-v1.1.0-bboe | 9 s |
| amonet-v1.1.0-bboe-booted | amonet-biscuit-v2.0.0 | 5 min 11 s, on 2026-10-09, by the plan: recovery 28 s, boot0 under 1 s, the table 1 s, the writes 5 s with TWRP 3 s of it, the preloader under 1 s, recovery 19 s, the wipes 4 s, the installs 96 s and 116 s, boot-root.zip 8 s, Fire OS 6's boot 24 s. The zip took 5 min 22 s, its install 19 s and its reboot 19 s |
| amonet-v2.0.0-booted | amonet-biscuit-v1.1.0 | 8 min 28 s through the bootrom, which this starting state no longer uses, kept to compare against: recovery 19 s, the downgrade 43 s, TWRP 27 s, the install 86 s, Fire OS 5's first boot 321 s, TWRP 3.2.3 7 s |
| amonet-v2.0.0-booted | amonet-biscuit-v1.1.0-bboe | 7 min 42 s, with no bootrom: recovery 19 s, the table 1 s, recovery 19 s, boot0 under 1 s, the chain 6 s, misc under 1 s, userdata 4 s, /system 79 s, the boot image 3 s, Magisk 1 s, the preloader under 1 s, Fire OS 5's first boot 321 s |
| stock-fastboot | amonet-biscuit-v1.1.0-bboe | 8 min 3 s, with no bootrom: the fastbrick, then the same route as from amonet-v2.0.0-booted |
| amonet-v1.1.0-bboe-booted | stock 8146 | 3 min 11 s: recovery 30 s, system_a 81 s, the rest under 7 s each, stock's first boot 32 s |
bcbtool get_activeprinted a bareaorb, and each OTA install changed it. The downgrade ran on the stock partition table the zip left.- Not run: the plan on amonet v1.1.0's partition table in v2.0.0's TWRP, and
--shortto amonet v2.0.0.
Both installs from an amonet TWRP are one route: amonet v2.0.0's plan from either amonet v1.1.0 TWRP, and amonet v1.1.0's from v2.0.0's TWRP.
- The plan resolves against the table on the disk, less the steps TWRP does
another way: the RPMB, forcing fastboot, the reboots, and boot0's header,
which the route clears itself. It writes zeros there, as the v2.0.0 install
and the stock restore did; amonet v1.1.0's plan writes
EMMC_BOOTand then zeros, and either leaves the bootrom no preloader. A table that does not fit is refused with nothing written. - When the plan's table moves a partition it writes, the table goes in alone
and the Dot restarts into recovery, because
BLKRRPARTisEBUSYwhile TWRP is up. The next run finds the table in place and writes the rest. Otherwise every partition the plan writes must be a block device of the size the table gives, or the Dot restarts into recovery with nothing written: a kernel holding another table would put a write by name in RAM. - Then boot0's header is cleared, with
boot0-erasedset, then the table, the writes but TWRP, TWRP, and the preloader last. amonet v2.0.0's writes end with zeroingmisc, as its zip does. amonet v1.1.0's route installs Fire OS 5.5.5.4 between TWRP and the preloader. - The table is the layout's own bytes, both copies, written by
ddand read back like the rest. The backup copy goes first, so a stop between the two leaves the old primary in front and the next run writes whichever copy differs. - The backup copy lies at block 7651295 of
mmcblk0; every other write goes to a partition's own node, near its start. bboe2's and v2.0.0's TWRPs carry addthat answersEINVALpast block 4194304 besidetoybox dd, which reaches it, and the writes taketoybox ddwhere it exists. TWRP 3.2.3 has no toybox, and itsdd, busybox's, reaches it.losetup -oreads 0 bytes there. So before anything is written, the route reads that block with theddit writes with, and refuses a table it cannot reach. - The layout also wipes
userdata's first blocks. This route leaves that out: amonet v1.1.0's step formatsuserdata, and amonet v2.0.0's installs wipe it. - Measured on a Dot at TWRP 3.7.0_9-bboe2:
toybox ddwrote the backup copy back over itself at block 7651295, 33 records out, andsgdisk --verifypassed.sgdisk --load-backupof a layout's two copies, onto a loop device the size of the disk, gave both back byte for byte, but it adds a tool the route does not need. Its exit status proves nothing: on a file over 2 GiB it read the size wrong, did not write the backup copy, and exited 0. - Measured on a Dot, 2026-10-10, each run from where the last ended:
amonet-v1.1.0-bboe-booted to
amonet-biscuit-v2.0.0in 4 min 54 s, thenamonet-biscuit-v1.1.0in 7 min 47 s, thenamonet-biscuit-v2.0.0from TWRP 3.2.3 in 5 min 17 s, thenamonet-biscuit-v1.1.0-bboein 7 min 48 s. Each table copy took under 1 s. On all three TWRPs the backup copy went in as 33 records and read back whole, and the Dot booted rooted at the end. The same four runs on another Dot from Windows, with/systemin slices, took 4 min 51 s, 8 min 17 s, 5 min 14 s and 8 min 16 s.
- The run writes v1.1.0's chain and Fire OS 5.5.5.4 from v2.0.0's TWRP. The
bootrom step stays for
--shortand for amonet v2.0.0's fastboot, where no TWRP is up. - The fastbrick appears to zero RPMB itself: its payload holds
Zeroing RPMB block 0...andRPMB block 0 cleared and verified. If so, a Dot that has never seen amonet v1.1.0 needs no bootrom either. This is read from the payload's strings, not measured. Every Dot here already had a zeroed RPMB, and the bootrom step it replaces verified the zero where this route assumes it. RPMB is not readable from TWRP, so nothing checks it at run time. A wrong assumption here is the one failure on this page that costs the eMMC short. - The table goes first, then a reboot, then the rest by the TWRP route above. Clearing boot0 before that reboot would land there.
- The chain is amonet v1.1.0's plugin plan (
docs/plugins.md), less the steps this route does another way: the RPMB, forcing fastboot, and the reboots. A region that already holds its bytes is skipped, so a rerun resumes where the last one stopped. Measured from stock 8146, on the route this one replaced: a run stopped during the first LK write, and the rerun skipped the four boot writes andtee1, wrote the rest, and the Dot booted rooted. It skipped the BCB too, which the stock install had already written. That route skipped boot0's header as well; this one clears it again on every run that writes past the table. ERASEDis set only after the plan resolves and every node checks out, so those stops leave the next run on its usual path. A stop between that and the boot0 clear leaves it set with boot0 intact, which costs the next run a bootrom handshake that times out.- Its writes use
toybox ddwithconv=notruncwhere it exists, as thestockpath does; theddon the path refusesconv, and does not truncate at aseekwithout it. - Each target is checked as a block device, at the length its table entry
gives, before anything is written, boot0 included. A table that goes in alone
is the exception: the run after its reboot checks them.
ddto a name that is not one writes a file in RAM/devthat reads back and matches. - The by-name bootloader nodes are decoys on both TWRPs, so the chain names
mmcblk0pN. bboe2 repoints by-nameboot_aatboot_a_xand v2.0.0's TWRP does not, so the boot image namesboot<slot>_x. lk_build_descdoes not say a Dot is unlocked. amonet v2.0.0 patches the stock LK and keeps its build string, so an amonet v2.0.0 Dot and a locked stock 8146 both read63cb91b-20221007_072309;unlock_statusis in fastboot only. Thestocktarget writes norecovery, so a restored Dot keeps the TWRP it had, and the probe would read that as amonet-v2.0.0-twrp.- A locked Dot cannot reach that state, so the reading above is a gap in the
logic and not a route. Measured twice on a Dot restored to stock 8146 with an
amonet TWRP left in
recovery, v2.0.0's and 3.7.0_9-bboe2's:fastboot reboot recoveryanswersOKAYand then nothing appears on USB for 90 s, in any mode. A locked LK refuses an unsigned recovery. - The TWRP route and
stage_install_fireos6still hashlk_aagainst amonet'slk.bin, amonet v2.0.0's then amonet v1.1.0's, and refuse the rest, so a chain write needs proof the Dot is amonet's rather than a build string it shares with stock. amonet v1.1.0's LK passes, so a part-written chain still resumes. Whenlk_amatches neither,lk_bis read too: the chain writeslk_afirst, so a run stopped inside that write leaveslk_atorn andlk_bas it was. Both routes into the chain leave amonet v2.0.0's LK in both slots: its installer writes both, and so does the fastbrick, measured on a Dot read the moment its TWRP came up from stock. Every stock LK is 241664 or 245760 bytes, against 359744 and 372368, so none can match. The check is defence in depth, not a fix for a reachable failure. - The probe waits for
mtpon every TWRP, not only amonet v1.1.0's. adb answers first, and the switch tomtp,adbre-enumerates USB, so a stage starting in that window reads truncated output with exit 255. So each device-side answer this route reads ends in a marker of its own,sgdisk --printincluded, because adb cuts the tail: a marker at the front leaves a cut line parsing as an empty, and therefore passing, result. A cut md5 is shorter than 32 characters, but a read that returned nothing hashes to a whole one, so the chain's reads and writes also requiredd's record count. - v2.0.0's TWRP costs the install nothing. It returns
adb shell's exit status and has every tool the install needs, and/systemtook 81 s there, as under bboe2:gunzipand the eMMC are the limit, not adb. misc's boot control block is written, as a read of block 1, 7 bytes patched at 0x160, and a write back, which is what v1.1.0'sreset_bcbdoes. Leaving it inherits whatever ran before: after anamonet-biscuit-v2.0.0install a Dot read00 41 42 42 01 af 3e, slot b at prio 14 with 3 tries, a live fallback to the Fire OS 6 images an amonet v1.1.0 chain cannot boot. The boot image goes to slot a to match, not toro.boot.slot_suffix, which here names the slot amonet v2.0.0 booted.- Measured on a Dot: the timings table above has the figures.
- firebreak erases boot0, then opens the port whose USB product id is the bootrom's, so a port that was already there does not matter.
- The engine is the GCPU,
gcpu@10210000in the device tree: a 4 KiB window that holds every register the exploit touches. Fire OS hands it to the TEE, which is encrypted, so the registers are read from the preloader instead. - The preloader drives the engine with the same sequence, at
0x1d1bcas Thumb-2. It names everygcpu.Registerand givesACQUIRE_VALUESandSLOT_POINTER_VALUES.0x400,0xC48and0xC68are computed, not literals, so a byte search misses them. The routines are identical in every cached preloader: biscuit's, crown's, and the radar one amonet-radar v1.0.0 writes.tests/test_preloader.pychecks them, and skips without a cache, so CI never runs it. - The bootrom owns
0x102868and0x1028A8, so the preloader does not hold them. Both agree with mtkclient (GPL-3.0); nothing was copied. mtkclient also clears0x1072DC. One write to0x102868has been enough on every run, and why is not known. - This route now runs only from amonet v2.0.0's fastboot, for
--short. From amonet v2.0.0's TWRP the targets write the chain in place instead; the section above has it. - The script stops if
lk_build_descis already v1.1.0's: a second erase gains nothing. It readsgetvar, and refuses a Dot whoseunlock_statusis nottrue, or whoseproductis notBISCUIT. Every unlocked amonet LK answers this fastboot. Without theproductcheck, an Echo (2nd Generation) on amonet-radar, whose LK saysRADAR, would read as amonet-v2.0.0-fastboot, and a run would erase its boot0 and write biscuit's preloader and LK to it. This is read from the code and the LK images, not seen on a radar. - It runs
fastboot erase boot0andfastboot reboot, and the Dot drops into its bootrom. - The step ends with the plan's own reboot. Every write reports its own result, so no log is read.
- The bootrom is
0e8d:0003. On macOS it is/dev/cu.usbmodem*. On Linux it is/dev/ttyACM*, owned bydialoutunless the udevttyline gives it toplugdev, and ModemManager can grab it first. On Windows it is a COM port that needs MediaTek's VCOM driver (cdc-acm.inf, class Ports, 3.0.1504.0, "MediaTek USB Port" in the Microsoft Update Catalog), installed by hand withpnputil /add-driver; without it0e8d:0003is an unknown device. - No other mode needs a hand-installed driver. On a Windows machine that rooted
a Dot, the driver store held only MediaTek's and Amazon's
FireDevicesUsbDeviceClass, which serves amonet fastboot (0bb4:0c01) and can come through Windows Update. Windows bound adb (1949:0110and Fire OS 6's0e8d:201c, by its interface class through its ownwinusb.inf) and v1.1.0's TWRP (18d1:4ee2) itself. TWRP 3.7.0_9-bboeN is untried on Windows. macOS and Linux need no driver. - A port that will not open is named once and retried: udev or a modem manager can hold it for a moment. The wait is 60 seconds after the erase, and 600 when the run waits for a short.
- The run uploads v2.0.0's payload from the fastbrick's zip, reads the Dot's partition table over USB, resolves v1.1.0's plan against it, and reads every write back. On macOS and Windows the step takes 18 s.
- The bootrom's word commands are big-endian and the Dot's memory is
little-endian, so the payload and the AES input go out byte-swapped per
word. Unswapped bytes load in the wrong order and nothing on the wire shows
it, which is why
write_wordstakes words. - It reads and writes 64 blocks per command,
0x1004and0x1003. One block per command was 10 times slower: every region is read twice, and each round trip costs about 10 ms. - The payload does not answer the area switch,
0x1002, so block 0 is checked after each one: boot0 startsEMMC_BOOTor is zeros, and the user area ends55 AA. A read-back alone cannot catch a lost switch, because it reads through the same wrong area. - A read-back proves the transfer, not that the eMMC kept the bytes: its 256 KiB write cache answers reads. The cache is off at power-on and the payload never turns it on, so this step writes uncached.
- v2.0.0's payload ends a 512-byte block read and a 256-byte RPMB read on a
full packet, and Windows' VCOM driver waits for a short packet. So each such
read is followed by a 4-byte read,
0x5000, whose bytes are dropped. - v1.1.0's
fastboot-step.shships a Linux-only fastboot. The script runs its three commands with the host's fastboot instead:bin/tz.imgtotee2, TWRP 3.7.0_9-bboe2 torecoveryin place of v1.1.0'sbin/twrp.img, thenfastboot oem reboot-recovery.
- The image is the
biscuitbuild of bboe/twrp_device_amazon_echo-mt8163, on bboe/android_kernel_amazon_biscuit. GitHub Actions builds and attests both. The script pins the SHA-256. - bboe2 adds v1.1.0's TWRP 3.2.3 links:
/dev/block/other-bootandother-systemname the current slot's kernel and system, andother-lkis/dev/null. EchoMuse's installer finds the kernel throughother-boot, and stopped on bboe1 without it. Measured on a Dot:other-bootwasmmcblk0p10andother-systemmmcblk0p13, and both followed a slot change. - v1.1.0's TWRP 3.2.3 moved 5.1 MB/s over adb, and 7.0 MB/s with cpu0 at
performance. This one moves 21.5 to 22.2 MB/s with either governor. - v2.0.0's TWRP cannot replace 3.2.3. Its kernel is 32-bit, and v1.1.0's LK starts only a 64-bit one.
- The kernel is Amazon's 5.5.5.4 one with two back-ports for Android 9's
init. Without SELinux ioctl permissions,initfails withavtab: invalid type or class. Withoutmmap_rnd_bits, it fails withUnable to set adequate mmap entropy value!. - amonet v1.1.0 keeps its image in
boot_aandboot_b. Fire OS boots fromboot_a_xandboot_b_x. This build points/bootat_x. The script does not rely on that. It writesboot<slot>_xandsystem<slot>by name. ddto a by-name path that does not exist writes a file in TWRP's RAM/dev, and the read-back of that file matches. So each target must be a block device.- A Dot in TWRP 3.2.3 gets this TWRP written to
recoveryand checked by md5. Then the script runsadb reboot recovery. The script drops 3.2.3's__bionic_open_tzdata...lines fromadb shelloutput. - adb answers before MTP is on, as
18d1:d001in this TWRP. The switch tomtp,adbputs the Dot back on USB under a new ID. In 3.2.3 the switch failed a push withfailed to read copy response: EOF. So the run waits formtpinsys.usb.config, and each push gets 3 tries. - This TWRP's
adb shellreturns the exit status. So each device-side step is a pushed script, judged by its status. The scripts have\nline endings. - The userdata script unmounts
/sdcardand/data. It formats userdata withmke2fs -t ext4 -b 4096 <userdata> <blocks - 256>, which leaves 1 MiB for the crypto footer. Then it mounts it withmount -t ext4. The script sets the size, block size and type itself, so the result does not depend on the TWRP's fstab. /datais then checked as a mountpoint. Otherwise Magisk's database lands in TWRP's RAM and is gone after the reboot.- This TWRP mounts system at
/system_root. So the script patches Fire OS's system at/tmp/fireos-system. - A failed patch leaves that mount. So the write first unmounts both.
/proc/mountskeeps the namemountwas given. So the check looks for the by-name link and themmcblk0pnode. - Never
umount -ain TWRP: it unmounts/proc. - Every push is read back by md5 on the device.
- The OTA zip's installer does 2 writes that matter:
system.new.dattoother-systemandboot.imgtoother-boot. In TWRP 3.2.3 those name the current slot'ssystemandboot_x. Its LK write goes nowhere: amonet's TWRP linksother-lkto/dev/null. Its TEE and preloader are byte for byte the ones v1.1.0 already wrote totee1and boot0. Its last write,target.blocklistto/cache/recovery/last_blocklist, is skipped: it is a file in/cache, not a partition the Dot boots from. - So the zip never goes to the Dot. The host builds the partition image from
system.transfer.listonce, into the cache: 805 MB, 386 MB gzipped. It takes 22 s on macOS and 31 s on Windows 11, in a background thread, while the Dot unlocks and downgrades. - The built image matched
system_aafter atwrp installin every MiB that the root's own edits and ext4's mount metadata leave alone. - It is built as 128 MiB slices, each its own gzip member, because a concatenation of members is one stream to any decompressor. So one cache serves both routes.
- Where
adb shellcarries a stream, the slices go out back to back on its stdin,gunzip -creads them andddwrites the partition, which overlaps the transfer with the write and needs no room on the Dot. Where it does not, each slice is pushed to/tmpand unpacked at its own offset: 35 MB at a time against a 240 MiB tmpfs, so nothing is staged on the eMMC. - The route is chosen by sending 1 KiB holding 0x1a and hashing it on the Dot, not by naming an operating system. A probe that does not complete takes the pieces, which need no stream, and says so; a probe that arrives cut takes them without a word.
- A Windows adb cannot carry it. Measured on bryce in amonet v2.0.0's TWRP,
from Windows 11 with adb 36.0.1:
gunzipread 12 bytes and stopped withgzclose: Illegal seek, having written 12 bytes, while macOS carried 64 MiB through the same recovery minutes apart.adb pushof the same bytes is intact from both, at 21.5 MB/s, so the sync protocol is the one that crosses hosts. What adb shell carries has the byte and the counts. - The command's last line says
stream-ok,slice-okorgunzip-failed, and a write is accepted on that line alone, not on silence or on the word elsewhere in the output. - The stream's deadline covers the transfer, not only the wait after it: one thread feeds the slices, another reads adb's output, and a stalled adb is killed when the 30 minutes run out.
- TWRP 3.2.3 used
adb exec-in: 65 s on macOS, 76 s on Windows 11. It is binary-safe but returns before the Dot has finished, with no status of its own, so a read taken when it returns can differ from what lands. - On a Dot the gzip reaches
cat > /dev/nullin 17.5 s. Throughgunzip, with the output discarded, it takes 36.4 to 39.3 s. Sogunzipon the Dot is the limit. The whole stage, with the eMMC and the md5 read-back, took 80 s while it streamed, and about 77 s with TWRP 3.2.3. Slicing costs 23.4 s against 15.9 s for 256 MiB. The sliced route took 108 s to 109 s across a whole install from Windows 11. - The partition is then read back by md5: 12 s with 3.2.3.
- The host builds it from the zip's
boot.imgand Magisk 17.3's zip, in about 2 s. Nothing runs magiskboot or Magisk's installer. - 17.3 is the last 17.x. Up to v25.2 support Android 5.1, but from v18 the
boot patch and
service.dpath change, and the host's boot image build follows 17.3's. - The ramdisk loses
verifyfrom every fstab, anddefault.propgetsro.secure=0,ro.debuggable=1andpersist.sys.usb.config=adb. - adb alone, not stock's
mtp, and notmtp,adb. On Ubuntu, GNOME'sgvfs-mtp-volume-monitorreset a booted Dot onmtp,adbabout every 7 s, so the last step's wait for Fire OS 5 timed out; 2 owners hit it. With adb alone the Dot has 1 interface, the ADB one, and nothing to probe. Nothing here uses MTP, and TWRP'smtp,adbis its own ramdisk's. - The property holds:
init.fosflags.sh, made to take its adb branch, only appends,adbwhere adb is missing, and a fresh root leaves no persisted value in/data/property. bryce and nether's Dot each readadbafter a reboot, and bryce kept one USB device number for 2 minutes. - The cmdline is the 512-byte header field at offset 64. Stock is
bootopt=64S3,32N2,64N2; the script appends a space andandroidboot.selinux=permissive. - The field ends in a NUL, so the line with that addition must stay under 512 bytes. Padding the field pads but never cuts, so a longer line grew the header past its page, moved the kernel and the ramdisk off their offsets, and raised nothing. The script stops instead. Biscuit's stock line leaves room; this guards another device's.
- Then Magisk's patch, as its
boot_patch.shdoes it withKEEPVERITYandKEEPFORCEENCRYPTfalse.initbecomesmagiskinitandverity_keygoes; both originals go into.backup, with.magisk. In the kernel,skip_initramfsbecomeswant_initramfs. - Magisk's installer also kept a gzipped copy of the image as it found it,
/data/stock_boot_<sha1>.img.gz, and named it in.backup/.sha1. Only Magisk Manager's image restore reads them, andstockis the way back to stock here, so neither is written. - The kernel is a 512-byte MTK header, a gzip stream and the dtb. Only the stream and the header's size field change. The ramdisk's cpio is written as magiskboot writes one: sorted, inodes from 300000, every mtime 0.
- Reading a cpio ends at the trailer or at the end of the data. A ramdisk cut short before its trailer read an empty magic past the end, and the run then blamed the format rather than the length.
- The stock image ends in a 2,048-byte signature. It is dropped, as magiskboot dropped it: the unlocked LK does not check it.
- Compared with the image that
twrp installof Magisk wrote on a Dot, every header field, the kernel, the dtb and every ramdisk file are the same, except in 3 places. There is no.backup/.sha1. 3 symlinks keep their stock modes, where busybox'scpiohad made them 0777. The MTK header keeps the stock 0xff padding, where magiskboot wrote zeros. - The image is padded to a 4096-byte multiple and written with
dd. Aftersyncthe page cache is dropped, and the partition is read back by md5 over the image's length. - The host's copies go in a directory under the cache, not in
%TEMP%. On Windows 11, reading a newly written boot image from%TEMP%stalled for 10.3 s in 10 of 15 trials, and in 0 of 23 trials elsewhere.
/system/etc/init.fosflags.shstrips adb from the USB configuration on every boot after the first. The script forces itsFOS_FLAGS_ADB_ONbranch true and neutralizes itsunset_adb_persistent_propertycalls, then checks both edits.- The 4 Amazon update hosts go into
/system/etc/hostsas127.0.0.1.
- The zip's installer is not run. Its writes go to the Dot as one cpio
archive, which TWRP's
cpiounpacks into/data. The files are then read back as one md5, over all of them in name order. /data/adb/magiskgetsarm/,common/andchromeos/from the zip, mode 755, and the installer's busybox: base64 of xz, inupdate-binaryasBB_ARM.- It also gets
magisk, whichboot_patch.shwrote withmagiskinit -x magisk, and which module installs and--unlock-blocksrun. It is the xz stream inmagiskinitthat unpacks to an ELF. Unpacked on the host and bymagiskinit -xon a Dot, its md5 is the same./sbin/magisk.bindiffers from it in 62 bytes, which magiskinit randomizes at boot. - On a new userdata the installer wrote to
/data/magisk, because/data/adbdid not exist yet. Magisk's daemon moved it to/data/adb/magiskat the first boot. The script writes there directly. - The rest of the installer changes nothing on this Dot: there is no
/system/addon.d, nosuin/system, and nothing in/datato migrate. - Its database is seeded so the adb shell gets root without a prompt the Dot
cannot show:
/data/adb/magisk.db, mode 600, withpolicies(uid INT, package_name TEXT, policy INT, until INT, logging INT, notification INT)holding(2000, 'com.android.shell', 2, 0, 1, 0).
- An update would replace the boot image and remove root. On the first rooted
run the script runs
pm hide com.amazon.device.software.ota, then readshidden=trueback fromdumpsys package. - Right after the first boot,
dumpsys packageandpm hidecan both exit 1 in the same second thatsys.boot_completedreads 1 andsu -c idanswers. Seen on 3 of 3 roots from Windows 11; a rerun a minute later hid it each time. So the step says so once, then waits 1, 2, 4, 8 and 16 seconds between tries, and stops 60 seconds after the first try. - Measured on a Dot from Windows 11, writing Fire OS 5.5.5.4 again from TWRP 3.7.0_9-bboe2: the first try failed as before, and the second, 1 second later, hid it.
firebreak stock <build> returns a Dot on amonet v1.1.0 or v2.0.0 to stock Fire
OS 6 or Fire OS 5, to test a root from a clean start. stock is a target
like the others, so every state has a route to it: a rooted Dot goes to
recovery, amonet v1.1.0's fastboot gets amonet v1.1.0's TWRP, Fire OS 5's
locked fastboot gets the fastbrick, and in any TWRP the restore runs.
- Only
stocktakes aBUILD, and without one it installs 8149. The run downloads the build before it touches the Dot. - Fire OS 5.5.3.1 to 5.5.5.4 carry Fire OS 6's anti-rollback versions, so they restore as Fire OS 6 does.
- 5.5.0.3's PL is 3, below RPMB's 5, so its preloader would stop at
version mismatch!. Its restore leaves boot0 clear and reboots into the bootrom. There amonet v2.0.0's payload zeroes RPMB, writes the preloader and reboots. A PL 5 preloader that started in between would raise RPMB to 5 again. - Stock Fire OS 5 shows nothing on USB once it starts, so a Fire OS 5 restore ends after the reboot.
- Measured on a Dot, 2026-10-13: each Fire OS 5 build restored in about
2.5 min, then read locked with its own
lk_build_desc, and unlocked again. - Measured on a Dot, 2026-10-14: 5.5.0.3 restored from amonet v2.0.0 in
3 min 50 s. The bootrom came up 5 s after the reboot, and its RPMB began
AMZN. The Dot read locked with 5.5.0.3'slk_build_desc. Its LK does not reportantirback_pl_version. The fastbrick then unlocked it, and the default target rooted it in 8 min 4 s. - The restore ends at stock-booted: the run waits for stock to start, which
shows as
adbansweringunauthorizedabout 1.5 minutes after the reboot. A Dot already stock-booted, or in stock fastboot, has nothing to restore. An amonet v2.0.0-unlocked Dot booted into Fire OS 6 without root reads the same. So the message says "appears", then names the + gesture, which starts v2.0.0's TWRP, for a Dot unlocked with amonet v2.0.0. The condition comes first, so the owner of a plain stock Dot can skip it. - The restore clears boot0 first and writes it back last. It creates
boot0-erasedbefore the clear and deletes it once boot0 reads back, as the downgrade does. So a restore stopped in between, then rebooted, is resumed through amonet v1.1.0's bootrom step and amonet v1.1.0's TWRP, and restored again. Before the marker,stockcould not resume that Dot: it needed a TWRP, and the Dot showed up only as its bootrom. - A resume clears the TWRP states from the run's acted-on set, so a run that started in TWRP acts on TWRP again after the bootrom step.
- Measured on a Dot, 2026-10-04: a restore stopped during the system write,
then
adb rebootfrom TWRP, then a rerun about 20 s later. The bootrom's one window was open when the rerun's bootrom step started, and amonet then skipped ports already present. The window closed, and nothing showed on USB for 9.5 minutes, until the cable was unplugged and plugged back in. The bootrom step then found it, and the run went on through amonet v1.1.0's TWRP and the whole restore to stock 8146:[1/19]to[19/19], 13 min 50 s with the wait. The resume now takes a bootrom port already present, and asks for the replug only if nothing happens within a minute. - A restore stopped without a reboot leaves TWRP running, so a rerun restores from there.
- The wait for stock to start has the 10-minute limit of any other state.
- A booted Fire OS 5 Dot without root gets
adb reboot recovery, as thestocksubcommand did. For the other targets that state is a boot still in progress, and the run waits. - v2.0.0's TWRP mounts by name, under
/dev/block/platform/.../by-name/, so an unmount pattern anchored on/dev/block/mmcblk0matched nothing there and the writes went to mounted filesystems. What is left mounted is named back, because a refusedumountis otherwise silent and the Dot has no screen to look at. - That TWRP carries two
ddimplementations which do not take the same operands: the one on the path answersconv option disabled. So the script usestoybox ddwhere it exists. TWRP 3.7.0_9-bboe1'stoybox ddtruncates at itsseekoffset, and the block device answersftruncate: Invalid argument. So the one write withseekpassesconv=notrunctotoybox dd. - Measured on a Dot, 2026-10-04, before
stockwas a target:stock 8146from amonet-v1.1.0-booted, through v1.1.0's TWRP 3.2.3, in 6 min 9 s; from amonet-v2.0.0-booted, through v2.0.0's TWRP, in 2 min 59 s; from amonet-v1.1.0-bboe-booted in 1 min 52 s. Each was rooted again after the fastboot gesture: toamonet-biscuit-v2.0.0in 7 steps,amonet-biscuit-v1.1.0in 10,amonet-biscuit-v1.1.0-bboein 9, as the table counted then. - As the target: from amonet-v1.1.0-bboe-booted in 3 min 11 s,
[1/17]to[17/17], ending when stock started, and 4315 in 4 min 9 s, which rooted back in 9 min 8 s. Run again on the stock Dot, it reported nothing to restore and downloaded nothing. sgdisk,mke2fs,blockdevandmd5sumare looked for before the countdown, becausesgdiskandmke2fsare not reached until after 1.6 GB has gone in.- 4315 ships a block OTA in place of a
payload.bin:system.new.datwith a transfer list, and boot, LK, TZ and the preloader as files in the zip.system.imgis built from the list, which must be version 3 or 4 and use onlyerase,zeroandnew, since the rest patch blocks this does not have. Its ranges must cover the image from 0, and no block may be written twice: Fire OS 5's own list erases the whole image and then writes over it, where 4315's erases only what it does not write. Only thenewranges are written, because the file is truncated to size first, andsystem.new.datmust end where the last of them does. A block OTA carries no hash per image, so they rest on the OTA's pinned SHA-256. - The built
system.imgstays in the build's own directory for the next run, which saves the 20 to 30 seconds it takes to build. It is built assystem.img.partand then renamed, and its SHA-256 goes insystem.img.sha256. A run reuses the image only when that hash matches, because an older run wrote the image in place, so a stopped run left one of full size with blocks missing. - LK, TZ and boot are padded to a multiple of 4096 bytes. A write is
checked over whole sectors,
n // 512of them, and 4315's LK is 241,448 bytes, so the check left the tail out and failed on a write that had gone in. A payload build's images are already whole blocks. - Each OTA comes from Amazon's CDN, at the URL ftvdb lists for it, and is pinned by the SHA-256 of a copy that matched ftvdb's md5. The run does not ask ftvdb.
- Each
payload.binoperation is read where it lies, so peak memory is one image plus one operation: about 1 GB, for the 768 MB system image. - The manifest's own lengths are not trusted. A number cut short raised a bare
IndexError, and a field cut short returned fewer bytes than it declared with no error, so the run ended in a traceback or wrote a short image. Both now stop with the reason. A field read as the wrong wire type stops too: it used to reach an attribute that an int does not have. - A passed check prints
okrather than the tick where the console cannot encode it, such as a legacy Windows code page, which would otherwise raiseUnicodeEncodeError.
- amonet v1.1.0 renames the stock
boot_aandboot_btoboot_a_xandboot_b_x, and adds its ownboot_aandboot_bas partitions 17 and 18, cut from the end of userdata. The stock table is rebuilt from the Dot's own rather than from constants, so every offset and size is that Dot's. - The backup table goes in before the primary: a run cut during the primary's write leaves it mixed, and the backup is what the next run reads instead.
- A table with no
_xname is already stock and is kept: that is what makes a stopped run safe to repeat, and why v2.0.0's table needs no surgery. - The kernel must reread the table before cache and userdata are formatted.
Against the old one,
mke2fswould size userdata to amonet's cut. - The first run on a Dot saves that Dot's own table as
current-gpt-<serial>.bin, and later runs keep it rather than saving the stock table over it.
- boot0's header is cleared first. With no valid preloader the bootrom waits for a USB host instead of running anything, so a failure anywhere after it leaves a Dot that amonet's bootrom step can reach without opening the case. Forced on hardware: the bootrom appeared 4 seconds after the reboot and waited about 39 seconds, and amonet v2.0.0's bootrom step rewrote the whole chain in 22. The order of the bootchain writes is not what makes this survivable -- with the header gone, no slot is reachable anyway -- the bootrom is.
miscgets zeros and, at 0x360, the boot control block amonet v1.1.0's bootrom step writes:00 41 42 42 01 8f 00. Slot a is thenprio 15 tries 0 success 1, and slot b is unused. Each slot byte packspriority:4,tries:3andsuccess:1, low bits first.- A zeroed
miscis not safe. Stock's first boot starts slot a with a few tries andsuccess 0, and each boot cut short spends one. On a Dot, a gesture during the first boot and the power-ons after it left slot a attries 0 success 0. The preloader then reset about every 30 s with the ring dark, never starting LK, so neither the gesture norFACTFACTreached fastboot. It did not fall back to slot b, whose tries stayed at 3. It took the eMMC short to get back. A slot marked good spends no tries. tee1andtee2are not slot-tied: every slot-tied partition takes a letter fromro.boot.slot_suffixand TEE takes digits, and amonet's bootrom step writes its LK to both slots unconditionally, but its TEE payload only totee1. So they go backup first and primary last, rather than by slot.- boot is padded with zeros to the size of
boot_a, which both slots share, so no byte of amonet's boot image survives and the read-back covers the whole partition. - The preloader is padded the same way, to boot0's size, which depends on the eMMC chip. devices.md has the sizes.
- TWRP answers
ro.product.deviceonly because amonet's TWRP sets it in itsdefault.prop; a recovery ramdisk has no reason to carry it otherwise. - A push and its read-back address different block devices, whose page caches are not coherent, so the cache drop between them is what makes the comparison mean anything.
adb pushto a node that does not exist does not fail: adbd creates a regular file in TWRP's RAM-backed/devand reports success. A partition's read-back still catches that, because it reads the raw disk at the computed offset rather than the node -- but only after 768 MB has gone into a tmpfs on a 512 MB device. So the start sector is checked first, which also catches a number that means a different partition in the table the kernel still holds.- boot0 is the one write whose read-back names the node it wrote, so there a regular file would match itself and pass. Hence the check that it is a block device. For the same reason the cleared header is counted twice, bytes read and bytes left non-zero: a read that returned nothing would otherwise look like a header that cleared.
- The skip compares a mebibyte of head first, which costs 0.07 s and is enough because a different build differs within a kibibyte or two. Only a partition that looks right pays for the full comparison, 13 s against the 80 s a write would take.
- A restored Dot has no Wi-Fi until it is set up in the Alexa app. To root it again, skip that setup: on Wi-Fi it can update to a build firebreak does not support, or away from the build under test.