fix(python): preserve historical signed code fields

Keep signed co_stacksize and co_flags for historical magic entries whose constructors accepted them. Preserve magic 3410 with a constructor-revision limitation; reject negative values from 3411 onward.

Fixes #6487
This commit is contained in:
morluto
2026-10-23 18:30:43 +08:00
parent 667402846c
commit c6ab965062
7 changed files with 253 additions and 79 deletions
+23 -1
View File
@@ -1520,9 +1520,12 @@ it("rejects code CPython treats as corrupt instead of complete coverage", () =>
expect(() => parse(file("py311", 3531, { flags: 0x80000000 }))).toThrow(
/co_flags is negative/,
);
expect(() => parse(file("py37", 3401, { flags: 0x80000000 }))).toThrow(
expect(() => parse(file("py38", 3411, { flags: 0x80000000 }))).toThrow(
/co_flags is negative/,
);
expect(() => parse(file("py38", 3411, { stacksize: 0xffffffff }))).toThrow(
/co_stacksize is negative/,
);
const valid = parse(
file("py311", 3531, {
@@ -1544,6 +1547,25 @@ it("rejects code CPython treats as corrupt instead of complete coverage", () =>
const negativePy37Flags = parse(file("py37", 3394, { flags: 0x80000000 }));
expect(root(negativePy37Flags).flags).toBe(-0x80000000);
expect(pythonPycSchema.safeParse(negativePy37Flags).success).toBe(true);
const negativePy37Stacksize = parse(
file("py37", 3394, { stacksize: 0xffffffff }),
);
expect(root(negativePy37Stacksize).stacksize).toBe(-1);
expect(pythonPycSchema.safeParse(negativePy37Stacksize).success).toBe(true);
const negativePy38AlphaFlags = parse(
file("py37", 3401, { flags: 0x80000000 }),
);
expect(root(negativePy38AlphaFlags).flags).toBe(-0x80000000);
const ambiguousPy38Fields = parse(
file("py38", 3410, { stacksize: 0xffffffff, flags: 0x80000000 }),
);
expect(root(ambiguousPy38Fields)).toMatchObject({
stacksize: -1,
flags: -0x80000000,
});
expect(ambiguousPy38Fields.limitations).toEqual(
expect.arrayContaining([expect.stringContaining("Magic 3410 is shared")]),
);
const py37 = parse(
file("py37", 3394, {
argcount: 1,
+13 -5
View File
@@ -235,7 +235,7 @@ export const parsePythonPyc = (
marshal,
mapped.code_layout,
input,
mapped.python_version !== "3.7",
mapped.negative_code_fields === "rejected",
);
const root = reader.readObject(0);
if (reader.offset !== marshal.length)
@@ -268,6 +268,12 @@ export const parsePythonPyc = (
? "source_hash_hex is the PEP 552 header field; the original .py is not read."
: "source_mtime and source_size are header fields; they do not prove a matching .py is present.",
"python_version is the pinned CPython magic table for this header, not a runtime interpreter.",
...(mapped.negative_code_fields === "revision-dependent" &&
codeObjects.some((code) => code.stacksize < 0 || code.flags < 0)
? [
"Magic 3410 is shared by CPython revisions with different negative co_stacksize/co_flags constructor checks; these raw values are preserved, but this header cannot establish constructor acceptance.",
]
: []),
...(unknownFlags === 0
? []
: [
@@ -323,7 +329,7 @@ class MarshalReader {
readonly bytes: Buffer,
readonly layout: PythonPycCodeLayout,
readonly input: InspectPythonPycInput,
readonly rejectNegativeCodeFlags: boolean,
readonly rejectNegativeCodeFields: boolean,
) {}
readObject(depth: number): MarshalValue {
@@ -437,9 +443,11 @@ class MarshalReader {
const kwonlyargcount = this.nonNegativeInt32("co_kwonlyargcount");
const nlocals =
this.layout === "py311" ? null : this.nonNegativeInt32("co_nlocals");
const stacksize = this.nonNegativeInt32("co_stacksize");
const stacksize = this.i32();
if (this.rejectNegativeCodeFields && stacksize < 0)
malformed("co_stacksize is negative.");
const flags = this.i32();
if (this.rejectNegativeCodeFlags && flags < 0)
if (this.rejectNegativeCodeFields && flags < 0)
malformed("co_flags is negative.");
const bytecode = asBytes(this.readObject(depth + 1), "co_code");
// 3.11+ `_PyCode_Validate` rejects a length that is not a whole code unit.
@@ -1501,7 +1509,7 @@ class MarshalReader {
return value;
}
/** CPython `r_long` int fields must not be negative in a code object. */
/** CPython `r_long` count fields that its constructors require non-negative. */
nonNegativeInt32(field: string): number {
const value = this.i32();
if (value < 0) malformed(`${field} is negative.`);