fix(python): reject code keys and set members with unhashable consts (#6424)

* fix(python): reject code keys and set members with unhashable consts

* fix(python): refuse to hash a tuple that is still being read

* fix(python): hash a code object's co_consts tuple, not its item list

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Wadeandmorluto authored and GitHub committed 2026-10-23 18:05:58 +08:00
1 parent 630e6becdf
commit cc99d896be
3 files changed
+107 -3

No files matched your search

+73
View File
@@ -933,6 +933,79 @@ it("accepts code objects as set members and dictionary keys", () => {
expect(pythonPycSchema.safeParse(report).success).toBe(true);
});
it("rejects code keys and members whose consts are unhashable", () => {
const list = Buffer.concat([Buffer.from([0x5b]), u32(0)]);
const int = Buffer.concat([Buffer.from([0x69]), i32(1)]);
const tuple = (...items: Buffer[]): Buffer =>
Buffer.concat([Buffer.from([0x29, items.length]), ...items]);
const code = (...consts: Buffer[]): Buffer =>
marshalCode("py311", { name: "u", qualname: "u", consts });
const setOf = (kind: number, member: Buffer): Buffer =>
Buffer.concat([Buffer.from([kind]), u32(1), member]);
const dictKey = (key: Buffer): Buffer =>
Buffer.concat([Buffer.from([0x7b]), key, int, Buffer.from([0x30])]);
for (const key of [code(list), code(tuple(int, list)), code(code(list))]) {
expect(() => parse(withConsts([setOf(0x3c, key)]))).toThrow(
PythonPycFormatFailure,
);
expect(() => parse(withConsts([setOf(0x3e, key)]))).toThrow(
PythonPycFormatFailure,
);
expect(() => parse(withConsts([dictKey(key)]))).toThrow(
PythonPycFormatFailure,
);
}
const cyclic = Buffer.concat([
Buffer.from([0x29 | 0x80, 1]),
marshalCode("py311", {
name: "u",
qualname: "u",
flagged: true,
constsObject: tuple(marshalRef(0)),
}),
]);
expect(() => parse(withConsts([cyclic, setOf(0x3c, marshalRef(1))]))).toThrow(
PythonPycFormatFailure,
);
// A set inside a tuple that reaches the unfinished tuple, directly, inside
// a code object's consts, or as those consts, before the list is read.
for (const member of [
marshalRef(0),
marshalCode("py311", {
name: "u",
qualname: "u",
constsObject: tuple(marshalRef(0)),
}),
marshalCode("py311", {
name: "u",
qualname: "u",
constsObject: marshalRef(0),
}),
])
expect(() =>
parse(
withConsts([
Buffer.concat([
Buffer.from([0x29 | 0x80, 2]),
setOf(0x3c, member),
list,
]),
]),
),
).toThrow(PythonPycFormatFailure);
const unhashed = parse(withConsts([tuple(code(list))]));
expect(unhashed.coverage.status).toBe("complete");
expect(unhashed.code_objects[1]?.consts[0]).toEqual({
kind: "list",
length: 0,
});
const hashable = parse(withConsts([setOf(0x3c, code(tuple(int)))]));
expect(root(hashable).consts[0]).toEqual({ kind: "set", length: 1 });
expect(hashable.coverage.status).toBe("complete");
});
it("uses the code equality fields for each CPython layout", () => {
const codeCollections = (
layout: "py38" | "py311",