news 2026/9/10 12:51:10

CPython marshal 格式 v6 修复:共享引用的 frozendict 正确往返序列化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython marshal 格式 v6 修复:共享引用的 frozendict 正确往返序列化

CPython marshal 格式 v6 修复:共享引用的 frozendict 正确往返序列化

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

本文基于 CPython 主仓库中的一条 NEWS 条目,讲解marshal模块在格式版本 6 中针对frozendict共享引用(shared reference)往返序列化的缺陷修复。文章围绕该修复展开,结合 Python/marshal.c 源码、Lib/test/test_marshal.py 测试用例与 Doc/library/marshal.rst 文档,说明问题成因、修复原理、验证方法及影响范围,帮助读者理解 CPython 内部序列化格式的引用追踪机制。

背景:一条 NEWS 条目与它所指向的缺陷

CPython 仓库的 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-14-45-02.gh-issue-155315.Mk9Fd2.rst 记录了一条简短的变更说明:

Fixmarshalso that afrozendictreferenced more than once in the serialized data round-trips correctly, instead of failing to load withValueError. Patch by tonghuaroot.

这条条目虽然只有两句话,却指向了marshal序列化器在支持frozendict类型后遗留的一个真实缺陷:当一个frozendict对象在待序列化数据中被引用多次(例如同一个frozendict同时出现在一个列表的两个元素中,或作为另一个frozendict的两个键值)时,marshal.dumps可以成功写出数据,但marshal.loads在读回时却可能抛出ValueError,导致往返(round-trip)失败。

本文将依次回答四个问题:

  1. marshal是什么,为什么它需要处理共享引用?
  2. frozendictdictmarshal支持上有何差异,缺陷为什么只出现在frozendict上?
  3. 修复在源码层面做了什么,测试如何验证?
  4. 该修复对格式版本、兼容性和用户代码有什么影响?

marshal模块与格式版本机制

marshal是 CPython 内置的序列化模块,位于 Lib/marshal.py(纯 Python 包装层),核心实现为 C 代码 Python/marshal.c。其官方定位(见 Doc/library/marshal.rst)是内部序列化工具,主要用于读写编译后的字节码(code object),因此:

  • 它不保证跨 Python 版本的兼容性("The format of code objects is not compatible between Python versions"),每个 Python 版本的格式都可能变化;
  • 它支持的类型集合有限,不覆盖任意 Python 对象(相比pickle的通用性);
  • 官方文档明确建议:需要版本无关、安全可靠的跨语言/跨进程数据交换时,应优先使用pickle或 JSON 等方案。

marshal的二进制格式通过版本号演进。在 Include/cpython/marshal.h 中定义了当前版本:

#define Py_MARSHAL_VERSION 6

根据 Doc/library/marshal.rst 的版本历史说明:

格式版本引入于新增能力
0历史首个版本基础标量类型
3Python 3.4支持递归容器(recursive containers)与引用共享(reference sharing)
4Python 3.4更高效的字符串与小型元组表示
5Python 3.14支持序列化slice对象
6Python 3.15支持序列化frozendict对象

本文讨论的修复正是围绕格式版本 6frozendict支持展开的。在 Python/marshal.c 的类型标签定义中可以看到frozendict的类型码:

/* Supported types */ #define TYPE_DICT '{' #define TYPE_FROZENDICT '}' #define TYPE_REF 'r' // References (added in version 3)

版本 3 引入的TYPE_REF(类型码'r')是理解本缺陷的关键:它让marshal能够在序列化流中表达"同一个对象被引用多次"这一事实,从而在加载时恢复对象身份(identity)的共享关系。

问题本质:共享引用与frozendict的引用注册时机

引用追踪的两个阶段

marshal格式版本 3 起支持循环引用共享引用。序列化端(w_object)与反序列化端(r_object)分别维护一套引用表(ref table):

  • 序列化时:每当一个容器对象被写出,序列化器会把它登记到引用表中,并用一个自增的索引代表它。后续再次遇到同一个对象时,不重复写出内容,而是只写一个TYPE_REF加索引号。
  • 反序列化时r_object读到容器类型标签后,会先在引用表中为该对象预留/登记一个位置(R_REFr_ref_reserve),再填充其内容;后续遇到TYPE_REF时,通过索引直接取回先前登记的对象。

这段逻辑的核心代码如下(Python/marshal.c):

case TYPE_DICT: case TYPE_FROZENDICT: v = PyDict_New(); if (v == NULL) { break; } if (type == TYPE_DICT) { R_REF(v); /* dict:立即登记引用 */ ... } else { idx = r_ref_reserve(flag, p); /* frozendict:先预留索引 */ if (idx < 0) { Py_CLEAR(v); break; } } for (;;) { PyObject *key, *val; key = r_object(p); if (key == NULL) break; val = r_object(p); if (val == NULL) { Py_DECREF(key); break; } if (PyDict_SetItem(v, key, val) < 0) { ... } Py_DECREF(key); Py_DECREF(val); } if (PyErr_Occurred()) { Py_CLEAR(v); } if (type == TYPE_FROZENDICT && v != NULL) { Py_SETREF(v, PyFrozenDict_New(v)); /* 将 dict 转为 frozendict */ v = r_ref_insert(v, idx, flag, p); /* 完成引用登记 */ } retval = v; break;

