
In our last article we turned a JPEG XL decoder into an artificial CPU and used it to build the first JXL SHA-256 hashquine. This time we did it with HEIC. It was much harder. The JXL decoder hands you a decision tree and tagged 31-bit words; HEVC hands you pixels and motion vectors, so we ended up writing a compiler from scratch instead of patching an encoder.
The exact 536,870,946-byte file has this SHA-256:
802cc6d654ad15aafbb1ca5db4824292cc39fd72cf23fd80e1230d90679b9ff1
Decode the final image in that same file with stock Apple ImageIO and its pixels spell:
802cc6d654ad15aa fbb1ca5db4824292 cc39fd72cf23fd80 e1230d90679b9ff1
The two values are identical, and the characters come from the decode itself: the HEVC decoder reconstructs a 564-picture computation that runs the SHA-256 message schedule and all 64 compression rounds, does the feed-forward, converts the digest bits to hex, evaluates a font ROM, and draws the result in the last picture. Our compiler wrote the file; Apple's stock decoder runs it.
| file | sha256_hashquine.heic, 536,870,946 bytes (2^29 + 34) |
| container | HEIF image sequence (msf1 brand), one hvc1 track |
| coding | monochrome (4:0:0) 8-bit HEVC, Range Extensions profile |
| pictures | 564, each 16,384 x 6,976 luma samples |
| prediction | one intra (I) picture, then 563 inter-predicted P pictures |
| output | picture 563 (zero-based) displays the file's SHA-256 in a 3x5 hex font |
Viewing note. Apple ImageIO reports all 564 pictures and correctly decodes picture 563, the final zero-based index shown above. Some photo-library UIs choose picture 0 as an image sequence’s poster frame; that picture is intentionally a mostly gray computation surface. The hashquine claim and verification concern the final decoded picture.
Image decoders are interpreters for small languages
A weird machine is what you get when a system's ordinary state transitions turn out to be programmable in a way the designers never intended. A parser, decompressor, or image decoder already reads a language and updates state by rules the input selects. Given logic, storage, and ordering, a file can describe a computation instead of just content — and the computation can ride on documented behavior alone.
The best-known example is FORCEDENTRY, NSO Group’s zero-click iMessage exploit. In Project Zero’s words: “Using over 70,000 segment commands defining logical bit operations, they define a small computer architecture with features such as registers and a full 64-bit adder and comparator.” Their machine ran on JBIG2 image operations and needed a memory-corruption bug to start. Ours runs on documented decoder behavior, start to finish.
In HEVC, intra prediction moves data locally, residuals apply exact corrections, raster order enforces causality, PCM gives us an input aperture, inter prediction moves data between pictures, and the final luma samples are the display.
The machine is finite and fixed-program: one SHA-256 compression plus glyph rendering, about 150,000 two-input gates, needing only storage, Boolean logic, modular addition, routing, and time. Whether a decoder could host an unbounded universal computer is a different question from whether one legal file can make it evaluate a particular 64-round circuit. We answered the second.
HEIC, HEIF, and the decoder surface we used
HEIF (ISO/IEC 23008-12) is a container built on the ISO Base Media File Format, the same nested-box structure as an .mp4. “HEIC” is the extension for HEIF files whose pictures use HEVC coding. Apple shipped system support in iOS 11 and macOS High Sierra, made it the default iPhone photo format, and decodes it with ImageIO — our test target.
A HEIF file can hold its pictures in two ways, and the final artifact uses the second:
HEIF still image
HEIF image sequence (our file)
“Any physical order” is what makes the self-reference work, as we'll see.
HEVC reconstructs pictures from blocks. For our purposes a cell is one 8x8 luma coding unit containing four 4x4 prediction units. An intra-coded unit picks a predictor that reads already-reconstructed samples to its north, west, or northwest, then applies a coded residual. A P picture can instead use motion compensation to copy samples from an earlier picture:
We pin the decoder into an exactly predictable configuration: cu_transquant_bypass so residuals add losslessly, PCM for raw literals, deblocking and SAO filters off. Every output sample is then an exact integer function of its references, and our software model of the predictor matches the decoder bit for bit. Without that, “the pixels equal the hash” could never hold.
The north, west, and previous-picture references define a physical machine. A block can only read what's already decoded, so information flows East and South. Moving a value West costs one cell per row, and only where the Z-order exposes the top-right reference. Keeping a value alive costs a column for every row it survives. East and South are cheap, West is expensive, and state pays rent — those three facts shape everything that follows.
| 8x8 coding unit | configurable processing element |
| directional intra mode | local wire or sample selector |
| transform residual | constant, correction, or gate term |
| north/west availability | causal timing rule |
| raster reconstruction | clock and instruction order |
| PCM coding unit | literal-byte input aperture |
| P-picture motion vector | inter-picture load/store address |
| decoded luma plane | register fabric and display |
| HEIF sample table | program order independent of physical byte order |
The compiler emits the bitstream directly: VPS, SPS, PPS, slices, prediction modes, residual coefficients, motion vectors, PCM blocks, CABAC bins, and finally the HEIF box and sample tables. The decoder stays stock, the codec features stay standard, and the encoder is ours — commercial encoders choose modes to minimize error, and we needed to dictate every mode, residual, and motion vector.
A useful benchmark for AI coding systems
The project also turned out to be a good benchmark for coding agents. Across our attempts with GPT-6 Ultra in Codex and Claude Opus 5.5 in Claude Code, agents repeatedly declared the target impossible. Even after being shown working intermediate artifacts, several returned to the same failed single-picture assumptions.
The SHA-256 equations were the easy part. The real work was abandoning abstractions as the evidence killed them: pixels became signal tracks, tracks became a systolic array, values became virtual registers, and one picture became an image sequence with a decoder-supplied state bus. Agents that could generate code all day still failed there, because the bottleneck was revising a wrong hypothesis against physical evidence.
The PXF clue: let state flow through stages
One useful analogy came from Cisco’s Parallel eXpress Forwarding work. Their PXF network processor, nicknamed “Toaster”, put 16 packet CPUs in a 4x4 grid: packets moved along a row as a pipeline while the columns ran in parallel with a shifted phase. Cisco’s patents describe it as a systolic-array pipeline — each stage owns a register file and functional units, processes a context, and hands it to the next stage.
The older idea behind it is H. T. Kung’s “Why Systolic Architectures?”: map a computation onto regular local stages and keep data moving. That beat pretending we had a random-access CPU. It didn't solve the problem by itself, but it gave us the right questions:
- What is the packet context? Our 128-byte persistent SHA register bank.
- What is a stage? One bounded HEVC P picture.
- What are the bypass paths? Intra-prediction wires and inter-picture motion copies.
- Where do register dependencies live? In the compiler’s slot map and checkpoint object.
- How do we avoid a global crossbar? Place values locally, rename them, and insert bounded repack stages.
FPGA and ROM-exploitation experience helped for the same reason. The job felt less like writing software and more like closing timing on a fixed-function fabric. A smaller Boolean expression could still be physically worse if it added a long wire, burned a reference sample, or boxed another route out of the raster.
Why HEIC was much harder than JXL
JXL gave us generous primitives: a Meta-Adaptive tree with explicit branching, tagged 31-bit values, lookup-like leaves, dozens of state channels, and a strict channel/raster schedule. We compiled large finite mappings into one image and treated optional channels as wide state lanes.
HEVC gave us smaller, more physical pieces:
- reconstructed samples rather than wide tagged words;
- local north, west, and diagonal dependencies rather than arbitrary channel reads;
- fixed predictor geometry rather than a large decision tree;
- a strict no-future-reference rule within a picture;
- practical decoded-dimension limits in Apple ImageIO;
- reference-sample boundary effects that make an isolated primitive behave differently when composed;
- arbitrary byte values that do not survive every long intra-prediction path exactly.
The dimension limit is sharp and we measured it. We wrapped solid test pictures of increasing size in HEIF with a clap (clean aperture) crop so the output buffer stays small, and decoded them to memory through Core Graphics/ImageIO:
| 32,768 x 32,768 | 1,073,741,824 = 2^30 |
| 32,768 x 33,792 | 1,107,296,256 |
| 16,384 x 65,536 | 2^30 |
| 8,192 x 131,072 | 2^30 |
A single coded picture holds at most 2^30 samples, whatever its shape; without a crop, the output buffer fails first near 2^29. One flat picture of SHA-256 needs roughly ten times that.
So the compiler had to solve problems from digital design and backend compiler work: cell characterization, logic synthesis, register allocation, liveness, placement, routing, crossing, carry propagation, bank assignment, motion-vector reachability, and physical verification.
The first mental model that held up was a one-dimensional cellular automaton drawn as a space-time diagram. Columns are signal tracks, rows are time. Every coding unit reads legal north/west/northwest neighbors, applies a local transition, writes once. SHA-256 has fixed rates and data-independent control, so it also fits synchronous dataflow: the token count per round is known at compile time.
Three layout laws followed:
- Keep each live bit on a known vertical track for as long as possible.
- Make transport local and obey one uniform causal shear.
- Let carry move through time or through a dedicated local wavefront; do not repeatedly gather entire words across the canvas.
Finding the actual instruction set
We characterized each candidate cell by reconstructing every relevant input combination in a software model, and promoted only the cells that survived composition and Apple decoding.
The Boolean alphabet is narrow: 128 is zero, 129 is one. Gates come from the only nonlinearity HEVC prediction has — integer rounding and clipping. Take DC mode, which averages the reference samples:
dc = (sum_x T_x + sum_y L_y + N) >> (log2 N + 1)
Give a single 4x4 DC unit north references [128+a, 128+a, 128+b, 128] and west references [128+b, 128+c, 128+c, 128]:
dc = 128 + floor((2(a+b+c) + 4) / 8) = 128 + [a+b+c >= 2] = 128 + Maj(a,b,c)
DC’s rounding is a majority vote. Vertical mode 26 corrects its first column with pred[0][y] = T_0 + ((L_y - T_NW) >> 1); with T_0 = 129, L_y = 128, T_NW = 128 + a that gives 129 + (-a >> 1) = 128 + (1-a) — an exact NOT. The cell library records these directly:
// src/cells.hpp // NOT one 4x4 PU, mode 26 (or 10): the zero-angle edge correction // T0 + ((L[y]-TL)>>1) with T0=129, L=128, TL=128+a gives 129-a. // MAJ one 4x4 DC PU: north [a,a,b,128], west [b,c,c,128] -> uniform // 128+Maj(a,b,c) (DC's +4 rounding is the threshold)
One bit register lives at south sample x=3 of an 8x8 coding unit; horizontal packets use east samples y=4,5,6. That separation gave us the first decisive primitive: a real crossover.
// src/point_fabric.hpp
inline hevc::CU hold() { return hevc::nxn_cu({26,26,26,26}); }
inline hevc::CU cross() { return hevc::nxn_cu({ 2,18,34,11}); }
inline hevc::CU read() { return hevc::nxn_cu({27,18,34,34}); }
inline hevc::CU write() { return hevc::nxn_cu({27,27,16, 2}); }
inline hevc::CU gate(bool is_and, bool forward_result=false) {
std::array<int,64> residual{};
residual[31] = 1;
if (is_and) residual[30] = -1;
return hevc::nxn_cu(
{2,18,33,forward_result ? 2 : 11}, residual);
}An earlier exhaustive search concluded no single-cell crossover existed — because it silently assumed both signals had to share a sample lane. Give them separate lanes and the cell exists.
With cross, a vertical register survives while an independent horizontal operand passes through the same 8x8 unit. The fabric becomes a small coarse-grained reconfigurable array instead of a planar tangle of blocking wires.
The working primitive set became:
| hold | preserve the vertical bit |
| read | broadcast a stored bit into a horizontal lane |
| cross | carry vertical and horizontal signals through one cell |
| write | deposit a horizontal value into a vertical register |
| gate(and/or) | combine north and west inputs |
| XOR macro | six-row composition built from predictor and residual cells |
| packet lane | move up to three independent horizontal bits |
| point-left | bounded one-row westward register move |
We checked every primitive at its observable boundary samples, which caught several designs that worked in isolation and failed when tiled.
The first complete compiler, and the 11-billion-pixel wall
With the crossing cell we could compile the full SHA graph into one picture. The front end built a word-level DAG:
enum Op { INPUT, CONST, XOR, AND, OR, ADD };
struct Word {
int id;
int rotate = 0; // a view, not copied data
int shift = 0;
};
Word sigma(Word x, int a, int b, int c, bool small=false) {
return gate(XOR,
gate(XOR, rotate(x,a), rotate(x,b)),
small ? shift(x,c) : rotate(x,c));
}The key word is “view.” A rotate is a tag on a virtual value; the compiler changes the bit-to-track mapping when a consumer reads it rather than building a physically rotated copy. Same idea as pointer arithmetic or register renaming.
Addition used a local ripple-carry adder. For bit j, propagate and generate:
P_j = A_j XOR B_j, G_j = A_j AND B_j
then ripples:
C_{j+1} = G_j OR (P_j AND C_j), S_j = P_j XOR C_jThe fabric’s forward-result gate lets all 32 carry steps share one CU row as a horizontal wavefront. With operand prep and the XOR macros, one 32-bit add costs about 20 CU rows.
The full one-picture lowering worked in the reconstruction oracle: one round composed decode-exact with all 2,176 probed samples matching software SHA-256, and four chained rounds matched at all 7,808 probes. It was also hopelessly large:
width: 1,040 coding units height: 168,129 coding-unit rows pixels: 8,320 x 1,345,032 = 11.19 billion
| global compaction | 82,212 |
| operand copies | 70,432 |
| addition | 11,048 |
| XOR | 4,224 |
| AND / OR | 192 |
Nine percent of the height was arithmetic. The other 91 percent was moving bits around.
The allocator behaved like a register allocator with no spill memory: bump-allocate result blocks east, leave holes where values died, and when the frontier hit the edge, compact() walked every live register back west through a long staircase. An operand copy then gathered 32 independently routed bits into a new stride-four block. Peak liveness was about 832 bits — the 16-word schedule window, eight working words, and temporaries.
We tried interval-batched copies, first-fit hole reuse, uniform stride-four storage, alternate Ch/Maj forms, and carry-save experiments. They helped individual cases and changed nothing. The last measured version still needed 9.26 billion pixels, 8.6x the 2^30 ceiling.
The lesson: optimize the physical cost model. Algebraic expression count is the wrong target — replacing XORs with AND/OR nodes looks cheaper until each node pays for another 32-row gather, and carry-save gained nothing while delivery dominated. Placement had to be solved first.
Slices, folds, and the halfway-automated routing process
The point fabric naturally suggests bit slices: repeat a small local arrangement for bits 0 through 31. Within a slice, A[j], B[j], propagate, generate, and carry tracks sit side by side, and one intra-prediction row computes all 32 Boolean gates in parallel. The densest slice we found is a full adder in one 2x2 block of cells — carry runs straight East, sum drops straight South:
// src/chainadd.hpp // [ U0 (planar) xor_cell ] west (top row) = 127 + c_in // [ U17 flood2 ] north = 129 + P_j , 129 + G_j // east, all 8 rows = 129 + (G_j | (P_j & c_in)) (carry out, uniform) // south, flood2 col 0 = 128 + (P_j ^ c_in) (sum bit) // Planar with the -1 west level is AND; DC with equal levels is OR.
Composition is where slices bite. A carry rippling across the full width of the picture through a row of horizontal-copy cells destroys any value travelling South through that row. We fixed that three ways over the project: a diagonal adder whose carry never drives a full-width row, the crossing cell, and finally an architecture where state doesn't cross adders at all.
Folds happen when a logical path hits the physical edge, or when a carry or message path needs to re-enter a reusable region. They're expensive because HEVC reference availability is directional — a mathematically valid fold can ask for an upper-right sample that the decoder hasn't reached yet at that coding-tree boundary. The folds that work are staircases of shift cells placed only where the Z-order allows. For two-samples-per-bit bus words they're lossless at every distance we tested, and they let us build a bounded-width accumulator that stays 536 CUs wide no matter how many operands it adds.
We chained all this together with a halfway-automated process:
- Search: a primitive search engine enumerates about 107 decoder-proven cells and their small compositions, and classifies every edge sample as
128 + f(a,b,g)for a Booleanf. - Generate a candidate layout from live ranges, typed stage contracts, and preferred track positions; the router automatically inserts a verified format adapter whenever a producer’s port format differs from its consumer’s.
- Use the software reconstruction oracle to inspect every exported sample.
- Emit a small native HEVC control artifact.
- Decode it through FFmpeg and Apple ImageIO.
- Feed exact failures back into the placement constraints, and encode the hard-won rules as assertions, so an illegal layout throws instead of silently decoding wrong.
The compiler tracks occupancy at CU granularity, reference halos at sample granularity, and logical values at word and bit granularity. The router moves one register, a three-lane packet, or a packed byte. The allocator knows a location can be free for Boolean tracks and still unusable because a prediction reference would cross a boundary.
This closed one full round, then four, then longer schedule fragments. It also made the wall undeniable: even a perfect single-picture placement couldn't fit the transport and presentation work under Apple's accepted dimensions.
The breakthrough: the HEVC sequence is the time axis
The fix was to stop insisting the computer live in one picture.
HEIF can hold an HEVC image sequence, and HEVC already has a byte-exact transport engine between pictures: inter prediction. Each bounded P picture becomes one hardware stage, and motion compensation becomes the state bus.
Each picture imports a 128-byte register bank from its predecessor, runs one schedule, round, feed-forward, plane, glyph, or atlas operation, and exports the updated bank at known sample coordinates. If the next stage lacks a legal motion vector to those coordinates, the compiler inserts a repack picture that moves every byte a bounded distance toward its canonical slot.
The decoder already has an optimized motion-compensation engine and a decoded-picture buffer, so we stopped hand-building hundreds of intra-picture copies and let it do the work. One ordinary inter-coded unit asks the decoder to fetch a sample from the previous picture. Motion vectors are quarter-sample units, so a byte at column x_src reaches column x_dst with mv_x = 4 * (x_src - x_dst):
// src/byte_program.hpp: importing the bank at the top of a picture
int mvx = 4 * (source_samples[i].x - (8*x + 7));
int mvy = 4 * (source_samples[i].y - 7);
if (mvx < -32768 || mvx > 32767 || mvy < -32768 || mvy > 32767)
throw std::runtime_error("byte resume motion-vector range");
set(x, 0, hevc::inter_cu(mvx, mvy)); // fetch from the previous picture
set(x, 1, pointfabric::main_read());
set(x, 2, byteregister::read());
set(x, 3, byteregister::hold());The source comment states the rule that made the design work: “Keep byte tracks fixed within a picture; frame-to-frame motion performs the only byte-safe repacking.”
The final logical pipeline contains:
| source I picture | 1 |
| initial relay | 1 |
| schedule updates W16..W63 | 48 |
| SHA compression rounds | 64 |
| feed-forward words | 8 |
| digest nibble bit-planes | 8 |
| font rows | 10 |
| atlas rows | 20 |
| final presentation | 1 |
| logical stages | 161 |
Bounded repacking adds 403 transport pictures: 564 total. Every picture is 2,048x872 coding units (16,384x6,976 luma pixels, about 1.14 x 10^8 samples) — a tenth of Apple's per-picture limit. The file is large, but no single picture breaks the acceptance geometry.
You can read the schedule straight off the sample-size table. The 158 compute stages are the 158 pictures coded at 2.1–3.4 MB; the relay and 403 repack pictures are the 404 pictures of 100–200 KB; the source I picture (30,790 bytes) and presentation picture (81,704 bytes) are the two smallest. In the first 16 rounds, three or four transport pictures precede each round; once the message schedule starts, the rhythm is four transports, a schedule stage, two transports, a round.
Here's one compute stage decoded: picture 4, the first SHA round, one dot per 8x8 CU. Dark gray is idle (every sample 128), green is active Boolean logic (129), blue and orange are byte lanes below and above 128, black and white are 0 and 255.

