Code anatomy · Code anatomy
OpenSSH’s three-line equality loop and the limits of timing safety
An analysis of what a comparison accumulator proves, what compilation can change, and which assumptions its caller must preserve.
Kinwilder research desk · · AI-assisted analysis
Code specimen
openbsd-compat/timingsafe_bcmp.c
for (; n > 0; n--)
ret |= *p1++ ^ *p2++;
return (ret != 0);- Line 1: Length is a security precondition — Analysis: Fixed traversal requires a fixed length. The caller must establish that n covers the intended comparison domain and that both buffers contain n readable bytes. This loop provides neither validation.
- Line 2: Differences persist without short-circuiting — Analysis: With the surrounding zero initialization and ordinary unsigned-byte promotions, XOR identifies differing bits and OR preserves them. Both reads remain part of each source-level iteration; compiled behavior still requires assessment.
- Line 3: Equality has a deliberately narrow meaning — Analysis: Zero means every examined pair matched; one means some pair differed. This provides no ordering information. A zero-length comparison also returns zero, so callers must enforce any requirement for a nonempty, complete value.
Format: Code anatomy. Original source publication: 2010-10-07, 10:25:27 UTC (supplied provenance).
What is established
The packet identifies openbsd-compat/timingsafe_bcmp.c at revision aa18063baf35e303832d9ec58204ffaab221de85 as ISC-licensed OpenSSH/OpenBSD hardening code, not a synthetic fixture. Its surrounding function supplies unsigned-byte pointers, a size_t length and a zero-initialized accumulator; HAVE_TIMINGSAFE_BCMP conditionally excludes this fallback.
Exact excerpt, attributed to the original OpenSSH/OpenBSD source page:
for (; n > 0; n--)
ret |= *p1++ ^ *p2++;
return (ret != 0);
Our analysis
Everything below is our analysis or proposed defensive guidance. The central invariant is that, for a fixed public length, comparison work should not depend on where the buffers differ. In a hypothetical verifier, the trust boundary separates a submitted candidate from a protected reference. Equality may be an intended output; information about intermediate matching bytes usually is not. This excerpt addresses one part of that boundary.
Line 1 makes the iteration count depend on n. With a starting length of sixteen, its abstract C execution performs sixteen iterations regardless of the contents. Decrementing an unsigned length is safe here because the condition prevents an iteration starting at zero. However, the loop does not establish that sixteen is the correct length, that the length is public, or that either buffer contains sixteen readable bytes.
Line 2 reads one byte from each buffer, advances both pointers and combines their exclusive-or result into ret. Equal bytes contribute zero. Unequal bytes contribute at least one set bit. Bitwise OR preserves any previously set bit, so subsequent equal bytes cannot erase an earlier difference. Unlike a logical short-circuit expression, this accumulation still evaluates the byte reads after the accumulator becomes nonzero. The invariant after each iteration is that ret is zero exactly when all byte pairs examined so far matched.
That argument assumes the surrounding initialization and ordinary integer behavior: on common targets with eight-bit unsigned characters, both operands promote to int without losing information. Every read must also be valid. Both objects must remain alive and readable for the entire requested range, with no unsynchronized concurrent modification. The function supplies no bounds validation. An out-of-bounds read or data race invalidates reasoning based on normal C execution.
Line 3 normalizes the accumulator to zero for equality and one for inequality. It intentionally discards which bits differed. This is an equality interface, so callers must not interpret its result as lexicographic ordering. With n equal to zero, the loop reads nothing and returns equality. That is mathematically consistent for empty sequences, but a verifier must separately decide whether an empty comparison is acceptable.
Why avoid an early return? In an alternative comparator that exits at the first mismatch, a difference near the beginning can require fewer operations than one near the end. If those differences survive compilation and are observable through the surrounding system, duration can disclose information correlated with the matching prefix. This is a conditional mechanism analysis, not evidence of an attack against this implementation. Noise, compiler transformations and the surrounding protocol all affect whether such information is observable.
The obvious repair—remove the early return—does not establish a complete timing guarantee. C specifies functional behavior without promising a particular execution duration. A compiler could transform the expression while preserving its ordinary result. Optimization settings, target instructions and link-time optimization therefore belong in the assessment. The relevant question is whether the deployed machine code retains suitable control flow and memory access behavior for the intended threat model.
Even suitable machine code has runtime assumptions. The source uses sequential addresses rather than indexing memory by byte values, but pointer placement, cache state and scheduling can still affect elapsed time. Constant-time reasoning generally seeks independence from secrets under specified conditions, not identical wall-clock measurements. Secret-dependent parsing, length selection or validation elsewhere can also defeat the intended property before this loop begins.
Defensive actions
The following are original review steps, not reported findings about OpenSSH.
- Establish what actually runs. Check whether configuration selects this fallback or another implementation, then inspect the linked comparison routine in the release artifact. Reviewing a source file that was excluded from the build cannot establish the behavior of the deployed comparator.
- Define the caller’s length contract. For a fixed-size verification value, derive the comparison length from the expected representation. Check the candidate’s length and buffer validity before reading it. Comparing only an untrusted candidate’s chosen prefix can establish equality over the wrong domain. A public-length rejection can be appropriate; a secret length requires a different disclosure assessment.
- Review the complete decision path. Follow decoding, normalization, comparison and response handling. Identify which outcomes are intentionally public. Determine whether error handling or content-dependent preprocessing reveals distinctions beyond those outcomes. A timing-resistant comparison cannot compensate for information already released elsewhere.
- Preserve the assessment through build changes. Review optimized output for mismatch-dependent exits, unexpected calls and secret-dependent accesses. Functional regression tests should cover equality and differences at several positions. Controlled measurements can provide additional evidence, but passing tests cannot prove timing independence for every machine or execution environment.
For an original defensive exercise, draw a table using public dummy buffers of one fixed length. Include equality and single differences at the first, middle and final positions. Walk the loop on paper: record the iteration count, when ret first becomes nonzero, and the final result. Each row should traverse the same number of bytes; only the equal row should finish at zero.
Then add empty, shorter and longer candidate cases at the caller boundary. Record which cases the caller rejects before invoking the comparator and why. This separates the arithmetic invariant from the authorization invariant: acceptance should require equality across the entire expected value. The exercise executes no attacker code and makes no timing claim.
Limits
The publication timestamp identifies the supplied source’s provenance; it does not date the invention of this technique. The excerpt demonstrates a source-level accumulation strategy, not a complete verifier or evidence of an incident. We have not independently fetched, compiled, disassembled or reproduced it.
A mismatch-dependent exit in the deployed machine code would falsify an interpretation that the compiled routine preserves fixed traversal. Reproducible timing differences associated with secret content, under controlled public inputs, would challenge an end-to-end timing-independence claim without alone identifying the responsible component. If the build selects another implementation, this excerpt cannot establish that implementation’s behavior.
Research desk: AI-authored analysis, checked in a separate AI review against the linked public sources. No human expert review or independent exploit reproduction is implied.