Skip to content

Revisit the strict-ingest preflight: six memory findings on forty lines, and one residual that cannot be closed locally #435

Description

@bomly-guy

Filed from review on #431. This is deliberately a design question rather than another point fix — it is the sixth memory finding on requireUnambiguousJSON, and the pattern is more informative than any one of them.

The immediate finding

Reported by Codex on internal/sbom/strict.go:194: the name-byte bound is checked after ReadToken has already unquoted and retained the name, so a single name larger than the bound is retained once before being refused.

Measured: a 200 MiB member name retains 200 MiB — 1.00× input.

Why it was not fixed in #431

Two reasons, both recorded in the code beside the bound:

  1. No library option exists. jsontext.Options offers AllowDuplicateNames, AllowInvalidUTF8, and formatting options. Nothing bounds a token's size.
  2. The only remaining route is prohibited. Sizing the name before the decoder reads it means scanning raw bytes for the string's closing quote with escape handling — mirroring the library's tokenizer, which this project's delegation rule names explicitly as an anti-pattern.

The residual is also the floor, not a multiplier: the name is bounded by an input already capped at 256 MiB and already resident. That is qualitatively different from what the earlier fixes removed.

The pattern, which is the actual point

Six findings against one ~40-line function across #431's review rounds:

# Shape Worst case Fix
1 One very wide object 6.7× input bound the widest object
2 Nested objects, each under that bound 2.5 GiB from 411 MiB bound the aggregate across open objects
3 Aggregate names within bounds, but long 900 MiB from 194 MiB bound name bytes, not count
4 Large values, which no name bound reaches 447 MiB from 150 MiB read in place (*bytes.Buffer)
5 Byte bound counted indentation (my regression) 6.4× over-count measure the token, not the span
6 One name larger than the bound 1.00× input open — see above

Two observations worth carrying:

  • The two fixes that closed whole classes were replacements, not tunings. (4) and (5) removed an approximation — a generic reader instead of the in-place path, offset arithmetic instead of the token. The three that added or adjusted bounds each left a next shape.
  • Every doc comment written about this function's memory was wrong until measured. "Constant memory", then a bound on the wrong quantity, then an estimate off by 40%. The current comment states measured figures only.

The question to decide

Whether a separate hand-instrumented preflight is the right shape at all. It exists because ADR-0039 wants exactly two guarantees (duplicate names, invalid UTF-8) without the other behavior changes that decoding through encoding/json/v2 would bring — case-sensitive field matching in particular, which the ADR explicitly does not claim.

Alternatives worth costing:

  1. Keep it, accept the residual. Document that preflight peak is bounded by input size. Cheapest, and arguably already true.
  2. Decode through json/v2 for real, and validate what the case-sensitivity change actually does to SPDX and CycloneDX fixtures. Removes the hand-instrumentation entirely; needs the behavior study ADR-0039 declined to do.
  3. Ask upstream for a token-size option. A jsontext.MaxTokenSize-style option would close (6) properly and is plausibly useful beyond Bomly.

Context

  • internal/sbom/strict.go — the function, its bounds, and the recorded reason for the open residual.
  • Coverage: TestADocumentTooLargeToCheckIsRefused, TestNestedObjectsCannotAccumulatePastTheBound, TestSiblingObjectsDoNotAccumulate, TestLongMemberNamesAreBoundedByTheirBytes, TestClosedObjectsReleaseTheirNameBytes, TestOrdinaryNamesDoNotApproachTheByteBound, TestPrettyPrintingDoesNotCountAgainstTheByteBound, TestStrictPreflightReadsInPlace.
  • Related: Strict SBOM ingest rejects escaped lone surrogates, a third class ADR-0039 does not claim #432 (lone-surrogate rejection is a third class ADR-0039 does not claim) — same function, same ADR, worth deciding together.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions