---
title: Perlica
description: A Perlica record on the Titan security chip and rescue payload recovery.
publishedAt: '2026-10-30T00:00:00Z'
meta:
  title: Recovering a Titan
  description: I love Google Tensor with Titan M2 and M3 totally secure with a nice crisp NuggetOS included, or how to make your nugget more chewy after it hardens like a brick
  image: media/4e91ec13a57adc69a80645d0b96e8aff7a39e82006afcbe1f5d7e8d930e85b29.webp
featuredImage: media/b04d38490d56a864644e57193ae4bf2931822555258de602aa423b7340a84609.png
design: perlica
designData:
  titleLineOne: RECOVERING
  titleLineTwo: A TITAN.
  quote: '「Putting too much faith on Google Security.」'
  leftNote: >-
    No header offset. The {{longjing}} manifest is a binary struct at a fixed position, not a key-value section. The `{{security_version}}` field lives in the manifest instead of an {{antirollback}} counter. No rec frame count  {{does not do framed rescue payloads}}. The timestamp is 0. {{thing}} ships in Pixel 11, and this image was built before its release, {{so the firmware carries no build date}}. The {{RW payload is 344076 bytes, a third larger than the d3m2's 258048}}. The {{manifest offset i}}s 65536, exactly half the Dauntless {{position}} .
  rightNote: >-
    This sequence is a ritual. Not because the chip requires ceremony. Because the engineer needs structure against the silence. The chip will never explain itself. The sequence is the only meaning the engineer can impose on a component that refuses to communicate. Every check is a rebellion against the irrational.
  chipCopy:
    - That’s the hardware module chip
    - It’s way too secure than tee
    - It doesn’t share your cpu
    - your memory, and has its own storage
    - it’s both sw&hw resistant against attack
    - even if you were a hw hacker you woudn’t be able to break in
---

<div class="perlica-warning">
  <div class="perlica-warning__note">
    <p>i definitely not revising this 67 times just to say 67</p>
    <figure class="perlica-video perlica-video--67">
      <video class="perlica-video__media" src="/perlica/video/67.mp4" poster="/perlica/video/67.webp" width="480" height="548" muted loop playsinline controls preload="metadata" aria-label="Looping meme: a sketch of the number 67 skidding across the screen."></video>
      <figcaption aria-hidden="true">67.mp4</figcaption>
    </figure>
    <p class="perlica-warning__script">This page is not designed to be read on mobile by design, I&rsquo;m tweaking while working on this and so will you, so bear with me.</p>
  </div>
</div>

<!-- INFOGRAPHIC: chip-frame
layout: full-width stage, chip at right per Frame 28 (8912:590)
show: hand-drawn Titan chip sketch with the caption line below
facts: the chip is Figma 8912:552's red sketch (public/perlica/figures/chip-drawing.webp). Stage renders between the lead-in paragraph and the body, matching Frame 28's order. The stage is decorative; its caption meaning is the body text that follows.
avoid: reintroducing the 3D model, the old title-stage chip layout, or inspection pips
errors: none, the stage is decorative and static
source: the rescue flow intro below
END INFOGRAPHIC: chip-frame -->

<div class="perlica__chip-frame" role="img" aria-label="The rescue flow for the Google Security Chip. Titan M2/3, Dauntless, Epic, whatever you call it. A hand-drawn sketch of the Titan security chip.">
  <p class="perlica-chip-label" aria-hidden="true">Google Security Chip</p>
  <p class="perlica-chip-caption" aria-hidden="true">Titan M2/3, Dauntless, Epic, whatever you call it</p>
  <p class="perlica-chip-lead" aria-hidden="true">The rescue flow for the</p>
  <img decoding="async" class="perlica-chip-arrow" src="/perlica/figures/chip-arrow.webp" alt="" aria-hidden="true" width="756" height="510" loading="lazy" />
  <img decoding="async" class="perlica-chip-drawing" src="/perlica/figures/chip-drawing.webp" alt="" aria-hidden="true" width="608" height="686" loading="lazy" />
  <div class="perlica-trace perlica-trace--chip" aria-hidden="true"><span class="perlica-trace__box perlica-trace--red" style="--x:58.253%;--y:35.025%;--w:26.135%;--h:48.72%"><span class="perlica-trace__tab">#00 GSC/TITAN 0.31</span><span class="perlica-trace__readout">x0740 y0266  332×370</span></span></div>
</div>

RW firmware is the single least understood piece of the whole Pixel stack. Most people only meet it the day an ordinary flash fails to explain why a phone with a working bootloader simply wont boot. The device is COOKED. No warning. No tutorial. Just a brick, and nobody at the vendor will tell you what the fuck happened.

Firmware rescue means opening the funny website, plugging in the phone, and doing whatever it tells you once it notices GSC RW firmware is dead. The bootloader still responds. The screen reads <span class="perlica-roboto">Secure boot: PRODUCTION</span>. The phone is technically still speaking. Its lowkey lying to you.

<p class="perlica-fine">This is fine. Everything is fine.</p>

Meanwhile, another line says <span class="perlica-roboto perlica-roboto--flat">NOS production: <span class="perlica-error">error!</span></span>. NOS is Nugget OS, the chip&rsquo;s own firmware. The active GSC RW firmware partition is corrupt. The bootloader can answer. The hardware trust root can&rsquo;t.<br />
This is the part where the NPC at <img decoding="async" src="/perlica/figures/gstore.png" alt="" width="24" height="24" class="perlica-gstore" /> that sold you the phone quietly looks away.

You want to write the firmware and go home. GSC rescue flow has other plans. Inspect the chip, build a plan, execute it, then check what actually came back. Writing is one part of that sequence.

Every rescue frame carries a SHA-256 check. A mismatched frame gets no ack and the stream stalls right there. Resend it clean and the run continues. Give up and the rescue is dead. Even a successful command can leave the partition state wrong. The command says gg. Perlica asks for receipts.

Meet **Perlica**, our sussy wifey. Shes a Rust CLI for inspecting Dauntless rescue records and Epic/长晶 RW payloads. She turns the protocol into an inspect, plan, execute, and verify sequence. We ask her to save the phone. She asks for the partition state. Romance is going well.

<!-- INFOGRAPHIC: sussy-wifey
layout: Frame 29 (8912:638): meme collage right, Rust CLI caption left
show: the meet-sussy-wifey collage image with the Perlica caption text at its left
facts: collage is an exported design asset (8912:636); the Rust CLI text is real article text per 8912:641.
avoid: recreating the collage in HTML or omitting its alt text
errors: none, decorative section
source: the Perlica intro above
END INFOGRAPHIC: sussy-wifey -->

<figure class="perlica-wifey" role="figure" aria-label="meet sussy wifey: a meme collage of Perlica screenshots with annotations">
  <img decoding="async" src="/perlica/figures/sussy-wifey.webp" srcset="/perlica/figures/sussy-wifey-640.webp 640w, /perlica/figures/sussy-wifey-960.webp 960w, /perlica/figures/sussy-wifey-1280.webp 1280w, /perlica/figures/sussy-wifey.webp 1581w" sizes="(max-width: 767px) 121.15vw, 41.66vw" alt="meet sussy wifey meme collage: Perlica screenshots with notes about her Endfield loyalty and Originium documents" width="1581" height="2331" loading="lazy" />
  <img decoding="async" class="perlica-wifey__arrows" src="/perlica/figures/wifey-arrows.webp" alt="" aria-hidden="true" width="643" height="594" loading="lazy" />
  <figcaption class="perlica-wifey__caption">Rust CLI for inspecting Dauntless rescue records and Epic/长晶 RW payloads that we working on.</figcaption>
  <div class="perlica-trace perlica-trace--wifey" aria-hidden="true"><svg class="perlica-trace__links" viewBox="0 0 761.35 1146.01" preserveAspectRatio="none"><line class="perlica-trace--red" x1="364.7" y1="251.1" x2="152.7" y2="693.1" /><line class="perlica-trace--ink" x1="364.7" y1="251.1" x2="611.2" y2="704.6" /><line class="perlica-trace--ink" x1="364.7" y1="251.1" x2="189.7" y2="220.6" /></svg><span class="perlica-trace__box perlica-trace--red" style="--x:35.815%;--y:14.672%;--w:24.168%;--h:14.485%"><span class="perlica-trace__tab">#01 PERLICA 0.98</span><span class="perlica-trace__readout is-inside">x0840 y0219  184×166</span></span><span class="perlica-trace__box perlica-trace--ink" style="--x:3.898%;--y:6.818%;--w:42.031%;--h:24.869%"><span class="perlica-trace__tab">#02 FACE 0.91</span><span class="perlica-trace__readout is-inside">x0597 y0129  320×285</span></span><span class="perlica-trace__box perlica-trace--ink" style="--x:60.902%;--y:56.12%;--w:38.747%;--h:10.733%"><span class="perlica-trace__tab">#03 FACE×2 0.88</span><span class="perlica-trace__readout is-inside">x1031 y0694  295×123</span></span><span class="perlica-trace__box perlica-trace--ink" style="--x:8.233%;--y:58.214%;--w:23.642%;--h:4.537%"><span class="perlica-trace__tab">#04 NAMEPLATE 0.76</span></span></div>
</figure>

<aside class="perlica-binaries">
  <img decoding="async" src="/perlica/figures/patreon-banner.webp" alt="" width="1495" height="838" loading="lazy" />
  <p>Perlica prebuilt binaries is available on <a href="https://patreon.helluvaos.com/posts/perlica-distrib-173718554">our Patreon</a></p>
</aside>

<figure class="perlica-scene">
  <img decoding="async" src="/perlica/bands/wuling-zone.webp" srcset="/perlica/bands/wuling-zone-640.webp 640w, /perlica/bands/wuling-zone-960.webp 960w, /perlica/bands/wuling-zone-1280.webp 1280w, /perlica/bands/wuling-zone-1920.webp 1920w, /perlica/bands/wuling-zone.webp 2560w" sizes="(max-width: 767px) 65vw, 65vw" alt="North Wuling city under a fractured purple sky" width="2560" height="1471" loading="lazy" />
  <figcaption aria-hidden="true">NORTH WULING<br />EXCLUSION ZONE</figcaption>
</figure>

## north wuling exclusion zone

<p class="perlica-valley-ink">Every security chip has one component that the phone trusts more than it trusts itself. GSC is that component. It signs. It attests. It decides whether the boot chain is intact. Final boss of the trust chain. Doesn't drop loot. Also the root of trust, and nobody is getting root on it. You can't grind it. You can't parry it. You can only ask it nicely, over SPI, and hope.</p>

<p class="perlica-hope">Hope is the only lubricant this interface ships with. The pull rate on a working phone is not posted.</p>

<div class="perlica-ooglers">
  <p class="perlica-ooglers__lead" aria-hidden="true">It&rsquo;s also the component whose rescue protocol is documented for</p>
  <img decoding="async" src="/perlica/figures/ooglers-qr.webp" alt="" width="801" height="801" loading="lazy" class="perlica-ooglers__qr" />
  <span class="perlica-ooglers__word" aria-hidden="true">ooglers</span>
  <p class="perlica-ooglers__tail" aria-hidden="true">and kept from everyone else.</p>
  <p class="perlica-sr-only">It's also the component whose rescue protocol is documented for googlers and kept from everyone else.</p>
</div>

<p class="perlica-valley-ink">Not "poorly documented." Not "lost." Just on the wrong side of a wall. The protocol lives in the bootloader and the flasher that drives it, nowhere a user is meant to look. Everyone outside the wall is an NPC running on guesswork. The vendor treats the protocol like a raid crew treats a rival MMO. You are here to use the phone. Reading the firmware is grounds for removal. This isn&rsquo;t up for debate. Tread CAREFULLY.</p>

<p class="perlica-valley-ink">The GSC talks through a set of oem commands that aren&rsquo;t advertised. The commands exist in the bootloader firmware. They don&rsquo;t appear in any help text. You find them by reading the binary or following protocol traces. This is the part where</p>

<figure class="perlica-video perlica-video--silverwolf">
  <video class="perlica-video__media" data-audio src="/perlica/video/silverwolf.mp4" poster="/perlica/video/silverwolf.webp" width="964" height="720" muted loop playsinline controls preload="metadata" aria-label="Silver Wolf: a chibi Silver Wolf figure perched by a gaming desk, filmed on a grainy security camera."></video>
  <figcaption aria-hidden="true">silverwolfgaming.mp4</figcaption>
</figure>

<p class="perlica-valley-ink">would skip the inspection, Aether-edit the bytes straight in, and then act surprised when the device dies. We aren&rsquo;t doing that.</p>

<p class="perlica-valley-ink">Ask for an explanation and you get partition markers. It doesn't do small talk. Perlica parses them and takes notes.</p>

<p class="perlica-valley-ink perlica-ask">Ask the chip for its state first:</p>

<p class="perlica-fastboot-line"><span class="perlica-cmd">fastboot</span> <span class="perlica-sub">oem</span> <span class="perlica-args">gsc version</span></p>

<p class="perlica-valley-ink">Rescue starts by asking for that report. Perlica does the same, then asks for each slot&rsquo;s header to fill in the details. The partition table returns <code>ok</code> or it doesn&rsquo;t, and when it doesn&rsquo;t, there is no error code. Just silence. The chip saw your flash and said jack shit. The markers tell the host what it can see. Perlica builds a plan from that report.</p>

<div class="perlica-commands">
  <p class="perlica-commands-label" aria-hidden="true">Related commands:</p>
  <ul class="perlica-commands-list">
    <li><code>fastboot oem gsc</code>: checks if GSC responds (10s timeout)</li>
    <li><code>fastboot oem "gsc header RO_A"</code>: per-partition header details</li>
    <li><code>fastboot oem gsc sjtag-cfg</code>: returns "0" or "1"</li>
    <li><code>fastboot flash gsc &lt;firmware.bin&gt;</code>: flash a full GSC firmware image</li>
    <li><code>fastboot oem gsc rescue</code>: execute firmware rescue</li>
  </ul>
  <p class="perlica-sjtag">Unless you are a Googler with sjtag approved device you are not seeing 1 on this</p>
</div>

<!-- INFOGRAPHIC: gsc-state-matrix
layout: a semantic legend above six state columns
show: `*` means selected and trailing `ok` means verified. `/spil_` is the separate Upgrading-from-SPIL marker in the reference flow. A selected RW without a parsed version is degraded and rescue-needed. If both RW versions are unavailable, the selected version is unavailable too. No selected RW, including both RW slots unreported, is rescue-needed.
render:
  slot grammar: `RW_[A|B]: [* ]<version>/<hash>[ ok]`
  selected + verified: `RW_B: * 0.3.2/nugget_hash ok` (`*` immediately follows `: `, `ok` is the terminal token)
  selected + unverified: `RW_A: * 0.2.2/rw_hash` (same prefix, no terminal `ok`)
  unselected + verified: `RW_A: 0.2.2/rw_hash ok` (no `*`, terminal `ok` retained)
  selected version unavailable: retain `*`, replace the version field with `?`, and label DEGRADED / RESCUE-NEEDED. This is a decoded state, not a literal raw row.
  both RW versions unavailable: render `?` in both version fields. If either row is selected, label DEGRADED / RESCUE-NEEDED. If neither is selected, use no active selection.
  no active selection: show both reported RW rows without `*`, label RESCUE-NEEDED / PURE RESCUE, never DEAD
  one or both RO slots unreported: omit those rows, label ROUTE UNCHANGED, never DEAD
  one RW slot unreported: omit that row entirely. Do not render it as invalid or dead.
  both RW slots unreported: omit both RW rows, label RESCUE-NEEDED / PURE RESCUE, never DEAD
  RW_A absent after Dauntless rescue: render `RW_A` missing or dimmed as degraded and rescue rejected
  Upgrading from SPIL: render `/spil_` as its own report line, never as an RW suffix. It selects intermediate image plus pure rescue.
facts: the GSC presents a logical A/B interface through `RO_A`, `RO_B`, `RW_A`, and `RW_B` labels when reported. `RW_A` and `RW_B` carry active-RW state. RO-row presence does not change the normal route. The columns show selected and verified, Upgrading from SPIL, post-rescue rejection, and version-unavailable conditions.
avoid: rendering columns as a single time series, calling absent RO, either RW row, or no selected RW a device-dead state, inferring `/spil_` from missing RW rows, treating selected as verified, or applying this Dauntless matrix to Epic
errors: inspection is unavailable when `gsc version` cannot be obtained or the chip identity cannot be parsed
source: state definitions and examples below
END INFOGRAPHIC: gsc-state-matrix -->

<!-- The gsc-state-matrix figure renders above this paragraph; the six decoded
     states live there, not in tables the design replaced. -->

<p class="perlica-valley-ink">GSC exposes a logical A/B interface through <code>RO_A</code>, <code>RO_B</code>, <code>RW_A</code>, and <code>RW_B</code>. An absent row isn&rsquo;t a missing bank. Its the banner slot with nothing pulled, no version, no hash, <code>valid</code> and <code>selected</code> both false. After Dauntless rescue, that empty record is bad news. If <code>RW_A</code> is absent, unselected, or missing its <code>ok</code>, GSC acceptance check fails. The rescue state is still degraded.</p>

<p class="perlica-valley-ink perlica-preflight">A selected RW with no parsed version is also degraded at preflight. If both RW versions are unavailable, the selected RW hits that same wall, and the reference flow doesn't mince words: rescue-needed, firmware may be corrupted or outdated, pure rescue.</p>

<p class="perlica-valley-ink">Neither RW selected. Both default records are unselected. No pull, no 50/50 to lose, same route anyway. Both RW rows unreported. Same route again.</p>

<p class="perlica-valley-ink">One or both RO rows missing does nothing to the normal route or the post-operation checks. This isn't the same thing as a completely unavailable inspection report. Don't confuse them. The chip can answer and still be cooked.</p>

<!-- INFOGRAPHIC: spil-card
layout: Frame 152 (8912:823): black rounded /spil_ card at left, prose at right
show: giant /spil_ token card beside the Upgrading-from-SPIL explanation
facts: the /spil_ token is display text from the reference flow, not an image.
avoid: rendering the card as an image or implying the token is interactive
errors: none, static display
source: the spil paragraph below
END INFOGRAPHIC: spil-card -->

<div class="perlica-spil">
  <img decoding="async" src="/perlica/figures/spil-photo.webp" alt="" aria-hidden="true" width="1100" height="1615" class="perlica-spil__photo" loading="lazy" />
  <div class="perlica-spil__body">
    <p class="perlica-spil__token" aria-hidden="true">/spil_</p>
    <p><code>/spil_</code> is literal text in the host-visible version report, not a decoded state the parser guessed at. The reference flow calls this Upgrading from SPIL and treats it as very old GSC RO firmware, so old it needs an intermediate image and the pure-rescue route to even talk to the current chip. The marker can coexist with a selected RW row. It isn&rsquo;t inferred from missing RW fields.</p>
  </div>
  <div class="perlica-trace perlica-trace--spil" aria-hidden="true"><svg class="perlica-trace__links" viewBox="0 0 1617 988" preserveAspectRatio="none"><line class="perlica-trace--white" x1="326.0" y1="208.0" x2="351.5" y2="505.5" /><line class="perlica-trace--red" x1="351.5" y1="505.5" x2="791.0" y2="359.0" /></svg><span class="perlica-trace__box perlica-trace--white" style="--x:13.482%;--y:39.271%;--w:16.512%;--h:23.785%"><span class="perlica-trace__tab">#05 SUBJECT 0.87</span><span class="perlica-trace__readout">x0218 y0388  267×235</span></span><span class="perlica-trace__box perlica-trace--white" style="--x:19.295%;--y:10.223%;--w:1.732%;--h:21.66%"><span class="perlica-trace__tab">#06 TRACE</span><span class="perlica-trace__readout">x0312 y0101  28×214</span></span><span class="perlica-trace__box perlica-trace--red" style="--x:43.29%;--y:30.364%;--w:11.255%;--h:11.943%"><span class="perlica-trace__tab">#07 /spil_ 1.00</span><span class="perlica-trace__readout">x0700 y0300  182×118</span></span></div>
</div>

<p class="perlica-valley-ink">Post-flash validation requires both RW slots reported valid. An omitted RW row fails the exact same predicate as an invalid row. Thats a failed flash validation, period.</p>

<h3 class="perlica-visually-hidden">How the implementation uses the report</h3>

<p class="perlica-valley-ink">Perlica follows the reference rescue flow, using the initial report to choose a route. It doesn't have to pass the checks that run after the plan executes. The report gets you started. The checks are what let you leave with a working phone. For Dauntless, <code>/spil_</code>, no selected RW reported, or no selected RW version selects the pure-rescue branch. Otherwise a compatible full image can select flash-then-rescue. After flash or rescue, the flow asks for a fresh report. This is an implementation of that flow.</p>

<p class="perlica-valley-ink perlica-p2-tail">Both the reference flow and this project require valid status for <code>RW_A</code> and <code>RW_B</code> after a full flash. Dauntless post-rescue acceptance is <code>RW_A</code>-specific, and this is the part people get wrong. <code>RW_A</code> must be selected and valid, and when an expected hash exists its hash must match. A selected and valid <code>RW_B</code> alone doesn't satisfy that check, no matter how proud of it you are. GSC does not give a shit about RW_B pride.</p>

<figure class="perlica-band perlica-band--wire">
  <img decoding="async" src="/perlica/bands/when-the-wire.webp" srcset="/perlica/bands/when-the-wire-640.webp 640w, /perlica/bands/when-the-wire-960.webp 960w, /perlica/bands/when-the-wire-1280.webp 1280w, /perlica/bands/when-the-wire-1920.webp 1920w, /perlica/bands/when-the-wire-2560.webp 2560w, /perlica/bands/when-the-wire-3840.webp 3840w, /perlica/bands/when-the-wire.webp 5120w" sizes="(max-width: 767px) 89.74vw, 100vw" alt="Wuling metro tunnel rendering of when-the-wire-is-80-1m-80-0m" width="5122" height="2889" loading="lazy" />
</figure>

<p class="perlica-valley-ink perlica-p2-tail">An <code>RW_A</code> missing at the start routes to rescue. An <code>RW_A</code> still missing after rescue fails acceptance. The rescue didn't save you. Don't declare victory. You are on guarantee now. The next attempt comes home as <code>RW_A</code> or it doesn't come home.</p>

<p class="perlica-valley-ink perlica-p2-tail">Perlica recognizes <code>RO_A</code>, <code>RO_B</code>, <code>RW_A</code>, and <code>RW_B</code> when present, and recognition isn't the same thing as a validation predicate. The tested Epic report has no <code>RO_B</code> row. Epic takes rescue-only, so the RO row being there or not doesn't change its route. For an unknown Epic revision the reference flow falls back to an RO-header query to identify the revision, and if the header metadata is missing that stops identification cold with <code>Unable to get key id from header</code>. Thats an identification failure. Calling it an RO-health state would be cope.</p>

<figure class="perlica__infographic" data-panel="rec-frame">

```rust
// this is what discovering the rescue protocol looks like
// 1056 bytes, just hashes and offsets. no file magic, no prefix.
// you learn this by reading the parser.
let flash_offset: u32 = 0x20000; // destination offset for this frame's payload
let sequence: u32 = 0; // final frame = bit 31 set (0x80000000)

let rec_frame = RecFrame {
    frame_hash: Sha256::digest(&[...]).into(), // first 32 bytes
    sequence: sequence.to_le_bytes(),
    offset: flash_offset.to_le_bytes(),
    payload: [0u8; 984], // payload data
    ack_hash: Sha256::digest(&[...]).into(), // then rewrite every 0x00 byte to 0x01
};
```
</figure>

<p class="perlica-valley-ink perlica-p2-tail">Each frame is 1056 bytes flat. This is frame data, literally, and the chip doesn't take any of it on faith. 32 bytes frame hash, 4 bytes sequence number (little-endian, bit 31 signals final frame), 4 bytes offset, 984 bytes payload data, 32 bytes acknowledgement hash. The acknowledgement hash is a second local SHA-256 over the record fields, with every <code>0x00</code> output byte rewritten to <code>0x01</code>. Each frame is verified against its own SHA-256. 263 frames in the d3m2 image, one frame at a time, no batching, and the chip acks each one before the next goes in. One bad hash and the chip just stops acking that frame. It never says which byte was wrong. Silence is the whole error message. Resend the frame clean and the stream moves again.</p>

<!-- INFOGRAPHIC: rollback-gate-comparison
layout: two vertical lanes with a shared warning between them
show: Dauntless uses an infomap bitmap and burn-count state with an observed forward gate that rejects a payload ahead of the device. Epic uses manifest security_version as a monotonic version gate that rejects an image below the device state.
facts: these are different fields and different observable directions. Classic boot-ROM antirollback is not recovered in this project.
avoid: calling infomap a scalar security_version, claiming Dauntless accepts every older image, claiming Epic uses the Dauntless forward gate, or presenting either lane as a live hardware result
errors: red stop for Dauntless forward-gate rejection and Epic security-version downgrade rejection. Keep unrecovered boot-ROM enforcement outside the diagram.
source: hidden counter section
END INFOGRAPHIC: rollback-gate-comparison -->

<figure class="perlica-band perlica-band--domain">
  <img decoding="async" src="/perlica/bands/domain-expansion.webp" srcset="/perlica/bands/domain-expansion-640.webp 640w, /perlica/bands/domain-expansion-960.webp 960w, /perlica/bands/domain-expansion-1280.webp 1280w, /perlica/bands/domain-expansion-1920.webp 1920w, /perlica/bands/domain-expansion-2560.webp 2560w, /perlica/bands/domain-expansion-3840.webp 3840w, /perlica/bands/domain-expansion.webp 5120w" sizes="(max-width: 767px) 89.74vw, 100vw" alt="Domain Expansion title card artwork" width="5120" height="2872" loading="lazy" />
</figure>

<!-- Heading kept semantic for the TOC and ids; Page 3 shows no visible
     heading, only a 79px gap from the Domain Expansion band into the body. -->
<h2 class="perlica-visually-hidden">dauntless valley</h2>

<p class="perlica-valley-ink perlica-p3-ink">Both chips check firmware versions, but they disagree on which direction is bad. You wanted to update a phone. You now need to identify which security chip considers progress a threat. Perlica compares the image's version state with the device's and refuses a payload the chip will reject.</p>

<p class="perlica-valley-ink perlica-p3-ink">长晶 uses the rule you expect. Per the OpenTitan manifest, <code>security_version</code> is a monotonically increasing counter used for anti-rollback protection. It only goes up. Older firmware takes the L. Perlica refuses an image below the device's <code>security_version</code>.</p>

<p class="perlica-valley-ink perlica-p3-ink">Dauntless carries no counter. Its rollback state is a 16-byte <code>infomap</code> bitmap, a burn-count measured in strikes, and Perlica reads the bits. It sounds backwards. The recovered firmware prints <code>blob version $ != $</code> to accept a blob by version match, and ALSO runs a forward gate that prints <code>I key $ requires upgrade: blob version $ -&gt; $</code> to reject a blob AHEAD of the device.</p>

<p class="perlica-valley-ink perlica-p3-ink">Perlica mirrors that forward gate. It refuses an image ahead of the device, not behind it. Anti-powercreep, in silicon. You pull the newer firmware. Dauntless refuses it. You explain that its an upgrade. Dauntless refuses it. Congratulations on your download. だめでした。</p>

<p class="perlica-valley-ink perlica-p3-ink">This was backwards in an earlier draft of this very tool. Perlica's own error is still named <code>DowngradeBlocked</code>. We read the firmware, identified the direction of the check, and implemented the other direction. The error name survived. Our credibility did not.</p>

<p class="perlica-valley-ink perlica-p3-ink">We built the check to stop you getting this backwards. We were the first customer.</p>

<p class="perlica-mazui perlica-valley-ink perlica-p3-ink">まずい。</p>

<p class="perlica-valley-ink perlica-p3-ink">Classic antirollback isn't in the analyzable firmware. If the chip enforces it, that lives in the unrecovered boot ROM. A strike count below the per-revision minimum is only a soft warning.</p>

<p class="perlica-valley-ink perlica-p3-ink">Different field, different direction, not the same rule. Read the gate before you copy the check. Especially if you're copying ours.</p>


<p class="perlica-mash">b hnjkl,.cfb hnjAWSXDbhjkmn,.hnjmkASXDHIJKLjofhnawsdrghjnouvrf huowdrf f jikkkkkkkkMN.ASDCV BJKLMN. I don't wanna be here I don't wanna be here<br />my keyboard is slightly tweaking while I'm writing this and this is hilarious so I kept this section intact.</p>

<figure class="perlica-video perlica-video--wide">
  <video class="perlica-video__media" src="/perlica/video/losingit.mp4" poster="/perlica/video/losingit.webp" width="498" height="428" muted loop playsinline controls preload="metadata" aria-label="Looping meme: a sketched cat-eared girl under the caption &ldquo;Im fucking losing it.&rdquo;"></video>
  <figcaption aria-hidden="true">losingit.mp4</figcaption>
</figure>

<!-- Figma Page 4: frame fill #292929 sits behind the factory band
<!-- Design Frame 149 (8916:883): full-bleed image 8916:884 with a 26% black
     wash and the THE FACTORY MUST GROW title 8916:885 (Novecento Bold
     154.53px, lh 77%, LS -3%, white, centered in a 1377px box, two lines, shadow 73.5 blur rgba(0,0,0,.4) y-2)
     rendered as DOM text over it. The dark run wraps the band, and its child
     width rule excludes .perlica-band so the hero stays full-bleed. -->
<div class="perlica-dark-run">

<figure class="perlica-band perlica-purge">
  <img decoding="async" src="/perlica/bands/factory-must-grow.webp" srcset="/perlica/bands/factory-must-grow-640.webp 640w, /perlica/bands/factory-must-grow-960.webp 960w, /perlica/bands/factory-must-grow-1280.webp 1280w, /perlica/bands/factory-must-grow-1920.webp 1920w, /perlica/bands/factory-must-grow-2560.webp 2560w, /perlica/bands/factory-must-grow-3840.webp 3840w, /perlica/bands/factory-must-grow.webp 5122w" sizes="(max-width: 767px) 100vw, 100vw" alt="Grayscale key art: a ruined industrial structure tilting under falling debris" width="5122" height="2889" loading="lazy" />
  <span class="perlica-purge__title" aria-hidden="true">THE FACTORY MUST GROW</span>
</figure>

<!-- Design P4 shows no heading here: first copy starts 95px after the hero.
     Kept semantic, hidden visually. -->
<h2 class="perlica-visually-hidden">the loadout</h2>

Perlica has some neat features, which are

<figure class="perlica__infographic">

```bash
perlica inspect-image fixtures/d3m2.ec.bin
perlica --serial <SERIAL> inspect-device
perlica --serial <SERIAL> rescue --rescue-image fixtures/d3m2.ec.bin --dry-run
perlica --serial <SERIAL> rescue --rescue-image fixtures/d3m2.ec.bin --yes
```
</figure>

`inspect-device` runs `fastboot oem gsc version` for partition state, then attempts `oem gsc header RO_A`, `RO_B`, `RW_A`, `RW_B` for per-slot header details (header failures are ignored). Perlica also provides `extract-rw --input --output` for extracting RW payloads from full images and `multi-retry --image-dir` for scanning candidate images when the chip revision is unknown.

<div class="perlica-reroll">
  <img decoding="async" src="/perlica/figures/basic-headhunting.webp" alt="" aria-hidden="true" width="1229" height="432" class="perlica-reroll__sticker" loading="lazy" />
  <img decoding="async" src="/perlica/figures/arrow-curve.png" alt="" aria-hidden="true" width="224" height="43" class="perlica-reroll__arrow" loading="lazy" />
  <img decoding="async" src="/perlica/figures/arrow-head.png" alt="" aria-hidden="true" width="18" height="10" class="perlica-reroll__arrow-head" loading="lazy" />
  <p>Its rerolling, but the account is a directory of funny binaries. No spark in the shop. The chip is allowed to be shy. Very reassuring in a security product.</p>
</div>

<p>Only the last line writes. It requires <code>--yes</code> instead of <code>-y</code> because a destructive command should look different from the commands that came before it. You don't put the nuke on the same key as the flashlight. No <code>-y</code>. You spell out the whole word before anything goes in.</p>

<figure class="perlica-video">
  <video class="perlica-video__media" src="/perlica/video/beevilandshi.mp4" poster="/perlica/video/beevilandshi.webp" width="256" height="238" muted loop playsinline controls preload="metadata" aria-label="Looping manga meme: a grinning girl with blood at her lip under the caption &ldquo;I be EVIL and shit&rdquo;"></video>
  <figcaption aria-hidden="true">beevilandshi.mp4</figcaption>
</figure>

<!-- INFOGRAPHIC: rescue-route-selector
layout: inspect device -> target family -> state gate -> selected route
show: Epic target 1 always takes rescue-only. Dauntless target 0 takes flash-then-rescue only with an active RW, no `/spil_`, and a selected version. Otherwise it takes rescue-only.
facts: flash route must pass both-RW-valid and selected-version-match inspection before staging rescue. Every rescue-only endpoint selects a compatible image, stages it, runs rescue, then re-inspects.
avoid: claiming target 0 always flashes, `/spil_` proves corruption, flash guarantees repair, host-side completion proves chip acceptance, or every stop state is chip rejection
errors: red stops for no compatible rescue image, missing flashable full image, stage or rescue transport failure, post-flash verification failure, and post-rescue verification failure. Show policy mismatch as amber because `--allow-policy-mismatch` can warn through it but never removes `--yes`.
source: planner checks and path rules below
END INFOGRAPHIC: rescue-route-selector -->

<figure class="perlica-routes">
  <img src="/perlica/figures/many-rescue.webp" srcset="/perlica/figures/many-rescue-640.webp 640w, /perlica/figures/many-rescue-960.webp 960w, /perlica/figures/many-rescue-1280.webp 1280w, /perlica/figures/many-rescue-1920.webp 1920w, /perlica/figures/many-rescue-2560.webp 2560w, /perlica/figures/many-rescue-3840.webp 3840w, /perlica/figures/many-rescue.webp 4250w" sizes="(max-width: 767px) 100.32vw, 83.28vw" alt="The many routes to rescue. Flowchart: the input is the gsc version plus image metadata, then the target family decides the route. Dauntless (target 0) takes flash-then-rescue only when it has an active RW with a parseable version, no literal /spil_ marker, ARB okay, and a flashable full image available; policy pass, or mismatch as a warning. Flash-then-rescue flashes the full image, verifies both RW, stages the .rec, then runs rescue, and the post-flash version tuple must match. If any check is false, Dauntless takes rescue-only. Epic (target 1) always takes rescue-only: a raw RW payload with no full flash, then stage, rescue, and re-inspect; the post-rescue hash must match. Rescue-only stages a compatible payload, runs rescue, and re-inspects, for Dauntless or Epic. Red stop states: no compatible image, no flashable full image, stage or rescue transport failure, post-flash verify failure, post-rescue verify failure. --allow-policy-mismatch can let an amber warning through; you still need --yes." width="4250" height="2640" loading="lazy" decoding="async" />
</figure>

<figure class="perlica-contract" role="img" aria-label="CONTINGENCY CONTRACT title break">
  <img decoding="async" src="/perlica/bands/contingency-l.webp" srcset="/perlica/bands/contingency-l-640.webp 640w, /perlica/bands/contingency-l-960.webp 960w, /perlica/bands/contingency-l-1280.webp 1280w, /perlica/bands/contingency-l-1920.webp 1920w, /perlica/bands/contingency-l-2560.webp 2560w, /perlica/bands/contingency-l.webp 2897w" sizes="(max-width: 767px) 58.6vw, 58.6vw" alt="" aria-hidden="true" width="1500" height="157" loading="lazy" />
  <img decoding="async" src="/perlica/bands/contingency-r.webp" srcset="/perlica/bands/contingency-r-640.webp 640w, /perlica/bands/contingency-r-960.webp 960w, /perlica/bands/contingency-r-1280.webp 1280w, /perlica/bands/contingency-r-1920.webp 1920w, /perlica/bands/contingency-r.webp 2160w" sizes="(max-width: 767px) 44vw, 44vw" alt="" aria-hidden="true" width="1125" height="158" loading="lazy" />
</figure>

<div class="perlica__infographic-band">

<h2 class="perlica-visually-hidden">the route selector</h2>

Perlica validates the local image format, target family, proto version, timestamp, hash, and policy fields. It asks the connected device for its GSC version, chip family, slot state, adn rollback state through the oem commands. It builds a plan and prints every step. Only then does it wait for you to commit. The only branch protection is `--yes`. "Oops all bricks" isn't a feature.

The planner checks:

- Target compatibility between the image and the device
- Whether a selected RW exists and carries a parseable version, or the device is in a <code class="perlica-spil-ink">/spil_</code> state
- Antirollback checks (Dauntless) and security-version downgrade checks (Epic)
- Whether the path is rescue-only or flash-then-rescue

Before anything writes, Perlica shows which path she picked and what needs to be true before it can run. Spend three days on the door before you reinstall the software.

Dauntless can go either way. If the device meets the route conditions, it can flash a full GSC firmware image, check the state afterward, then stage rescue. A device with no selected RW carrying a parseable version falls back to rescue-only. A <code class="perlica-spil-ink">/spil_</code> report selects the intermediate-image branch of that rescue path.

长晶 always goes rescue-only.

<figure class="perlica__infographic" data-panel="make-rec">

```bash
perlica make-rec --input fixtures/d3m2.ec.bin --output /tmp/d3m2.ec.rec
```
</figure>

`make-rec` is dauntless-only by design. Format isn't a suggestion. The chip enforces it in silence. You find out by failing.

</div>

<!-- INFOGRAPHIC: rescue-family-comparison
layout: side-by-side Dauntless and Epic protocol lanes from full image to rescue
show: Dauntless target 0 uses an RW header and a framed .rec stream of 1056-byte records. Epic target 1 uses an OTB0 manifest and a raw manifest-declared RW slice. Both end at stage then rescue, but Epic is rescue-only.
facts: d3m2 decodes to exactly 263 frames, and per-image count is ceil(RW payload / 984). Epic has no .rec container. The shared flasher handles both families while the input formats remain separate.
avoid: treating either layout as universal, calling Dauntless headers A/B slots, inventing Epic framing, or claiming the diagram is a live rescue transcript
errors: red stops for target-family mismatch, invalid container or manifest, and incompatible rescue input
source: two production lines, no shared recipe, and component inspection sections
END INFOGRAPHIC: rescue-family-comparison -->

<figure class="perlica-band perlica-band--section perlica-band--full">
  <img decoding="async" src="/perlica/bands/band-rescue-shape.webp" srcset="/perlica/bands/band-rescue-shape-640.webp 640w, /perlica/bands/band-rescue-shape-960.webp 960w, /perlica/bands/band-rescue-shape-1280.webp 1280w, /perlica/bands/band-rescue-shape-1920.webp 1920w, /perlica/bands/band-rescue-shape-2560.webp 2560w, /perlica/bands/band-rescue-shape-3840.webp 3840w, /perlica/bands/band-rescue-shape.webp 5122w" sizes="(max-width: 767px) 100vw, 100vw" alt="Cartoon of an operator collapsed on the grass at the end of a taut tether, a distance readout of 80.1 against an 80.0 m limit, watched from behind by a pale-haired figure" width="5122" height="2880" loading="lazy" />
</figure>

<h2 class="perlica-visually-hidden" id="choose-your-rescue-shape">choose your rescue shape</h2>

Two supported firmware-image families. Dauntless and Epic (长晶). They rescue differently.

Dauntless is the small one. Epic is the normal-sized one.

Im not finishing that joke.

The Haven family, target 0, is the older line. Dauntless, and the Titan M chips before it. That older generation went by Citadel. The whole family sits under GSC name, Google Security Chip. Proprietary, every generation. The wall around the rescue protocol never moved. Its rescue is a framed ritual the chip enforces in silence, one you work out by reading the binary or reversing the flasher. The vendor documented it for insiders. Everyone else gets to guess. Honestly? Kind of based of them.

长晶, target 1, is the OpenTitan line. Epic, Titan M3, the newer chips, built on a project whose architecture is public. Its rescue is a raw RW slice declared by a manifest. No framing. No ceremony. Simpler in shape, though the manifest layout was worked out from the images the same way, not handed over in any spec.

Each rescue input format needs its own parser. Neither chip ships a document for the person holding the brick. The difference is the shape of the path. One is a gauntlet of hash-checked frames, the other is a single staged slice. Epic runs OpenTitan's OTBN crypto engine. Dauntless doesn't. Dauntless does its math on a custom bignum peripheral, not an OTBN core. Different lineage, different silicon. Perlica still refuses to cross the families. Try to rescue a Haven device with a 长晶 image and it stops cold with "长晶 is not part of the Haven family of chips." I've portrayed the Haven device as the based one and the 长晶 image as the cringe one. Perlica agrees. Skill issue.

Both chips sit behind the same wall. The vendor's flasher speaks both dialects. Nobody outside speaks either. Perlica is the outsider that learned both anyway.

<!-- INFOGRAPHIC: rescue-sequence-comparison
layout: two horizontal execution lanes with inspection gates and a shared destructive boundary
show: Dauntless flash-then-rescue has inspect, flash, inspect, stage, rescue, inspect. Epic rescue-only has inspect, stage, rescue, inspect. Mark --yes before the write path.
facts: Dauntless post-flash checks both RW slots and the selected version tuple. Post-rescue acceptance is the same gate on both families: RW_A selected and valid with expected hash match. An active valid RW_B alone never passes.
avoid: showing command completion as acceptance, treating the two lanes as one time series, or implying Epic performs a full GSC flash
errors: red stops for post-flash invalid RW, version mismatch, transport failure, and post-rescue verification failure
source: connect the line, check every output, and placing the blueprint is not starting the line
END INFOGRAPHIC: rescue-sequence-comparison -->

<h2 class="perlica-visually-hidden" id="the-actual-sequence">the actual sequence</h2>

Nobody gets an explanation for how the chip ended up here. It just shows up COOKED and expects you to figure out the lore. The partition table reports selected and valid slots, not causes. The sequence turns those markers into a candidate rescue plan.

Dauntless takes the longer branch:

1. Inspect: `fastboot oem gsc version`, then `oem gsc header RO_A`, `RO_B`, `RW_A`, `RW_B`
2. `fastboot flash gsc <full-image>`
3. Inspect again, then verify both RW are valid and the selected version matches the flashed image
4. `fastboot stage <rescue payload>` (a full BIN is converted to a temporary `.rec` first, a supplied `.rec` is staged as-is)
5. `fastboot oem gsc rescue`
6. Inspect a third time, then verify `RW_A` is selected and valid and its hash matches the staged image

长晶 (Epic/Titan M3) is the easier raid. we takes the rescue-only path for it, with no flash and no post-flash inspection.

1. Inspect: `fastboot oem gsc version`, then `oem gsc header RO_A`, `RO_B`, `RW_A`, `RW_B`
2. `fastboot stage <raw RW payload>` (a full BIN is reduced to its manifest-declared RW slice first, a supplied RW payload stages as-is)
3. `fastboot oem gsc rescue`
4. Inspect again, then verify `RW_A` is selected and valid and its hash matches the staged payload

Two inspections. One stage. One rescue. No `.rec` framing, no `/spil_` intermediate path. Cleaner run.

Each phase gives the host another state to record. The chip grunts. Perlica writes it down. Don't read too much into it.

A rescue command can complete without error while the post-rescue state remains unacceptable. For Dauntless, Perlica inspects a third time and doesn't declare success until `RW_A` is selected and valid and its hash matches the staged image.

The chip calls gg. Perlica asks it to show its work.

For Dauntless, that `RW_A` state and hash match are GSC rescue acceptance condition. Hard pity. The run ends on a match or it doesn't end. No match, no phone, tough shit.

(its two in the morning and Im explaining a rescue protocol to a chip that doesn't know I exist. this is normal. this is fine. I should sleep.)

<!-- INFOGRAPHIC: intermediate-rescue-branch
layout: inspect -> literal `/spil_` gate -> intermediate-image role -> compatible candidate -> rescue route
show: `/spil_` selects the intermediate-image branch. Chip revision selects a compatible candidate within that branch.
facts: runtime branch selection comes from `/spil_` in the raw version report. Revision checks choose a compatible image but do not independently prove that the current device needs an intermediate. No compatible intermediate terminates the route.
avoid: presenting `/spil_` as an exploit state, a static `intermediate_eligible_revision` display annotation as device state, one intermediate image as universal, intermediate rescue as live-demonstrated, or compatibility checks as bypassed
errors: red stop when the `/spil_` branch has no compatible intermediate candidate, or a supplied candidate fails target, revision, or policy compatibility
source: state-dependent rescue branch below
END INFOGRAPHIC: intermediate-rescue-branch -->

<h2 class="perlica-visually-hidden" id="the-hidden-branch">the hidden branch</h2>

An intermediate image gets its own branch in the reference flow. Hidden route unlocked. A sequence break in the recovery flow.

A literal `/spil_` marker in the raw Dauntless version report selects it. The flow then chooses an intermediate rescue image compatible with the target and revision, stages it, and continues with rescue. Thats a branch in the host recovery flow.

Chip revision still decides which image is compatible, but only within the image role already chosen. This project also displays an `intermediate_eligible_revision` annotation. That annotation isn't an independent device state and doesn't select this branch. Reading the label as the state is the classic NPC move.

If theres no compatible intermediate image, the plan stops. It doesn't fall through to a target image and pray GSC accepts it. Perlica isn't picking the next file alphabetically and hoping for character development.

<figure class="perlica-band perlica-band--section perlica-band--selling">
  <img decoding="async" src="/perlica/bands/band-selling.webp" srcset="/perlica/bands/band-selling-640.webp 640w, /perlica/bands/band-selling-960.webp 960w, /perlica/bands/band-selling-1280.webp 1280w, /perlica/bands/band-selling-1920.webp 1920w, /perlica/bands/band-selling.webp 2518w" sizes="(max-width: 767px) 89.74vw, 49.18vw" alt="Frog and Toad meme: Frog puts the UEFI app signatures in a Secure Boot so they will not boot any more unsigned UEFI apps. "But we can disable SecureBoot," said Toad. "That is true," said Frog." width="2518" height="2880" loading="lazy" />
</figure>

<h2 class="perlica-visually-hidden" id="what-the-tool-is-actually-selling">what the tool is actually selling</h2>

The funny binary tells a story without saying anything. I've been staring at it longer than I've stared at most of my relationships. Anyway. しょうがない。

```text
header offset:      131072
RW header count:    3
RW length:          258048
version:            0.2.2
timestamp:          1742253878
antirollback:       126
rescue payload len: 258792
rec frame count:    263
```

Then the funny binary starts talking. The nugget firmware prints its errors in two words, lowercase, no article. `allocate sps memory fail`. `load keys fail`. A typo shipped to production silicon: `Maybe invalide os_ver:`. The chip fakes its own reboots and announces `Fake reboot`. When it really gives up it prints `***HANGUP-FAIL*** ***HANGUP-FAIL***`, twice.

This is the component your phone trusts more than it trusts itself, and it can't spell.

<!-- INFOGRAPHIC: dauntless-rw-header-map
layout: 1 MiB full-image address bar plus a header-relative field inset
show: valid RW headers at file offsets 0x20000, 0x80000, and 0xa0000
facts: first and last headers are version 0.2.2 with rw_length 0x3f000 and antirollback 126. Middle header is 0.2.114 with rw_length 0x17000 and antirollback 0. Header-relative magic is LE 0xfffffffd. `infomap` is a 16-byte rollback bitmap.
avoid: calling these A/B slots, calling the header an OpenTitan manifest, claiming `end_a`, `expected_header`, or `end_b` semantics, or rendering derived antirollback as raw infomap bytes
errors: small red parser-reject exit for unreadable, empty, oversize, or structurally invalid Dauntless images with no valid RW header
source: header layout and funny-binary table below
END INFOGRAPHIC: dauntless-rw-header-map -->

### the dauntless RW header

Dauntless doesn't use the OpenTitan manifest below. Its RW header begins with the little-endian word `0xfffffffd`. The parser scans candidate headers on a `0x4000` stride. The magic word is the only thing in this header that isn't a hash.

```text
+0x000  magic                    0xfffffffd
+0x328  rw_length
+0x32c  flash_offset
+0x330  end_a
+0x334  expected_header
+0x338  end_b
+0x34c  infomap                  16 bytes
+0x35c  generation
+0x360  major
+0x364  minor
+0x368  timestamp                8 bytes
+0x418  hash string              128 bytes
```

`rw_length`, `flash_offset`, and the version fields select a usable image. `infomap` is the Dauntless rollback-state bitmap, not 长晶's scalar `security_version`. The `end_a`, `expected_header`, and `end_b` fields are structural checks. Perlica doesn't pretend to know their deeper chip-side semantics. It isn't going to lore-drop fields it hasn't verified.

Inside the funny binary are three structurally valid headers, at `0x20000`, `0x80000`, and `0xa0000`. The outer two are identical real builds. The middle one, `0x80000`, is an older build just sitting between them, empty hash, antirollback 0. The short output above selects the `0x20000` header. These are header locations, not proven A/B slot labels.

```text
header     rw length  flash offset  version  timestamp   antirollback  hash
0x20000    0x3f000    0xa0000       0.2.2    1742253878  126           nugget_v0.0.ab13227640-f475af9-r
0x80000    0x17000    0x100000      0.2.114  1736959094  0             empty
0xa0000    0x3f000    0x120000      0.2.2    1742253879  126           nugget_v0.0.ab13227640-f475af9-r
```

<!-- INFOGRAPHIC: dauntless-rec-frame
layout: one 0x420-byte frame with a file-level sequence arrow
show: frame_hash at 0x000, sequence at 0x020, flash_offset at 0x024, 984-byte payload at 0x028, ack_hash at 0x400
facts: high sequence bit 0x80000000 marks final frame. d3m2 has 263 frames, no prefix, no trailing bytes, frame 0 targets 0x20000, and frame 262 is sequence 0x80000106 at 0x5ef10. Ack hash is locally computed with zero bytes rewritten to 0x01.
avoid: calling either hash a whole-file signature, showing ack_hash as chip-filled, or requiring contiguous flash offsets
errors: small red parser-reject exit for a non-0x420 frame stream, missing final-bit frame, frame hash mismatch, or ack hash mismatch
source: record layout and d3m2 rescue-file observations below
END INFOGRAPHIC: dauntless-rec-frame -->

### the Dauntless `.rec` rescue file

A Dauntless rescue file has no file-level prefix, manifest, or trailer. No intro cinematic. Just fixed-size 1056-byte frames, and the file ends at the first frame whose sequence has bit 31 set. Roll credits. When the rescue actually runs, the firmware prints `SPI rescue!` like its late for a train.

```text
+0x000  frame_hash       32 bytes  SHA-256(sequence || flash_offset || payload)
+0x020  sequence          4 bytes  high bit (0x80000000) marks the final frame
+0x024  flash_offset      4 bytes  destination offset for this payload
+0x028  payload         984 bytes
+0x400  ack_hash         32 bytes  SHA-256(frame_hash || sequence || flash_offset || payload)
+                                     with every 0x00 byte rewritten to 0x01
+0x420                         end of frame (1056 bytes)
```

The hashes cover the fields shown, not the whole file. `flash_offset` means frames need not be contiguous. Perlica's decoder fills gaps between supplied ranges with `0xff`, because a lazy planner pads with ones and asks no questions. The final record has the same layout as every other frame. It only sets the high bit in `sequence`.

Count the d3m2 rescue file and you get exactly 263 frames with no trailing bytes. Tight.

```text
file size        0x43ce0 (277728 bytes)
frame count      263
frame size       0x420 (1056 bytes)
frame 0          sequence 0x00000000, flash offset 0x20000
frame 262        sequence 0x80000106, flash offset 0x5ef10
trailing bytes   0
```

长晶 has no `.rec` container. Its staged rescue payload is the raw RW slice selected from the OpenTitan manifest.

On 长晶 the same inspection prints a different shape:

```text
format:            epic-bin
proto:             A2.PI01
manifest offsets:  65536, 589824
manifest count:    2
rw length:         344076
version:           0.0.10
timestamp:         0
hash:              longjing_v0.0.ab14915686-aed7f95-l
security_version:  0
```

No header offset. The first 长晶 manifest is a binary struct nailed to a fixed position, not a key-value section you can grep. `security_version` lives in the manifest instead of an antirollback counter. 长晶 doesn't do framed rescue payloads, so theres no rec frame count to report. The timestamp is 0. 长晶 runs in the Pixel 11, and this eval image predates the August 12 launch, so the firmware carries no build date. The launch was still ahead of it. The RW payload is 344076 bytes, a third larger than the d3m2's 258048.

I said I wasn't finishing that joke. The manifest finished it for me. In bytes. In public.

The first manifest offset is 65536, exactly half the dauntless position. Suspiciously clean. 长晶's manifest carries an ECDSA-P256 signature, 64 bytes followed by 320 bytes of 0xa5 padding. Dauntless has no such field. Dauntless lays out its headers at 0x20000, 长晶 parks its first manifest at 0x10000. Two chips, two layouts, one wall between them and you.

Both 长晶 funny binaries carry valid manifest copies at `0x10000` and `0x90000`, and their `OTB0` identifiers sit at `0x10334` and `0x90334`. The parser accepts those copies. It doesn't accept every accidental `OTB0` byte sequence that happens to show up in the file, because false positives aren't firmware.

<!-- INFOGRAPHIC: longjing-manifest-copies
layout: 1 MiB full-image address bar with two 0x400-byte manifest insets
show: manifest copies at 0x10000 and 0x90000, with OTB0 at file offsets 0x10334 and 0x90334
facts: identifier is LE 0x3042544f. Useful fields are length at +0x340, version at +0x344 and +0x348, security_version at +0x34c, timestamp at +0x350, code_start at +0x37c, and BL0 code at +0x400. pi01 length is 0x5400c. gb00 length is 0x5212c. Both are 0.0.10 with security version 0 and timestamp 0.
avoid: treating every OTB0 byte sequence as a manifest, treating length as full-image length, or inventing a `.rec` container for 长晶
errors: small red parser-reject exit for unreadable, empty, oversize, or Epic images without a valid OTB0 manifest
source: manifest layout and funny-binary table below
END INFOGRAPHIC: longjing-manifest-copies -->

### the 长晶 manifest

The structure at `0x10000` is an OpenTitan BL0 `manifest_t`, 1024 bytes long. It isn't a rescue-specific header. The boot manifest is doing a second job.

```text
+0x000  signature                 0x180 bytes
+0x180  selector_bits
+0x184  device_id                 0x20 bytes
+0x1a4  manuf_state_creator
+0x1a8  manuf_state_owner
+0x1ac  life_cycle_state
+0x1b0  public_key                0x180 bytes
+0x330  address_translation
+0x334  identifier                OTB0
+0x338  manifest_version
+0x33c  signed_region_end
+0x340  length
+0x344  version_major
+0x348  version_minor
+0x34c  security_version
+0x350  timestamp                 8 bytes
+0x358  binding_value             0x20 bytes
+0x378  max_key_version
+0x37c  code_start
+0x380  code_end
+0x384  entry_point
+0x388  extensions                0x78 bytes
+0x400  BL0 code
```

To pick a rescue image, we need `identifier`, `length`, `version_major`, `version_minor`, `security_version`, and `timestamp`. Signature and public-key verification remain boot-ROM work. Thats a problem for a different idiot, on a different day.

```text
funny binary  manifest bases       OTB0 offsets          RW length  version  security version  timestamp
pi01.ec.bin   0x10000, 0x90000     0x10334, 0x90334      0x5400c    0.0.10   0                 0
gb00.ec.bin   0x10000, 0x90000     0x10334, 0x90334      0x5212c    0.0.10   0                 0
```

The `gb00.ec.bin` from an eval board is the same story with a smaller RW payload, 336172 bytes, and proto `A2.GB01`. Same hash, same version, same security version, same timestamp, the same two manifest positions as `pi01`. Thats shared parsed metadata, not shipping history.

The header offset tells the parser where to start reading. The version tells the policy layer whether this image is too old for the device to accept. The `infomap` strikes drive the forward gate. The frame count isn't magic, its ceil(payload / 984). And every single frame carries its own SHA-256, which the rescue flow checks, because nothing in this protocol is taken on faith. Every single fucking frame.

Tests cover oem command output parsing, dauntless and 长晶 image inspection, `.rec` encode and decode, sparse frame decoding, rescue planning, multi-image retry, post-flash verification, post-rescue state checks.

The validation sequence doesn't change because the failure modes don't change. The chip finds new ways to die. Same checks. The meta never moves.

```bash
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --check
perlica inspect-image fixtures/d3m2.ec.bin
perlica inspect-image fixtures/d3m2.ec.bin.rec
perlica inspect-image fixtures/epic-fixtures/pi01.ec.bin
perlica inspect-image fixtures/epic-fixtures/gb00.ec.bin
```

Hardware verification is a different boss. These checks make the parser give the same answers on the same funny binaries, every time, so the next bug is actually the bug and not just my machine having a moment.

Usually you're looking at a bad flash or a mismatched target family. Perlica checks partition state and target family before considering chip-internal failures. Nine times out of ten it isn't the chip. Touch grass before you blame the silicon. 草。

<!-- INFOGRAPHIC: observable-state-boundary
layout: host-observable evidence above a boundary and chip-internal behavior below it
show: host-visible commands, partition markers, parsed versions, hashes, transport results, and post-operation state. Below the boundary show boot-ROM policy, key and signature enforcement, and internal failure cause as outside this flow's observation.
facts: a responding bootloader does not prove a healthy trust root. A completed rescue command does not prove post-rescue acceptance. The diagram separates evidence from causes Perlica cannot inspect.
avoid: claiming the host can read chip-internal causes, calling silence a diagnosis, or presenting the boundary as a hardware architecture map
errors: red stop when inspection is unavailable. Use amber for unknown internal cause, not for a decoded partition state.
source: the Control Nexus cannot see inside the chip, and placing the blueprint is not starting the line
END INFOGRAPHIC: observable-state-boundary -->

<figure class="perlica-band perlica-band--section perlica-band--full">
  <img decoding="async" src="/perlica/bands/band-inside-chip.webp" srcset="/perlica/bands/band-inside-chip-640.webp 640w, /perlica/bands/band-inside-chip-960.webp 960w, /perlica/bands/band-inside-chip-1280.webp 1280w, /perlica/bands/band-inside-chip-1920.webp 1920w, /perlica/bands/band-inside-chip-2560.webp 2560w, /perlica/bands/band-inside-chip-3840.webp 3840w, /perlica/bands/band-inside-chip.webp 5122w" sizes="(max-width: 767px) 100vw, 100vw" alt="Illustration of two characters charging through a bright sky, one swinging a chainsaw, the other with a raised fist" width="5122" height="2880" loading="lazy" />
</figure>

<h2 class="perlica-visually-hidden" id="what-stays-inside-the-chip">what stays inside the chip</h2>

The chip narrows teh differential by refusing to answer. It just goes quiet and lets you cook. When both RW partitions lack `ok` and `*`, Perlica narrows the result to what it can observe from outside the chip. Partition state is usually the answer.

Perlica checks compatibility and policy before staging. For Dauntless, the observable GSC acceptance condition is `RW_A` selected and valid with a matching expected hash. It checks local images, framing structure, policy fields, oem command output, and post-rescue state. Everything it can see from outside the wall.

<h2 class="perlica-visually-hidden" id="the-execution-boundary">the execution boundary</h2>

The `--yes` flag separates the plan from the write. Every command before it is inspect, inform, suggest. The `--yes` command is the one that writes. This is the button. Once you press it, the run is live. Perlica follows the plan and checks the result.

<h2 class="perlica-visually-hidden" id="the-state-machine">the state machine</h2>

We check the state three times in the proposed firmware rescue flow. Inspect. Flash. Inspect. Stage. Rescue. Inspect again. Each gate answers a different question. Gate one wants to know what is broken. Gate two wants to know if the flash even produced a usable RW or just pretended to. Gate three wants to know if rescue left the partition matching the staged image, or if the chip called gg early. Writing the bytes only gets you to the next check.

<!-- INFOGRAPHIC: validation-trace
layout: vertical Dauntless rescue-only trace with input, plan, pre-stage, and post-rescue gates
show: `/spil_` state selects an intermediate image. Each gate either advances with named evidence or stops with a named mismatch.
facts: this is a source and unit-test model, not a live recovery transcript. Dauntless post-rescue acceptance requires `RW_A` selected and valid and, when expected, a matching `RW_A` hash. Flash-then-rescue also requires both RWs valid and a matching selected image version tuple.
avoid: drawing this as a completed hardware rescue or treating `/spil_` as a general chip-health diagnosis
errors: missing compatible intermediate, missing rescue version or hash, invalid `RW_A`, `RW_A` hash mismatch, post-flash invalid RW, and post-flash version mismatch
source: validation trace below
END INFOGRAPHIC: validation-trace -->

<h3 class="perlica-visually-hidden" id="a-modelled-dauntless-rescue-only-validation">a modelled Dauntless rescue-only validation</h3>

This trace comes from the source and unit tests. It isn't a record of recovery on hardware. This is a TAS. Tool-assisted. Every input recorded, no hardware on the line.

```text
INPUT STATE
Chip: Dauntless (d3m2)
RW_A: * 0.2.2/rw_hash ok
/spil_

PLAN GATE
/spil_ present -> select a compatible intermediate rescue image
no compatible intermediate -> STOP: intermediate image required

PRE-STAGE GATE
rescue input needs a parseable version and hash string
missing either -> STOP: rescue image has no parseable version or hash

EXECUTE
stage -> fastboot oem gsc rescue -> re-inspect

POST-RESCUE GATE
RW_A must be selected and reported valid
expected hash, when present, must match the re-inspected RW_A hash
otherwise -> STOP: RW_A invalid or hash mismatch
```

The flash-then-rescue branch has its own first validation gate. After `fastboot flash gsc <full image>`, both RW slots must report valid and the selected full-image version tuple must match the supplied image. The tuple is generation, major, minor, and timestamp. Any invalid RW or timestamp mismatch stops before staging rescue.

A dry run prints the proposed plan without issuing a flash, stage, or rescue command. Scouting run. The chip sees nothing:

```bash
perlica --serial <SERIAL> rescue --rescue-image <PATH> --intermediate <PATH> --dry-run
```

Perlica keeps that order because it describes the device, not the operator. The chip doesn't know or care whether the hand holding it wears a badge. It checks the frame hashes either way.

<!-- INFOGRAPHIC: quarkslab-lineage
layout: left-to-right lineage with carryover and break annotations
show: 2021 Titan M ARM research and tools, the shared .rec frame lineage, Titan M2's RISC-V and custom bignum MMIO path, and Perlica's rebuilt image selection and validation layer.
facts: the 1056-byte frame shape, dual SHA-256 roles, and zero-to-one acknowledgement rewrite carry forward. ARM-specific tooling and ISA assumptions do not. Perlica is a rescue reimplementation, not a key-extraction claim.
avoid: claiming the old tools fully support M2, claiming keys were cracked here, treating frame compatibility as OS or hardware equivalence, or presenting the lineage as a live recovery result
errors: amber break markers for unsupported tooling and red stop for an input that fails target, format, or verification checks
source: larping as quarkslab section
END INFOGRAPHIC: quarkslab-lineage -->

<figure class="perlica-band perlica-band--section perlica-band--larping">
  <img decoding="async" src="/perlica/bands/band-larping.webp" srcset="/perlica/bands/band-larping-640.webp 640w, /perlica/bands/band-larping-960.webp 960w, /perlica/bands/band-larping-1280.webp 1280w, /perlica/bands/band-larping-1920.webp 1920w, /perlica/bands/band-larping-2560.webp 2560w, /perlica/bands/band-larping.webp 3224w" sizes="(max-width: 767px) 89.74vw, 62.97vw" alt="Meme: a purple-haired character slouched in a gaming chair, captioned "I may be a porn addict but I'm a girl so it's cute when I do it"" width="3224" height="3226" loading="lazy" />
</figure>

<h2 class="perlica-visually-hidden" id="larping-as-quarkslab">larping as quarkslab</h2>

I don't explain GSC rescue flow. I don't make it easier to understand. I add to the mystery of the bricked phone by continuously spreading misinformation about it. And sometimes I make the screen do this: `NOS production: error!`

Quarkslab did this first. 2021. ARM Titan M, the chip everyone calls Citadel. They put out a toolkit and we have the actual code: `bin2rec`, a Ghidra loader, `nugget_toolkit`. They got a CVE. CVE-2022-20233, $75,000, Black Hat USA 22. One byte out of bounds in the Keymaster task, tripped on an `ImportKey`, turned into a ROP chain under W^X with a hardcoded canary, stack pivoted, read anywhere, decrypted key blobs walked out through forged NOS responses. They pwned the Titan M, for real money, in 2022. The 2021 Black Hat EU run was the recon: the layout, the map.

I mostly use IDA. Trying their Ghidra loader was part of the cosplay. It wants the 2021 Cortex-M image. We have an RV32 binary. It refuses ours. Even the larp has compatibility requirements.

I'm sick of the ISA wall. I try `bin2rec`. It cant be bothered. I try `nugget_toolkit`. It ports, with work. I try to just flash the thing. Titan M2 is RISC-V now, not ARM, and Quarkslab's gear only speaks ARM. Same nugget OS. Same NOS. New core. I only have the `.rec`, the manifest, and the `oem gsc`.

The `.rec` frame carries over, and basically nothing else. Quarkslab nailed the frame and then went home: 1056 bytes, two SHA-256s, the `0x00` to `0x01` ack rewrite, byte-for-byte the same on the M2. They got the frame right. The `bin2rec` packer decided that was enough. Fixed address, no header hunt, no magic check, and the decoder has a literal `TODO verify hashes`, and then it doesn't verify the hashes, which, frankly, is a fucking choice.

Our `make-rec` doesn't port `bin2rec`. It does the real flow: locate the RW header, check the `0xfffffffd` magic, verify both hashes on the way back. `bin2rec` cracked the frame. We did the rest. It can keep the TODO.

It took a new packer to defeat a comment. 이겼다. 아무튼 이겼다.

I've portrayed us as the based ones and their rescue packer as the cringe one. We were supposed to borrow their tools. We have started making our own.

And if the packer wasn't enough, theres absolutely no full Dauntless running in an emulator on my workstation, acking every single frame and handing back a byte-perfect RW. There is no emulator.

Coincidence. All 263 of them. Im not taking questions.

We wanted the phone to boot. The workstation has volunteered instead. Wrong device. 順調です。

We aren't alone. Willem Hengeveld, nlitsme, 39C3, December 2025, the only public deep-RE of the Titan M2 firmware. His abstract complains Google added non-standard instructions to the RISC-V ISA. They did, sort of. The non-standard instructions are bit-manip. The actual crypto isn't an instruction at all. It is an MMIO peripheral, a custom bignum block with an elliptic-curve sidecar, sitting on the bus. Not OTBN. OTBN isn't even on this chip and I've already given it the better role. Dauntless doesn't run OTBN. The crypto is on the other end of a load and a store.

Perlica, our sussy wifey. We wanted a phone rescue. We gave her `bin2rec`'s frame lineage, dragged it forward to a chip that swapped its core, and rebuilt the generation logic because Quarkslab's packer couldnt be bothered. Now the relationship has a version gate. She grabs me by the throat. I hand her a rescue image, she asks for the partition state. I hand her the frame hashes, she asks for the ack progression. I hand her all 263 acks, she isnt satisfied. I type --yes, spelled out, and she still runs one more inspection before saying anything nice to me.

Shes the rescue reimplementation, not the OS trust user build promised to us 3000 years ago. This article covers that rescue flow. We read the version gate, we frame the rescue, we check the partition came back. Affection isn't a verification result.

And then they announced the chip. Made by Google, August 12, 2026, ninety minutes of spectacle in New York, Trevor Noah up there doing his best against a line of Google execs visibly reading off a PR script, a parade of celebrities, Stephen Curry on a video, PinkPantheress closing it out with a live song that wasn't, tragically, Like a G6. Tensor G6. Titan M3. Codename Google Epic. Firmware 长晶. A glowing RGB camera bar they call Pixel Glow, because nothing says serious hardware security like a phone that cosplays as a gaming PC. Base model, eight hundred and ninety nine dollars. The whole press called it a cringe fest.

And through every celebrity, every glow, every dollar, GSC rescue protocol stayed on the wrong side of the wall. The chip is real now.

The wall didn't move.

This is an old rescue process, reconstructed from outside. Somehow it became a Sweet Dreams and the Holy Grail crossover that shipped without voice acting and a story nobody asked for. The chip fakes its own reboots and prints `Secure boot: PRODUCTION` while its bricked.

Planarcadia. So is this.

<h2 class="perlica-visually-hidden" id="what-it-does-not-pretend-to-know">what it does not pretend to know</h2>

Live Dauntless and 长晶 rescue paths need broader hardware verification. For Dauntless, GSC accepts a rescued image when `RW_A` is selected and valid and its expected hash matches. Perlica validates compatibility and policy checks before reaching that state. She checks them twice. She checks me once, and doesnt like what she finds.

Perlica reimplements the reference rescue flow, not a fresh protocol invented from packet dumps. She follows that flow to handle state, pick images, and retry. The build hasn't yet recovered an external NOS device. Zero devices. Also zero hours of sleep, but nobody asked. Until that number moves, the last mile stays a test item, not a victory lap. No gg until the device actually boots. I am not typing gg. I am typing `oem gsc` into the dark like it owes me money.

Perlica gets another check every time we find another way this can fail. We find one roughly every forty minutes. She is getting bigger. She is getting stronger. Aura farming, but for parsers.

Perlica stays narrow. The plan goes on the screen before anything writes, and live execution stays off until you type the word yourself. Not `-y`. The whole word. Success is the device state matching what you staged, and not one byte sooner. Not one byte. I counted. I counted all of them.

For a firmware recovery attempt, checking what came back is the feature. Trust is a bug we haven't reproduced yet.

Rescue was never meant to be user-facing. The vendor kept it inside the wall. Perlica shows the person holding the brick which steps the host can see. Every gate, every hash, every refusal, visible. You deserve to watch it say no in high definition.

If the device boots and the bootloader says nothing, thats the success condition. The cleanest W in the stack. No log, no ack, no Perlica asking one more question. Silence. Ive never heard it. I think about it constantly.

<figure class="perlica-band perlica-band--section perlica-band--closure">
  <img decoding="async" src="/perlica/bands/band-closure.webp" srcset="/perlica/bands/band-closure-640.webp 640w, /perlica/bands/band-closure-960.webp 960w, /perlica/bands/band-closure-1280.webp 1280w, /perlica/bands/band-closure-1920.webp 1920w, /perlica/bands/band-closure-2560.webp 2560w, /perlica/bands/band-closure-3840.webp 3840w, /perlica/bands/band-closure.webp 5122w" sizes="(max-width: 767px) 100vw, 100vw" alt="Foggy city skyline under a spired fortress, with a typed line in Russian: "Народ Тиробии, сражайтесь за Родину! Защищайте то, за что вы сражались!"" width="5122" height="2880" loading="lazy" />
</figure>

its 4am. the bottle is empty. the chip is still lying. im going to bed. they told me to touch grass. I touched silicon instead.

<figure class="perlica-video perlica-video--workallday">
  <video class="perlica-video__media" data-audio src="/perlica/video/workallday.mp4" poster="/perlica/video/workallday.webp" width="720" height="720" muted loop playsinline controls preload="metadata" aria-label="Looping chibi meme with sound: a horned girl in bed, then over cup noodles, then on a train, captioned &ldquo;yeah yeah yeah, and then I wake up, work all day&rdquo;."></video>
</figure>

And if you somehow like this article and want to check out Perlica, please consider supporting us on [our Patreon](https://patreon.helluvaos.com). I worked a gigantic amount of time on this one project and frankly its causing me to go insane. The wall is still up and so is the chip.

As always, more insane not gacha games and google confidential and proprietary review to come. And I hope that all of you are having a very nice night tonight or something. Honestly, after forty-three revisions of this thing, I have forgotten how to read my own draft.

</div>