注意这里dictfrozendict处理方式的差异:

  • dict是可变的,可以先PyDict_New()创建空 dict、立即R_REF(v)登记引用,再逐条填充键值对;后续任何指向该 dict 的TYPE_REF都能立刻取到"正在构建中"的对象。
  • frozendict不可变的,不能"先建空对象再填充"——它必须先构建一个临时dict,填充完所有键值后再调用PyFrozenDict_New(v)一次性转换为不可变的frozendict。因此反序列化器无法在构建完成前就把最终对象登记进引用表,只能先用r_ref_reserve预留索引、在构建完成后用r_ref_insert补登记。

缺陷的触发路径

问题就出在frozendict补登记(r_ref_insert)与TYPE_REF解析之间的时序上。

设想如下数据:

import marshal fd = frozendict({'a': 1, 'b': 2}) out = marshal.loads(marshal.dumps([fd, fd]))

序列化[fd, fd]时,列表写出后,第一个fd被完整写出并登记引用索引 0;第二个fd写为TYPE_REF指向索引 0。

反序列化时,r_object读到列表,然后读第一个TYPE_FROZENDICT:创建临时 dict、填充键值,再PyFrozenDict_New转为frozendictr_ref_insert登记。此时若r_ref_insert的返回路径存在问题(例如返回 NULL 或未正确更新引用表条目),当r_object继续读到第二个元素是TYPE_REF并尝试解析索引 0 时,就可能取不到有效对象,最终抛出ValueError

此外还有一个更隐蔽的嵌套场景(Lib/test/test_marshal.py 中的test_shared_reference_frozendict覆盖):

nested = marshal.loads(marshal.dumps(frozendict({'x': fd, 'y': fd}))) self.assertIs(nested['x'], nested['y'])

同一个fd作为外层frozendict的两个值。外层frozendict自身要等两个值都反序列化完成后才能PyFrozenDict_New并补登记;而在解析第二个值yTYPE_REF时,需要能从引用表中取回x解析出的那个frozendict。这条"值引用外层尚未完成登记对象"的路径正是修复的关键所在。

循环引用与错误路径的防御

测试文件 Lib/test/test_marshal.py 中同时还覆盖了frozendict的循环引用与非法自引用数据:

def test_reference_loop_frozendict(self): a = frozendict({None: []}) a[None].append(a) # frozendict 通过可变值间接自引用 for v in range(marshal.version + 1): self.assertRaises(ValueError, marshal.dumps, a, v) def test_loads_abnormal_reference_loops(self): ... b'\xfdN[\x01\x00\x00\x00r\x00\x00\x00\x000', # frozendict({None: [<R>]}) b'\xfdN{Nr\x00\x00\x00\x0000', # frozendict({None: {None: <R>}) ...

由于frozendict不可变、其键必须可哈希,直接让frozendict自引用(如frozendict({None: <自身>}))在 Python 层无法构造,但这些"异常引用环"仍可通过手工构造的字节串喂给marshal.loads,因此测试以ValueError断言这些畸形数据被正确拒绝——这正是 NEWS 条目中提到的"instead of failing to load withValueError"所指的另一侧:合法共享引用必须成功,畸形引用环必须报错。

修复方案:r_ref_reserve+r_ref_insert延迟登记

从源码结构可以推断,本次修复在反序列化TYPE_FROZENDICT分支中引入了与TYPE_FROZENSET分支(Python/marshal.c)一致的**延迟登记(delayed registration)**模式:

  1. 先预留索引idx = r_ref_reserve(flag, p)在引用表中占一个坑位,但不立即放入对象;
  2. 构建临时 dict:逐条读入键值对,PyDict_SetItem填充;
  3. 转换为不可变对象PyFrozenDict_New(v)生成最终frozendict
  4. 补登记r_ref_insert(v, idx, flag, p)把最终对象写入预留索引,使后续TYPE_REF能正确解析到它。

对应地,序列化端(Python/marshal.c)在写出TYPE_FROZENDICT后调用w_complete(v, p)完成引用登记,与tuplefrozenset等不可变容器的处理保持一致。frozenset分支中注释 "must use delayed registration of frozensets because they must be init with a refcount of 1"(Python/marshal.c)同样适用于frozendict:不可变对象在完全构造前不能被引用表持有,否则引用计数与共享语义会出错。

修复后,Lib/test/test_marshal.py 中的共享引用测试能够通过:

def test_shared_reference_frozendict(self): # A frozendict referenced more than once must round-trip with the # shared identity preserved, like frozenset. fd = frozendict({'a': 1, 'b': 2}) out = marshal.loads(marshal.dumps([fd, fd])) self.assertEqual(out[0], fd) self.assertIs(out[0], out[1]) nested = marshal.loads(marshal.dumps(frozendict({'x': fd, 'y': fd}))) self.assertIs(nested['x'], nested['y'])

