Zero Input, Zero Output: The Fail-Open Bug Nobody Audits

PlanBtoshi
Academy

Zero Input, Zero Output: The Fail-Open Bug Nobody Audits

Hook

On a Tuesday morning in this bear market, a research pipeline completed a nine-dimension protocol analysis. Every field was present. Every field read N/A. The decomposition stage had returned an empty payload — no title, no source, no information points, no thesis. The downstream analyzer did not invent a technology assessment, a token supply table, or a competitive landscape. It emitted the template, marked every dimension as unassessable, and appended one line: fuel is zero, the engine does not start.

That output is correct. It is also rare. In eighteen years of watching systems process data, I have learned that the default behavior of software under missing input is not silence. It is fabrication with a straight face. The empty report is a case study in a failure mode the industry refuses to name, and the name is fail-open analysis.

Context

An analytical pipeline has three stages: ingestion, decomposition, emission. Ingestion fetches the source. Decomposition extracts entities, claims, and information points into a structured payload. Emission runs the analytical dimensions against that payload and renders a report.

The contract between stages is implicit. Stage three assumes stage two returned something with content. Nothing in the schema enforces it. A field typed as a list of information points is satisfied by an empty list. A field typed as a string is satisfied by an empty string. The type checker passes. The report renders. The consumer reads a document structurally identical to a real analysis, and the only difference is a word repeated ninety times.

Diagnosing which stage failed requires three independent signals, and most teams instrument none of them: the HTTP status and body length at ingestion, the exception and timeout log on the decomposition process, and the payload length at the stage boundary. A 200 with empty body is an ingestion fault. A non-zero exit code with a populated body is a parser fault. A clean exit with a zero-length list is a schema fault — the worst kind, because every downstream check passes.

This is not a research problem. It is a boundary-condition problem, and boundary conditions are where I have spent most of my career. In 2018 I audited a refund contract line by line and found three edge cases in the withdrawal logic that could have blocked refunds for roughly 50,000 users. None were exploits. All were states the developers never enumerated — zero balance, zero allowance, zero remaining. In 2020 I reviewed early cToken contracts and found a boundary condition in the interest rate calculation where utilization approached the edge of its domain and the rate curve denominator collapsed toward zero. Twelve lending pools inherited it. The math was not wrong. The domain was unexamined.

Empty input is the same class of bug wearing a different jacket.

Core

Look at how emptiness propagates.

In Go, json.Unmarshal into a struct leaves missing keys at their zero value. A missing array field becomes nil. An explicitly empty array becomes a non-nil slice of length zero. Both report len() == 0, but slice == nil is true for only one of them. A guard written as if points == nil { return error } passes a payload that was deliberately emptied upstream. The guard exists. The guard is useless.

In Python, if not data: catches empty list, empty dict, empty string, and None — and also 0 and False. The check is too broad in one direction and, when written as if data is None:, too narrow in the other. Neither idiom expresses the actual invariant.

The invariant is semantic. len(points) > 0 is necessary and insufficient. Ten points that are all placeholder strings pass the length check and fail the analysis. The correct assertion has three clauses: cardinality greater than zero, per-element non-null provenance, and per-element type tag drawn from a closed enumeration of fact, opinion, rumor, official. Anything less validates syntax while the semantics are empty.

Now move the reasoning on-chain, because that is where the money leaves.

Every oracle consumer calls latestRoundData(). It returns five values: roundId, answer, startedAt, updatedAt, answeredInRound. The canonical integration requires four checks — answer > 0, updatedAt > 0, answeredInRound >= roundId, and block.timestamp - updatedAt <= heartbeat. Most integrations implement one. A feed that has never been updated returns updatedAt == 0 and answer == 0; a consumer that checks only answer > 0 reverts, which is safe. A consumer that skips the staleness clause prices collateral against a number that stopped moving three days ago. The protocol does not crash. It lends against a frozen price and reports healthy solvency until it does not.

Zero-knowledge systems fail the same way at the constraint layer. I spent six months in 2022 reverse-engineering zk-SNARK verification logic, and the recurring question was never whether a proof verified — it was what the proof verified over. A circuit that attests to properties of a credential set will happily accept a proof over an empty set unless the constraint system explicitly forbids it. The verifier returns true. The statement is vacuous. Adding a batching optimization to lift throughput past 500 TPS meant touching that exact surface, and the reason the change was safe is that we wrote the emptiness constraint first.

The structural parallel is exact. An N/A in a research report and a stale price in a lending market are the same object: a placeholder consumed as data. The report says unassessable; the reader sees a complete document. The feed says round seven, answer 1,847; the contract sees a valid integer. Both failures are silent. Silence is the strongest proof of truth only when someone is listening for it.

Here is the number that matters. A pipeline that fails closed produces zero reports and one alert. A pipeline that fails open produces one report per request, indefinitely, at full cost, with no signal that anything is wrong. The second system is cheaper to operate, easier to demo, and strictly more dangerous.

Contrarian

The industry has spent two years optimizing for graceful degradation. Sequencers that keep producing blocks when the prover stalls. Oracles that fall back to a secondary feed without checking the secondary feed's heartbeat. Pipelines that always return something, because an empty response scores zero on every benchmark. Robustness is the stated goal, and robustness in practice means never returning nothing.

That is the blind spot. Graceful degradation in an analytical pipeline is hallucination with better UX. When the reward function penalizes empty output, the system learns to fill the template. It will produce a technology assessment for a protocol that does not exist, a supply table for a token with no contract, and a competitive landscape with three invented peers — in the same confident register as real work. The output is indistinguishable in form and worthless in content. In a bear market, that is how capital reaches nothing: not through fraud, but through documents that were structurally valid and semantically empty.

The 2018 refund contract taught me that developers enumerate the states they expect and skip the ones they do not. The 2020 rate boundary taught me the same lesson at the arithmetic layer. The empty report is that lesson at the semantic layer, and it is the one nobody instruments, because an empty list does not throw. Complexity hides its own failures — and a template full of N/A is complexity's most polite disguise.

Takeaway

Add the fuse. Every pipeline boundary should assert a non-empty payload before emission, and the assertion should be semantic, not structural. Every oracle consumer should check all four conditions, not the one that compiles. Every system that consumes a report should verify it contains at least one traceable claim before acting on it. In 2024 I designed a zero-knowledge identity framework for a tier-one bank, and the first artifact we shipped was not a circuit. It was the list of inputs the circuit must reject.

Structure outlasts sentiment. The question for the next cycle is not which protocol ships the most features. It is which teams can prove their data path fails closed — and can show you the alert that fires when it does.