What the compiler actually contains
1. SHA-256 front end
The front end implements FIPS 180-4. At round t:
T1 = h + Sigma1(e) + Ch(e,f,g) + K_t + W_t (mod 2^32) T2 = Sigma0(a) + Maj(a,b,c) (mod 2^32) a' = T1 + T2, e' = d + T1
The schedule ring updates in place:
W_t = sigma1(W_{t-2}) + W_{t-7} + sigma0(W_{t-15}) + W_{t-16} (mod 2^32)2. Word IR and rotation views
The word IR has INPUT, CONST, XOR, AND, OR, and ADD. Rotates and shifts are edge annotations. Constant folding removes operations before physical lowering. Each small program has an independent evaluator, so the compiler can check every expected word against the placed circuit.
The round builder stays compact because the complexity lives in the lowerer:
auto hs = p.add(h, p.sigma(e, 6, 11, 25));
auto ch = p.gate(XOR, g, p.gate(AND, e, p.gate(XOR, f, g)));
auto t1 = p.add(p.add(p.add(hs, ch), p.constant(K[t])), w);
auto ne = p.add(d, t1);
auto maj = p.gate(OR, p.gate(AND,a,b),
p.gate(AND,c,p.gate(OR,a,b)));
auto t2 = p.add(p.sigma(a,2,13,22), maj);
p.outputs = { p.add(t1,t2), ne };Ch is g XOR (e AND (f XOR g)) and Maj is (a AND b) OR (c AND (a OR b)) — equivalent to the textbook forms, cheaper on this fabric.
3. Physical lowerer and router
The lowerer expands byte operands into 32 Boolean tracks, allocates temporary panels, places gates and adders, routes operands through crossover cells, packs result bits back into bytes, and records the exact output samples the next picture must import. It's occupancy-aware: every live bit has a column, every packed byte has a port, every route reserves its reference halo. Non-overlapping routes share a row by interval partitioning.
4. Persistent registers and virtual renaming
The register file is a 128-byte bank: 32 words. An 8-bit sample holds a whole byte, so a word needs four cells instead of 32. The byte register cells are small, and we checked every read, cross, and write contract over all 65,536 input pairs:
// src/byte_register.hpp
// Consumes N3; produces S3..S7 and E4..E6 equal to N3.
inline hevc::CU read(){return hevc::nxn_cu({26,10,26,10});}
// Preserves S3=N3 and independently passes W4/W5/W6 to E4/E5/E6.
inline hevc::CU cross(){return hevc::nxn_cu({2,18,34,18});}
// Requires W5=W6=H; writes H to S3/S7.
inline hevc::CU write(){return hevc::nxn_cu({26,26,14,10});}
// Independently preserves N3 at S3 and N7 at S7.
inline hevc::CU hold(){return hevc::nxn_cu({26,26,26,26});}- slots
0..7hold the working state; - slots
8..23hold the 16-word schedule ring; - slots
24..31retain the initial chaining state for feed-forward.
Only two state words are physically overwritten per round: new a goes into the slot that held h, new e into the slot that held d. The rest of the SHA state shift is pointer renaming, same as register renaming in a real CPU:
kernel(program, mapping, {state_slots[7], state_slots[3]});
state_slots = {
state_slots[7], state_slots[0], state_slots[1], state_slots[2],
state_slots[3], state_slots[4], state_slots[5], state_slots[6]
};The schedule works the same way: W[t] occupies slot 8 + (t mod 16), and once its last old use has passed, W[t+16] overwrites the slot. No physical 16-word shift.
5. Bytes to bits and back
Arbitrary bytes would break Boolean gates, so every stage starts by normalizing the bytes it needs into eight 128/129 point tracks (a 12-row panel) and ends by packing computed bits back into bytes (a 59-row panel). The packer does its arithmetic with the decoder's own predictors. A fixed “gain” cell computes
R(P) = floor((8P + 24*clip(P + floor(P/2), 0, 255) + 16) / 32)
After subtracting 127, eighteen applications map levels 1 and 2 to 1 and 255; subtract one, apply one more step, and you get exactly 0 and 255. Eight DC-average cells then fold the bits together, least significant first, from q = 0:
q <- ceil((q + 255*b_j) / 2) (j = 0..7) => q = ceil((255 * byte) / 256) = byte for every 0 <= byte <= 255
The decoder’s averaging is the bit-packing instruction.
6. Picture scheduler and motion-vector-bounded repacking
The picture scheduler wraps each small program in a Checkpoint and canonicalizes the bank before each compute picture. The repack planner picks unique target tracks near canonical columns while keeping every source-to-target displacement inside the motion-vector range.
for (int i = 0; i < 128; ++i) {
int want = 4 + i;
int lo = std::max(0, source[i] - 511);
int hi = std::min(width - 1, source[i] + 511);
target[i] = nearest_unused(want, lo, hi);
}A constrained assignment problem, solved as one. The compiler inserts as many bounded pictures as convergence needs and proves all 128 destinations distinct.
7. HEVC and HEIF backend
The backend writes monochrome HEVC directly: 32x32 coding-tree units, 8x8 minimum coding units, NxN prediction units, PCM where needed, and explicit L0 motion vectors in P slices, each referencing only the previous picture. Then it writes the HEIF image-sequence container with one sample per chunk and hvc1 sample entries.
The hvc1 detail was empirical and load-bearing. A semantically identical hev1 remux was rejected by the Apple path we target; keeping the hvc1 configuration contract made the sequence acceptable to ImageIO.
The self-reference construction
A hashquine sounds circular: the image must display a digest that depends on the bytes encoding the display. We sidestepped a fixed-point search with SHA-256’s chaining structure. SHA-256 is Merkle–Damgård: the digest depends only on the chaining state after the second-to-last block (the midstate) and on the last block — the same property behind length-extension attacks.
Let the final file be:
F = P || S || FE80
where:
Pis an already fixed prefix of exactly2^29 = 536,870,912bytes;Sis the 32-byte SHA-256 chaining state after hashingP;FE80is two ordinary bytes of the file;- the total file length is
536,870,946bytes.
|P| is a multiple of the 64-byte block size, so let H_P be the eight-word chaining state right after P:
S = BE32(H_P[0]) || ... || BE32(H_P[7])
An ordinary SHA-256 implementation reads the final 34 file bytes, then appends its own 0x80, zero fill, and 64-bit length. Its last compression block is:
S || FE 80 80 || 21 zero bytes || BE64(8|F|)
The decoder imports S as its initial chaining state and uses those same bytes as message words W0..W7. It hardwires W8 = 0xFE808000, zeroes W9..W13, and puts the file bit length in W14..W15. The file is larger than 512 MiB, so 8|F| = 4,294,967,568 = 2^32 + 0x110 overflows 32 bits — W14 = 0x00000001, W15 = 0x00000110, and the circuit's length input had to be widened to 64 bits. Both paths run exactly the same compression and feed-forward:
SHA256(F) = Compress(H_P, S || FE8080 || 21*00 || BE64(8|F|))
The container trick: physical file order and decode order are allowed to differ. The finalizer writes the P-picture program samples first in mdat, pads the physical prefix to exactly 2^29 bytes, and stores the source I-picture bytes last. The sample tables still name that source as logical sample zero, so the decoder visits it first. Hashing follows byte order; decoding follows the sample table. So the fixed prefix can contain almost the entire computer before the 32-byte value that initializes it.
The finalizer performs these steps:
- Build the complete sequence using a 32-byte placeholder.
- Reorder the physical sample payloads while preserving logical sample order in
stco/stsc/stsz. - Fill the prefix to exactly
2^29bytes. - Stream SHA-256 over that prefix and extract its chaining state.
- Check that substituting the state does not create a forbidden HEVC emulation-prevention pattern at the replacement boundary.
- Append the 32-byte state and
FE80. - Hash the complete file independently.
Once the prefix is fixed, the suffix follows deterministically — the finalizer computes it in one pass. In the delivered file the I picture's sample starts at byte offset 536,840,156 and runs to end of file, and its tail is the prefix midstate:
prefix 536870912 bytes, tail 34 bytes midstate : 3f4afacaa0d8b13afeffe5e47d93eb0b582721cf29ee35a5337f95162009a22b tail[0:32]: 3f4afacaa0d8b13afeffe5e47d93eb0b582721cf29ee35a5337f95162009a22b MATCH: file ends with its own prefix midstate
You can see it directly: the bottom-right row of luma samples in decoded picture 0 reads 3f 4a fa ca a0 d8 b1 3a ... — the midstate itself.
From eight words to 64 readable glyphs
First, eight digest-plane stages transpose four SHA words at a time. The four resulting 32-bit values hold the four nibble bits for 32 hex digits, which makes the next stage word-parallel: one Boolean operation evaluates the same font pixel for 32 different glyphs.
The font is a compact 3x5 design for 0–9 and a–f. For each of its 15 pixels the compiler uses a minimized sum-of-products over nibble bits a,b,c,d. The top-middle pixel, for instance, is lit for every digit except 4 (0100), so it compiles to a OR ~b OR c OR d. Four input bits selecting one of sixteen fixed glyphs is exactly where an ordered binary decision diagram perspective helps — modeling the whole SHA core as an OBDD would explode, but a four-bit font ROM is ideal.
for (int column = 0; column < 3; ++column) {
int pixel = 3 * font_row + column;
Word sum = p.constant(0);
for (const char* term : minimized_terms[pixel]) {
Word product = p.constant(~0u);
for (int k = 0; k < 4; ++k)
if (term[k] != '-')
product = p.gate(AND, product,
is_lower(term[k]) ? nibble[k] : not_nibble[k]);
sum = p.gate(OR, sum, product);
}
p.outputs.push_back(sum);
}The compiler writes the result into a 128x48-CU glyph atlas, committing rows bottom to top so no later route crosses a finalized row.
One late decoder discrepancy forced the last architectural change. Long intra-prediction paths preserved the 128/129 Boolean alphabet but mangled some arbitrary byte values under ImageIO — the software model kept them exact. Rather than fight it, the atlas stage uses inter prediction to copy the twelve arbitrary bytes it needs from the preceding picture into three disjoint normalization panels. A fast normalizer expands them to bit tracks, and a gain stage maps 128/129 to exact 0/255 samples. (A still image could have used an ICC color profile as a font ROM, but ImageIO ignores track ICC profiles on HEIF sequences, so here the font is computed.)
The presentation picture then lets the decoder enlarge the atlas. Each source font pixel gets one independently addressed motion-copy seed; horizontal intra mode 10 fills the rest of the first row, vertical mode 26 fills the remaining rows. One computed atlas pixel becomes a 128x128 visible square almost entirely through decoder-native prediction — the same principle as the rest of the project: when the decoder already has a correct, cheap primitive, compile to it instead of rebuilding it from gates.
Verification
We verified from both directions. The byte side:
$ shasum -a 256 sha256_hashquine.heic 802cc6d654ad15aafbb1ca5db4824292cc39fd72cf23fd80e1230d90679b9ff1
The decoder side, stock ImageIO on the final artifact:
file bytes: 536,870,946
ImageIO picture count: 564
final picture index: 563
decoded dimensions: 16,384 x 6,976
decoded glyphs: 64 of 64
decoded text: 802cc6d654ad15aafbb1ca5db4824292
cc39fd72cf23fd80e1230d90679b9ff1
cell mismatches: 0The verifier samples the center of every 3x5 glyph cell, maps the bitmap back through the fixed font table, and compares all 64 characters against the independently hashed file. The final picture's samples are exactly 0, 128, and 255, so the read-back is unambiguous. The container side checks out independently too: the file's last 34 bytes are the midstate of its first 2^29 bytes.
During compilation we also ran differential tests at every layer:
- word-IR evaluation against an independent SHA-256 reference;
- exhaustive truth tables for each primitive;
- boundary-sample checks for every coding unit;
- full-picture software reconstruction probes;
- FFmpeg decode controls;
- Apple ImageIO decode controls;
- final glyph OCR against the physical file digest.
The software model stayed useful, but Apple's decoder was the acceptance oracle. Wherever they disagreed, we redesigned around the shipped behavior.
What computer did we build?
The final machine is best described as a finite, spatially and temporally unrolled, decoder-hosted processor:
| program | prediction modes, residuals, motion vectors, and sample tables |
| ALU | AND/OR cells, six-row XOR, ripple-carry word adder |
| registers | 128/129 vertical tracks and packed-byte bank |
| virtual registers | Word rotation/shift views and state_slots |
| register allocator | liveness and occupancy maps in Lowerer |
| router | read/cross/write cells and three-lane packets |
| memory/bus | inter-picture motion compensation |
| clock | raster order within a picture; POC across pictures |
| control | compile-time fixed SHA schedule |
| ROM | SHA constants and minimized hexadecimal font functions |
| input | PCM source aperture near the physical end of the file |
| output | final monochrome luma picture |
Depending on the level, it looks like a CGRA, a systolic array, or a cellular automaton: a local CA at the bit level, a routed dataflow machine at the word level, a pipeline of state-transforming stages at the picture level, and a self-referential program with separated physical and logical order at the container level. Same machine, four compiler layers.
Security implications
This artifact is benign. It computes a fixed SHA-256 continuation and draws pixels using valid decoder operations — it touches no memory it shouldn't and runs entirely inside the normal decode path.
It does show why decoder semantics deserve attention beyond parser bugs. The FORCEDENTRY analysis proved standardized image operations can be composed into a logic-gate machine. A semantic computer can do deterministic work before, after, or alongside a memory-safety flaw — transforming embedded data, choosing among precomputed strategies, regularizing timing, or creating more predictable decoder state, all without executing injected native code.
A format-level machine still answers to modern mitigations. Arm MTE and Apple’s Memory Integrity Enforcement still protect the domains they cover, and a standards-conforming circuit has no authority to read arbitrary memory or forge a tag. The relevance is compositional: given a separate bug that provides an unsafe transition, a rich semantic machine can reduce the fragile post-corruption work needed to reach or stabilize it.
A few properties of this construction are worth flagging on their own:
- Image sequences are a quieter surface. Everything here lives in inter prediction and the HEIF sequence track boxes: paths that “image” handling reaches, but that receive far less scrutiny than the still-image path.
- What you see depends on which picture a viewer shows. Picture 0 of this file is a gray field; picture 563 carries the payload. Thumbnailers, content scanners, perceptual hashes, and moderation pipelines that look only at a poster frame can see something entirely different from a viewer that scrubs the sequence.
- Per-picture limits don’t bound total work. 564 pictures of 1.14 x 10^8 samples is about 6.4 x 10^10 decoded samples from one 512 MiB file.
- Provenance assumes inert data. Media that can evaluate a chosen circuit over its own bytes complicates schemes, from C2PA manifests to forensic deduplication, that treat an image as passive content.
The useful question moves past “is each operation safe?” to the harder one: “what computations become possible when thousands of safe operations are composed under attacker-controlled dataflow?”
Closing the loop
No single HEVC mode solved this. Each failure forced a better abstraction, and each abstraction unlocked the next step:
- orthogonal sample lanes turned an apparent planar wall into a crossover;
- rotation tags turned bit movement into pointer-like views;
- schedule and state maps turned physical shifts into register renaming;
- instrumentation revealed that movement, not SHA arithmetic, consumed the canvas;
- image sequences turned pictures into time steps;
- motion compensation became a byte-safe state bus;
- sample-table indirection separated physical hash order from decoder execution order;
- decoder-native replication scaled a tiny computed atlas into readable text.
The result is a legal HEIC image sequence containing a specialized computer. Stock ImageIO reconstructs it, runs the fixed SHA-256 program, and draws the file’s own digest as pixels.
Get the artifact and source
The 536,870,946-byte hashquine, the custom compiler, the verification tools, and the experiments behind every measurement in this post.