Found by adversarial review of #30 and reproduced locally — verified, not a report.
In the capability loop at vinci/containment-broker/src/admission.mjs:119-121 the guard is written
sortedUniqueStrings(capabilities ?? []), but the very next line uses sha256(capabilities) with no
?? []. When an attestation omits capabilities (or one of the five sets), capabilities is
undefined, sortedUniqueStrings([]) returns true, so the else if runs and
canonicalBytes(undefined) throws.
$ node -e 'validatePrelaunchAttestation({ attestation: { no_new_privs: true }, policy: {} })'
threw: TypeError | code: (none) | reasons: (none)
message: unsupported canonical value: undefined
CONFIRMED — escapes as a bare TypeError, not AdmissionRefusedError
Why it matters. validatePrelaunchAttestation is a refusal path: it is supposed to fail with
AdmissionRefusedError carrying code and an enumerated reasons list. A malformed attestation
instead escapes as a bare TypeError with neither. Any caller branching on e.code === "CONTAINMENT_ADMISSION_REFUSED"
mishandles it, and the enumerated refusal reasons — the thing that makes a refusal auditable — are lost.
A missing field should be a reason, not a crash.
Every sibling check in the same function (limits, supplementary_groups, inherited_fds,
ingress, controller_objects) is guarded. This one is the outlier.
Suggested fix. sha256(capabilities ?? []), and a test that a wholly empty attestation object
produces AdmissionRefusedError with reasons rather than a TypeError.
Not a regression from #30; pre-existing in the v3 boundary.
Found by adversarial review of #30 and reproduced locally — verified, not a report.
In the capability loop at
vinci/containment-broker/src/admission.mjs:119-121the guard is writtensortedUniqueStrings(capabilities ?? []), but the very next line usessha256(capabilities)with no?? []. When an attestation omitscapabilities(or one of the five sets),capabilitiesisundefined,sortedUniqueStrings([])returns true, so theelse ifruns andcanonicalBytes(undefined)throws.Why it matters.
validatePrelaunchAttestationis a refusal path: it is supposed to fail withAdmissionRefusedErrorcarryingcodeand an enumeratedreasonslist. A malformed attestationinstead escapes as a bare
TypeErrorwith neither. Any caller branching one.code === "CONTAINMENT_ADMISSION_REFUSED"mishandles it, and the enumerated refusal reasons — the thing that makes a refusal auditable — are lost.
A missing field should be a reason, not a crash.
Every sibling check in the same function (
limits,supplementary_groups,inherited_fds,ingress,controller_objects) is guarded. This one is the outlier.Suggested fix.
sha256(capabilities ?? []), and a test that a wholly empty attestation objectproduces
AdmissionRefusedErrorwith reasons rather than aTypeError.Not a regression from #30; pre-existing in the v3 boundary.