Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.

fix: a sealed_from chain expands to the template it ultimately names (audit semantic F-06/F-07) - #23

Merged
sinkog merged 1 commit into
develfrom
fix/nested-sealed-template
Aug 9, 2026
Merged

fix: a sealed_from chain expands to the template it ultimately names (audit semantic F-06/F-07)#23
sinkog merged 1 commit into
develfrom
fix/nested-sealed-template

Conversation

@sinkog

@sinkog sinkog commented Aug 9, 2026

Copy link
Copy Markdown
Member

A template entry may itself be sealed from another template. Go expanded the chain; Rust resolved exactly one level — and because a sealed_from node declares no shape of its own, the node then defaulted to object and materialized as {}.

Two different objects from one schema:

go:   values: from-inner
      origin: [{sealed: {template: $inner, path: $.leaf}}, schema]

rust: values: {}
      origin: [{sealed: {template: $outer, path: $.mid}}]

The corpus had no nested template — vectors 003 and 004 cover a single level — so neither runner could see it, and the divergence sat behind a claim that the two implementations check each other.

What the expansion also settles

The origin names the innermost template and path — the place the value actually came from — not the mount point where the chain was entered.

Go answered that question by implementation and nothing wrote it down. The vector pins it now; SPEC §5 should say it (audit semantic F-06).

The bound

64 levels in both. Go had it; Rust had none, which was harmless only because it did not recurse at all. Recursing without a bound would turn a chain of templates referring to each other into an unbounded loop over caller-supplied input (audit semantic F-07).

Measured: a 200-level chain now returns template expansion exceeded the maximum depth rather than running.

Both implementations produce byte-identical output for the new vector. 38 vectors.

A note on the gate from #21

Adding this vector made docs/en/architecture.md's "37 vectors" false, and make ci refused before anything else ran.

That is exactly the class of defect the status-claims gate was written for — caught on the commit that created it, rather than by an audit months later.


make ci passes. 17 of 37 audit findings closed.

…(audit semantic F-06/F-07)

A template entry may itself be sealed from another template. Go expanded the
chain; Rust resolved exactly ONE level, and because a `sealed_from` node
declares no shape of its own the node then defaulted to object and materialized
as `{}`.

Two different objects from one schema:

    go:   values: from-inner
          origin: [{sealed: {template: $inner, path: $.leaf}}, schema]
    rust: values: {}
          origin: [{sealed: {template: $outer, path: $.mid}}]

The corpus had no nested template — 003 and 004 cover a single level — so
neither runner could see it, and the divergence sat behind a claim that the two
implementations check each other.

What the expansion also settles, and the specification does not state: the
origin names the INNERMOST template and path, the place the value actually came
from, not the mount point where the chain was entered. Go answered that
question by implementation and nothing wrote it down. The vector pins it now;
SPEC §5 should say it (audit semantic F-06).

Expansion is bounded at 64 levels in both. Go had that bound; Rust had none,
which was harmless only because it did not recurse at all. Recursing without a
bound would have turned a chain of templates referring to each other into an
unbounded loop over input a caller supplies (audit semantic F-07). Measured: a
200-level chain now returns "template expansion exceeded the maximum depth"
rather than running.

Both implementations produce byte-identical output for the new vector. 38 now.

One thing the status-claims gate did while this landed, worth recording because
it is the first time it fired for real: adding the vector made
docs/en/architecture.md's "37 vectors" false, and `make ci` refused before
anything else ran. That is the class of defect it was written for, caught on the
commit that created it rather than by an audit months later.

---
[signing-metadata]
key = cic-my-sign-key
signature = vault:v1:MEQCIAk1sTJ3nIGR30UpebXbjIscwc/ReWGCt3rfWBjlGLfvAiBGzDlrMq7ppr62+W+uoIbyUGLLlubZ4168qhmOBIS0aQ==
hash-algorithm = sha256
digest = Gc2FvV2yyH4ThVDStMZTrkjd/V+UeBuPeMTuv5BtdwE=

[certificate]
-----BEGIN CERTIFICATE-----
MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw
QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV
BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy
MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb
MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe
i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU
yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS
WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0
lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW
-----END CERTIFICATE-----
@sinkog
sinkog merged commit 353f295 into devel Aug 9, 2026
2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant