PostgreSQL 更新组合类型的 domain 类型数组导致崩溃
最近发现 PostgreSQL 在更新组合类型构成的 domain 数组将导致数据库崩溃。在写这篇文章的时候这个 bug 已经被修复了。
现象
为了复现这个问题,我们使用 998d060f3d 的代码来进行复现。首先,我们通过下面的 SQL 准备一个基本的测试环境。
1 | CREATE TYPE comptype AS (cf1 int, cf2 int); |
接着,我们向表中插入数据,此时将导致 coredump。
1 | postgres=# INSERT INTO dcomptable (f1[0].cf1) VALUES (4); |
日志如下所示:
1 | 2021-10-20 11:14:22.313 CST [649100] LOG: server process (PID 649976) was terminated by signal 11: Segmentation fault |
分析
当程序崩溃后,堆栈信息如下:
1 | (gdb) bt |
这里 pg_detoast_datum()
函数的值为空导致了崩溃,函数 pg_detoast_datum()
由 DatumGetHeapTupleHeader()
调用,如下所示。
1 | /* |
然而这里 tupDatum
是空的,因此,为了避免出现崩溃,我们可以在这里加上一个判断,如下所示:
1 | diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c |
重新编译执行测试 SQL 语句。
1 | postgres=# INSERT INTO dcomptable (f1[0].cf1) VALUES (4); |
似乎一切都正常了,然而真的是这样么?我们来尝试一下更新语句。
1 | postgres=# UPDATE dcomptable SET f1[0].cf2 = 10; |
WTF? 为什么 f1[0].cf1
的值丢失了呢?我们尝试同时更新 cf1
和 cf2
,看看会发生什么。
1 | postgres=# UPDATE dcomptable SET f1[0].cf2 = 1, f1[0].cf1 = 2; |
这里仅仅保留了最后一个值(f1[0].cf1
)。
因此,这就迫使我们重新思考上面的 patch 的正确性了(当时并没有那么仔细的想过这个问题)。为什么当初这里并没有判断 tupDatum
是否为空呢?是否存在某种前提,从而可以断定这里不会存在空的情况呢?
到这里,Tom Lane 大神站出来了。
This patch seems quite misguided to me. The proximate cause of
the crash is that we’re arriving at ExecEvalFieldStoreDeForm with
*op->resnull and *op->resvalue both zero, which is a completely
invalid situation for a pass-by-reference datatype; so something
upstream of this messed up. Even if there were an argument for
acting as though that were a valid NULL value, this patch fails to
do so; that’d require setting all the output fieldstore.nulls[]
entries to true, which you didn’t.
大意就是在 ExecEvalFieldStoreDeForm()
函数中不应该出现 *op->resnull
和 *op->resvalue
同时为空的情况,换言之,若出现这种情况,那么肯定是上层某个地方出错了。随后,大神也给出了解释。
After some digging around, I see where the issue actually is:
the expression tree we’re dealing with looks like{SUBSCRIPTINGREF :refexpr {VAR } :refassgnexpr {COERCETODOMAIN :arg {FIELDSTORE :arg {CASETESTEXPR } } } }
The array element we intend to replace has to be passed down to
the CaseTestExpr, but that isn’t happening. That’s because
isAssignmentIndirectionExpr fails to recognize a tree like
this, so ExecInitSubscriptingRef doesn’t realize it needs to
arrange for that.
这是由于 isAssignmentIndirectionExpr()
在处理间接表达式是遗忘了 domain
数组类型导致的。最终的解决方案如下:
1 | diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c |
再次测试,一切正常。
1 | postgres=# UPDATE dcomptable SET f1[0].cf2 = 1, f1[0].cf1 = 2; |
官坐堂,众后中有撒一响屁者。
官即叫:“拿来!”
隶禀曰:“老爷,屁是一阵风,吹散没影踪,叫小的如何拿得?”
官怒云:“为何徇情卖放,定要拿到。”
皂无奈,只得取干屎回销:“禀老爷,正犯是走了,拿得家属在此。”