其中assertIs断言的是对象身份共享被保留:反序列化出的两个fd是同一个对象,而非内容相等的两个独立对象——这正是共享引用语义("round-trips with the shared identity preserved, like frozenset")的验证。

版本约束与兼容性影响

需要特别强调的是,frozendictmarshal支持是格式版本 6 专有的。序列化端在 Python/marshal.c 中做了显式检查:

if (PyFrozenDict_CheckExact(v)) { if (p->version < 6) { w_byte(TYPE_UNKNOWN, p); p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_FROZENDICT, p); }

即:当用户指定marshal.dumps(value, version=N)N < 6时,写出frozendict会直接失败(ValueError: unmarshallable object),而不是降级为普通dict——因为降级会破坏往返后类型的一致性。

对使用方的实际影响可归纳为:

  • 默认版本行为marshal.dumps/marshal.dump默认使用Py_MARSHAL_VERSION(当前为 6),因此默认情况下frozendict可正常往返;
  • 低版本兼容:若与旧版本(< 6)生成的marshal数据交互,包含frozendict的数据无法互操作,需使用支持 v6 的 CPython 3.15+;
  • 嵌套共享:修复同时保证了"同一frozendict出现在多个容器中"与"同一frozendict出现在另一个frozendict内部"两种共享场景都能保持对象身份一致;
  • 安全边界marshal的官方文档(Doc/library/marshal.rst)始终警告:不应在不可信数据上调用marshal.loads,因为它可能构造任意对象,存在安全风险。本文讨论的ValueError防御仅是格式健壮性的一部分,不代表marshal可安全处理任意输入。

总结

本次修复针对 CPythonmarshal格式版本 6 中frozendict支持遗留的共享引用缺陷:由于frozendict不可变,无法像dict那样"先登记、再填充",导致被多次引用的frozendict在反序列化时引用表解析失败、抛出ValueError。修复在 Python/marshal.c 的TYPE_FROZENDICT分支中引入r_ref_reserve预留 +PyFrozenDict_New转换 +r_ref_insert补登记的延迟登记流程,使frozendictfrozenset一样能够正确保留共享对象身份。配套测试 Lib/test/test_marshal.py 与循环引用防御测试(Lib/test/test_marshal.py)共同保证了合法共享引用可往返、畸形引用环被拒绝。

对普通开发者而言,该修复的直接影响是:在 CPython 3.15+ 上,包含重复frozendict引用的数据结构可以放心交给marshal.dumps/marshal.loads往返处理;而若需要跨版本或跨语言交换数据,仍应遵循官方建议优先选用pickle或 JSON。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 12:50:44

STC51四轴飞控从原理图到代码:传感器融合与PID控制

简介&#xff1a;基于STC8A8K16S4A12单片机的四轴飞控开源项目&#xff0c;是一份适合航模爱好者与电子DIY初学者的入门级资料包。整套设计聚焦姿态飞行控制&#xff0c;包含完整原理图与C语言源码&#xff0c;可在250mm至750mm轴距多类机架上通过PID调参适配&#xff0c;帮助读…

作者头像 李华
网站建设 2026/9/10 12:49:55

3步装好 ExplorerPatcher:把 Windows 11 界面调回你熟悉的样子

3步装好 ExplorerPatcher&#xff1a;把 Windows 11 界面调回你熟悉的样子 【免费下载链接】ExplorerPatcher This project aims to enhance the working environment on Windows 项目地址: https://gitcode.com/GitHub_Trending/ex/ExplorerPatcher 刚把电脑升到 Windo…

作者头像 李华
网站建设 2026/9/10 12:47:55

CANN/ge图引擎SetMarks接口

SetMarks 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华
网站建设 2026/9/10 12:46:58

Matlab实现GRU时间序列回归预测:从原理到代码详解

在科研、课程作业和工程辅助决策里&#xff0c;时间序列回归预测任务出现得频率相当高。很多人第一次接触这类问题&#xff0c;是被“GRU”“LSTM”这些名字劝退的&#xff0c;总觉得深度学习模型离自己很远。实际上&#xff0c;用Matlab做一个能跑的GRU时间序列回归预测模型&a…

作者头像 李华
网站建设 2026/9/10 12:46:05

双通道ECG小样本分类:Transformer建模原理与PyTorch实现

简介&#xff1a;本资源是一份面向医学信号处理与深度学习初学者的实战项目&#xff0c;聚焦多导联心电图&#xff08;ECG&#xff09;二分类任务&#xff0c;基于PyTorch实现轻量级Transformer模型&#xff0c;适用于生物医学工程、AI医疗方向的学习者与研究者。压缩包共32个文…

作者头像 李华
网站建设 2026/9/10 12:44:12

TDD时域模态分解:无需激励力的振动振型直接提取方法

简介&#xff1a;本资源是一套基于时域分解&#xff08;TDD&#xff09;方法提取结构模态振型的MATLAB实现代码&#xff0c;面向机械、土木及航空航天等领域的工程技术人员、高校师生与科研初学者&#xff0c;解决结构动态特性分析中模态参数&#xff08;频率、振型&#xff09…

作者头像 李华