导语:这是一次典型的数据库内核内存安全事件——PostgreSQL pltcl 释放后重用漏洞 CVE-2026-116323(CVSS 8.8)允许具备对象创建权限的攻击者,借助 domain CHECK 约束重入 Tcl 解释器,释放外层结果转换仍在引用的字符串,最终以数据库运行操作系统用户的身份执行任意代码。作为长期负责数据库内核与安全加固的架构师,我认为它的价值不在于 CVSS 分数,而在于它精准暴露了“扩展语言运行时 + 类型系统回调”这一组合下长期被忽视的重入语义缺口。
一、PostgreSQL pltcl 释放后重用漏洞的事件全景
根据 GitHub Security Advisory(GHSA-ccfq-qwmr-4cgg)披露,该问题编号为 CVE-2026-116323,严重级别 high,CVSS 3.x 评分 8.8,属于典型的“高权限前置 + 高影响输出”组合。受影响版本覆盖 PostgreSQL 18.7、17.12、16.16、15.20、14.25 之前的全部主线分支,意味着从 14 到 18 的绝大多数生产集群都在射程范围内。
漏洞的一句话描述非常关键:“Use after free in PostgreSQL pltcl allows an object creator to execute arbitrary code as the operating system user running the database, via a domain CHECK that re-enters the Tcl interpreter and frees or changes a string still used by the outer result conversion.” 这句话里包含了三个必须被拆开理解的技术要素:PL/Tcl 的结果转换路径、domain CHECK 约束的重入能力、以及字符串对象在重入过程中被释放或改写。三者叠加,才构成完整的 PostgreSQL pltcl 释放后重用攻击面。
不要把该漏洞简单归类为“扩展语言的小 bug”。它本质上是数据库类型系统(domain 约束)与外部语言运行时(Tcl 解释器)之间的重入契约缺失。任何在类型转换回调中允许执行用户可控逻辑的设计,都必须重新审视其内存生命周期假设。
企业级架构规范
flowchart TD
subgraph 接入层 ["表现层与接入网关 (Client & Gateway Tier)"]
A["攻击者以对象创建者身份发起调用"] --> B["SQL 函数调用进入 PL/Tcl 执行器"]
end
subgraph 规则层 ["规则编排与上下文层 (Policy & Context Tier)"]
B --> C["Tcl 过程返回结果字符串"]
C --> D["结果转换准备目标类型 (domain 类型)"]
end
subgraph 领域层 ["核心执行与重入引擎 (Core Reentry Tier)"]
D --> E["domain CHECK 约束被触发"]
E --> F["CHECK 表达式重入 Tcl 解释器"]
F --> G["内层执行释放或改写外层字符串"]
G --> H["外层结果转换继续读取已释放内存 (UAF)"]
end
subgraph 存储层 ["基础设施与执行环境 (Infrastructure Tier)"]
H --> I["堆内存被可控数据覆盖"]
I --> J["以数据库运行 OS 用户身份执行任意代码"]
end
二、PL/Tcl 结果转换与 domain CHECK 重入机制拆解
2.1 PL/Tcl 的结果转换路径
PL/Tcl 作为 PostgreSQL 的过程语言扩展,其核心工作模式是:数据库调用一个用 Tcl 编写的函数,Tcl 过程执行完毕后返回一个字符串(或字符串列表),PostgreSQL 再通过类型输入函数(type input function)将其转换为目标 SQL 类型。对于普通标量类型,这条路径相对直接;但当目标类型是 domain 时,转换过程会额外触发 domain 的约束检查(CHECK 约束)。
问题就出在这个“额外触发”上。domain CHECK 约束的表达式本身可以调用函数,而在 PostgreSQL 的权限模型中,对象创建者(object creator)可以在自己拥有的 domain 上定义 CHECK 约束,并在其中调用自己拥有的 PL/Tcl 函数。这就为“外层转换尚未完成、内层逻辑已经重入”埋下了伏笔。
2.2 重入如何击穿内存生命周期假设
在正常的非重入路径中,PL/Tcl 结果转换的代码会假设:从 Tcl 解释器取出的字符串指针,在转换完成之前保持有效。这个假设在单线程、无回调的语境下成立。但 domain CHECK 的重入打破了它:
- 外层:PL/Tcl 拿到 Tcl 返回的字符串 S,准备将其转换为 domain 类型,持有指向 S 的引用。
- 内层:domain CHECK 表达式被求值,重入 Tcl 解释器,执行攻击者控制的 Tcl 代码。
- 释放:内层代码通过 Tcl 的字符串管理机制释放或重新分配了 S 所在的内存。
- 回归:外层转换继续使用已经失效的 S 指针,形成典型的 use-after-free。
一旦 UAF 成立,攻击者就可以通过精心构造的 Tcl 代码控制被释放内存的再分配内容,将函数指针或关键数据结构覆盖为可控值,从而在数据库进程上下文中实现任意代码执行。由于 PostgreSQL 后端进程以数据库运行用户(通常是 postgres)身份运行,攻击者获得的正是该操作系统用户的权限。
很多团队会认为“只有超级用户才能创建 PL/Tcl 函数,所以风险可控”。但该漏洞明确指向 object creator:在允许普通业务账号创建 domain、创建函数、并被授予 PL/Tcl 使用权限的多租户或 SaaS 场景中,攻击面会被显著放大。请立即审计 pg_language 中 pltcl 的 ACL 与 domain 创建权限的授予情况。
三、影响范围与版本矩阵
官方修复覆盖了五个主线分支。对于仍在使用 14 及更早版本、或已经 EOL 的集群,需要特别注意:这些版本不会获得补丁,唯一的出路是升级到受支持的主线版本。
| 主线版本 | 受影响版本 | 修复版本 | 处置建议 |
|---|---|---|---|
| PostgreSQL 18 | < 18.7 | 18.7 | 立即升级 |
| PostgreSQL 17 | < 17.12 | 17.12 | 立即升级 |
| PostgreSQL 16 | < 16.16 | 16.16 | 立即升级 |
| PostgreSQL 15 | < 15.20 | 15.20 | 立即升级 |
| PostgreSQL 14 | < 14.25 | 14.25 | 立即升级 |
| PostgreSQL ≤ 13 | 全部 | 无 | 规划迁移 |
四、PostgreSQL pltcl 释放后重用漏洞的工程化加固方案
4.1 第一优先级:版本升级
对于 PostgreSQL pltcl 释放后重用这类内核内存安全漏洞,升级是唯一彻底的修复手段。建议采用滚动升级策略,先在只读副本上验证,再切换主库。以下为基于包管理器的升级命令示例,请按实际发行版调整。
点击展开:Debian/Ubuntu 与 RHEL 系升级命令与验证脚本
# Debian / Ubuntu (PGDG 源) sudo apt update sudo apt install --only-upgrade postgresql-16 postgresql-client-16 # RHEL / Rocky / AlmaLinux sudo dnf update postgresql16-server postgresql16-libs # 验证版本 psql -c "SELECT version();" # 检查 pltcl 扩展是否安装及 ACL psql -c "SELECT lanname, lanacl FROM pg_language WHERE lanname LIKE 'pltcl%';"
4.2 第二优先级:收缩 PL/Tcl 与 domain 的攻击面
在无法立即升级的窗口期,应通过权限收敛降低可利用性。核心思路是:不让非受信角色同时具备“创建 domain + 创建函数 + 使用 pltcl”这三项能力。
-- 1. 回收 PUBLIC 对 pltcl 的使用权限
REVOKE USAGE ON LANGUAGE pltcl FROM PUBLIC;
REVOKE USAGE ON LANGUAGE pltclu FROM PUBLIC;
-- 2. 审计当前拥有 pltcl 使用权限的角色
SELECT r.rolname
FROM pg_language l, aclexplode(l.lanacl) a, pg_roles r
WHERE l.lanname = 'pltcl' AND a.grantee = r.oid;
-- 3. 审计业务 schema 中 domain 的创建权限
SELECT n.nspname, has_schema_privilege('app_user', n.oid, 'CREATE') AS can_create
FROM pg_namespace n
WHERE n.nspname NOT LIKE 'pg_%' AND n.nspname <> 'information_schema';
若你的业务确实依赖 PL/Tcl(例如历史遗留的 Tcl 存储过程),在升级前请勿贸然 REVOKE,否则会导致业务函数调用失败。正确顺序是:先盘点依赖 → 建立专用低权限角色承载 pltcl 函数 → 再回收 PUBLIC 权限 → 最后升级内核。任何一步跳过都可能引发生产事故。
4.3 第三优先级:运行时检测与监控
UAF 类漏洞在利用阶段往往伴随异常的内存分配模式与后端进程崩溃。建议在数据库侧开启 log_min_error_statement 并采集后端进程的 core dump 信号(SIGSEGV/SIGABRT),同时结合 pg_stat_activity 对长时间运行的 Tcl 函数调用进行告警。对于高安全等级环境,可考虑在数据库进程外层部署 seccomp 或 AppArmor 策略,限制 postgres 用户的 execve 能力,作为 RCE 的最后一道纵深防御。
五、架构层面的长期反思
这已经不是 PostgreSQL 扩展语言第一次因重入问题进入安全公告。从架构视角看,PL/Tcl、PL/Perl、PL/Python 这类“嵌入式解释器”天然引入了控制流重入与内存所有权跨边界两个难题。类型系统的输入/输出函数、domain 约束、触发器、以及事件触发器,都是潜在的重入入口。真正稳健的设计应当在扩展语言边界处引入显式的“结果快照”语义:在进入任何可能回调用户代码的路径之前,先将结果字符串复制到由 PostgreSQL 内存上下文(MemoryContext)独立管理的内存中,从根本上切断外层引用与内层释放之间的耦合。
对于使用 PostgreSQL pltcl 释放后重用漏洞作为安全基线检查项的团队,建议将“扩展语言权限审计”和“domain 约束回调审计”纳入常规的数据库安全评审清单,而不是等到下一个 CVE 出现才被动响应。
