From 5d5fe72218e88ee8bb579f8da9ce73e46b80fd7e Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:58:35 +0800 Subject: [PATCH] fix(plist): reserve Evidence framing in nesting allowance --- docs/guides/native/native-investigation.md | 11 ++++++----- src/contracts/native/nativeToolContracts.ts | 3 ++- src/domain/apple/plistValue.ts | 5 ++++- src/native/parsers/plist.test.ts | 8 +++++--- src/native/parsers/plist.ts | 6 ++---- .../applications/nativePlistNesting.test.ts | 12 ++++++------ .../providers/native/nativePlistBinarySource.test.ts | 4 ++-- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/guides/native/native-investigation.md b/docs/guides/native/native-investigation.md index 77797a9157..fa11a413b3 100644 --- a/docs/guides/native/native-investigation.md +++ b/docs/guides/native/native-investigation.md @@ -674,11 +674,12 @@ node of the decoder's DOM at a measured upper bound, so a plist made of many small elements reaches it at a few MiB of source. JSON decode memory has not been measured and has no equivalent decode estimate. A refused input reports non-retryable `resource_constraint` with `reported_limits.maximum_input_bytes` or `maximum_decoded_bytes` as applicable. -Ordinary direct-source decoding has a ceiling of 998 source nesting levels, -derived from the existing 1000-level JSON result capacity by reserving one level -for typed scalar fields and one for the result's `value` field. Deeper input reports -`resource_constraint` with `reported_limits.maximum_nesting_depth: 998` and names -the fixed nesting allowance. XML admission also requires native validation, which +Ordinary direct-source decoding has a ceiling of 997 source nesting levels, +derived from the existing 1000-level JSON result capacity. Three levels are +reserved for typed scalar fields, the report value and its Evidence envelope. +Deeper input reports `resource_constraint` with +`reported_limits.maximum_nesting_depth: 997` and names the fixed nesting allowance. +XML admission also requires native validation, which can refuse input below this decoding ceiling. Compatibility conversions can likewise encounter an earlier native utility refusal; both retain native diagnostics. Archive and Interface Builder decoding retain their separate diff --git a/src/contracts/native/nativeToolContracts.ts b/src/contracts/native/nativeToolContracts.ts index dedfbc8f88..112f365101 100644 --- a/src/contracts/native/nativeToolContracts.ts +++ b/src/contracts/native/nativeToolContracts.ts @@ -1,3 +1,4 @@ +import { MAX_NATIVE_PLIST_NESTING_DEPTH } from "../../domain/apple/plistValue.js"; import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js"; import { z } from "zod"; import { nativeCallObservationInputSchema } from "../../domain/native/nativeCallObservation.js"; @@ -121,7 +122,7 @@ export const NATIVE_TOOL_CONTRACTS = [ ), native( "inspect_plist", - "Requires an open binary or bundle-resource session; use open_binary or open_bundle_resources first. Parse its Info.plist by default, or select a local plist with path within that session. Supplying path does not open a session. Returns normalized JSON rather than plutil text. native_command_timeout_ms is each file or plutil command's wall clock (omitted means 60000 ms). The 67108864-byte plist input and decode allowances and the 998-level nesting allowance have no runtime override. XML sources also require native plutil validation, which can refuse shallower inputs with its own diagnostics. The 4096-byte markup probe is a probe window, not a document cap, and has no runtime override.", + `Requires an open binary or bundle-resource session; use open_binary or open_bundle_resources first. Parse its Info.plist by default, or select a local plist with path within that session. Supplying path does not open a session. Returns normalized JSON rather than plutil text. native_command_timeout_ms is each file or plutil command's wall clock (omitted means 60000 ms). The 67108864-byte plist input and decode allowances and the ${MAX_NATIVE_PLIST_NESTING_DEPTH}-level nesting allowance have no runtime override. XML sources also require native plutil validation, which can refuse shallower inputs with its own diagnostics. The 4096-byte markup probe is a probe window, not a document cap, and has no runtime override.`, z.object({ path: localPathStringSchema .optional() diff --git a/src/domain/apple/plistValue.ts b/src/domain/apple/plistValue.ts index 499f0c7177..cd81093d2e 100644 --- a/src/domain/apple/plistValue.ts +++ b/src/domain/apple/plistValue.ts @@ -1,7 +1,7 @@ import { types } from "node:util"; import { z } from "zod"; -import type { JsonValue } from "../jsonValue.js"; +import { MAX_JSON_DEPTH, type JsonValue } from "../jsonValue.js"; import { copyPlistUidIdentity } from "./plistUid.js"; import { UnknownPlistScalar } from "./unknownPlistScalar.js"; @@ -18,6 +18,9 @@ const jsonScalarSchema = z.union([ */ export const MAX_PLIST_DEPTH = 128; +/** Reserve typed scalar fields, the report value, and Evidence.normalized_result. */ +export const MAX_NATIVE_PLIST_NESTING_DEPTH = MAX_JSON_DEPTH - 3; + /** Agent-visible refusal for nesting past the JSON projection allowance. */ const plistNestingLimitMessage = (maximumDepth = MAX_PLIST_DEPTH): string => `Plist exceeds ${String(maximumDepth)} nesting levels. This release has no runtime override for this ${String(maximumDepth)}-level nesting allowance.`; diff --git a/src/native/parsers/plist.test.ts b/src/native/parsers/plist.test.ts index 68f594b1d1..6cc4592226 100644 --- a/src/native/parsers/plist.test.ts +++ b/src/native/parsers/plist.test.ts @@ -59,8 +59,8 @@ it.each([ ] as const)( "bounds ordinary %s by the projected JSON capacity, including scalar wrappers", (encoding) => { - // One level for typed scalar fields and one for normalized_result.value. - const maximum = MAX_JSON_DEPTH - 2; + // Typed scalar fields, report.value and Evidence.normalized_result each add a level. + const maximum = MAX_JSON_DEPTH - 3; for (const depth of [maximum, maximum + 1]) { let binary: PlistValue = Buffer.from([0, 1]); if (encoding === "binary data") @@ -99,7 +99,9 @@ it.each([ } else { if (!parsed.ok) throw parsed.error; expect(() => - jsonValueSchema.parse({ value: parsed.value.value }), + jsonValueSchema.parse({ + normalized_result: { value: parsed.value.value }, + }), ).not.toThrow(); let leaf: unknown = parsed.value.value; for (let level = 0; level < depth; level += 1) { diff --git a/src/native/parsers/plist.ts b/src/native/parsers/plist.ts index c3d4b78822..0a9415fc99 100644 --- a/src/native/parsers/plist.ts +++ b/src/native/parsers/plist.ts @@ -5,13 +5,14 @@ import { AnalysisResourceConstraintError, } from "../../domain/analysisErrorCore.js"; import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js"; -import { MAX_JSON_DEPTH, type JsonValue } from "../../domain/jsonValue.js"; +import type { JsonValue } from "../../domain/jsonValue.js"; import { binaryDatePrecisionOverlapLimitation, negativeZeroReal, negativeZeroRealLimitation, nonFiniteRealLimitation, projectPlistValue, + MAX_NATIVE_PLIST_NESTING_DEPTH, unknownDateLimitation, unknownScalarLimitation, type ProjectedPlistValue, @@ -49,9 +50,6 @@ export const MAX_NATIVE_PLIST_BYTES = 64 * 1024 * 1024; /** Provisional decode allowance. This release has no runtime override. */ const MAX_NATIVE_PLIST_DECODE_BYTES = 64 * 1024 * 1024; -// Reserve one level for typed scalar fields and one for normalized_result.value. -export const MAX_NATIVE_PLIST_NESTING_DEPTH = MAX_JSON_DEPTH - 2; - const mebibytes = (bytes: number): string => `${String(bytes / (1024 * 1024))} MiB`; diff --git a/tests/acceptance/applications/nativePlistNesting.test.ts b/tests/acceptance/applications/nativePlistNesting.test.ts index 7df7423ec7..816d481ce7 100644 --- a/tests/acceptance/applications/nativePlistNesting.test.ts +++ b/tests/acceptance/applications/nativePlistNesting.test.ts @@ -56,12 +56,12 @@ describe.skipIf(process.platform !== "darwin")( { format: "binary", depth: 200, data: false }, { format: "xml", depth: 200, data: false }, { format: "json", depth: 200, data: false }, + { format: "binary", depth: 997, data: true }, { format: "binary", depth: 998, data: true }, - { format: "binary", depth: 999, data: true }, + { format: "xml", depth: 997, data: true }, { format: "xml", depth: 998, data: true }, - { format: "xml", depth: 999, data: true }, + { format: "json", depth: 997, data: false }, { format: "json", depth: 998, data: false }, - { format: "json", depth: 999, data: false }, ] as const) { const { format, depth, data } = fixture; const path = join(root, `${format}-${depth}.plist`); @@ -81,7 +81,7 @@ describe.skipIf(process.platform !== "darwin")( // Compare its upper-edge admission with the actual utility, while // requiring the historical depth-129/200 controls to succeed below. const native = - format === "xml" && depth === 998 + format === "xml" && depth === 997 ? await lintXmlPlist(path) : undefined; const output = await cli.run({ @@ -90,7 +90,7 @@ describe.skipIf(process.platform !== "darwin")( }); const response = await call("inspect_plist", { path }); expect(Buffer.compare(await readFile(path), bytes)).toBe(0); - if (depth === 999) { + if (depth === 998) { expect(output.exitCode, output.stderr).toBe(1); const expectedFailure = { code: "resource_constraint", @@ -98,7 +98,7 @@ describe.skipIf(process.platform !== "darwin")( retryable: false, details: { resource: "memory", - reported_limits: { maximum_nesting_depth: 998 }, + reported_limits: { maximum_nesting_depth: 997 }, }, remediation: { action: expect.stringMatching(/\S/u), diff --git a/tests/boundary/providers/native/nativePlistBinarySource.test.ts b/tests/boundary/providers/native/nativePlistBinarySource.test.ts index d75bc79308..81a7c3eaf7 100644 --- a/tests/boundary/providers/native/nativePlistBinarySource.test.ts +++ b/tests/boundary/providers/native/nativePlistBinarySource.test.ts @@ -96,8 +96,8 @@ it.each([ budget: "nesting", value: nested(MAX_JSON_DEPTH - 1), reason: /binary plist nesting.*decode budget/iu, - limits: { maximum_nesting_depth: MAX_JSON_DEPTH - 2 }, - allowance: /998-level.*plist nesting.*no runtime override/iu, + limits: { maximum_nesting_depth: MAX_JSON_DEPTH - 3 }, + allowance: /997-level.*plist nesting.*no runtime override/iu, }, ])( "names the fixed binary plist $budget allowance instead of host memory",