Segfault: `id()` compared against a variable-bound list crashes the backend (`master` and PG18 1.8.0)
- Dominant language
- C
- Stars
- 4.8k
- Forks
- 523
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 9
Description
### Summary
On `master` (`8014174`) and on the `PG18` branch (= `release/PG18/1.8.0`, both at
`e43dc1a`), a Cypher query that
compares `id()` against a list **held in a variable** dereferences a `graphid` as if it
were a varlena pointer and terminates the backend with `SIGSEGV`. PostgreSQL then
recycles every other backend and enters crash recovery, so one ordinary query takes
down the instance.
`release/PG18/1.7.0` built from source on the identical base image is unaffected.
### Minimal reproduction
```sql
CREATE EXTENSION IF NOT EXISTS age CASCADE;
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
SELECT create_graph('gtest');
SELECT * FROM cypher('gtest', $$ CREATE (:P {name: 'a'}) $$) AS (v agtype);
-- crashes on 1.8.0; returns 0 on 1.7.0
SELECT count(*) FROM cypher('gtest',
$$ WITH [1] AS ids MATCH (n) WHERE id(n) IN ids RETURN n $$) AS (n agtype);
```
The `CREATE` is required: the crash only happens once `MATCH` actually produces a row
(see the scope table). A single vertex is enough, and the list element does not have to
be a real id — `[1]` matches nothing and still crashes.
Server log from running exactly the snippet above against a freshly created graph:
```
LOG: client backend (PID 141) was terminated by signal 11: Segmentation fault
DETAIL: Failed process was running: SELECT count(*) FROM cypher('gtest',
$$ WITH [1] AS ids MATCH (n) WHERE id(n) IN ids RETURN n $$) AS (n agtype);
LOG: terminating any other active server processes
LOG: all server processes terminated; reinitializing
```
### Backtrace
Core dump from a `-O0 -g3` build:
```
Program terminated with signal SIGSEGV, Segmentation fault.
#0 pg_detoast_datum ()
#1 agtype_in_operator (fcinfo=0x...) at src/backend/utils/adt/agtype.c:5052
#2-#6 [ExecQual / ExecScan]
#7 standard_ExecutorRun ()
#9 PortalRun ()
#11 PostgresMain ()
```
`agtype.c:5052` is the fetch of the **left-hand** operand of `IN`:
```c
5051 /* get the item to search for */
5052 agt_item = AG_GET_ARG_AGTYPE_P(1);
```
### Root cause
`agtype_in_operator` is **byte-for-byte identical** between 1.7.0 and 1.8.0 (I diffed the
function body). What changed is the type of what reaches it.
On 1.8.0 `id()` produces `graphid`, not `agtype`:
```
-- 1.8.0
SELECT i FROM cypher('gtest', $$ MATCH (n:P) RETURN id(n) $$) AS (i bigint);
ERROR: cannot cast type graphid to bigint for column "i"
-- 1.7.0: returns 844424930131969
```
(`agtype` → `bigint` still works on 1.8.0 — e.g. `count()` declared `bigint` — so this is
specific to `id()`'s new output type. This is consistent with 1.8.0 introducing native
`vertex` / `edge` types: `pg_cast` gains `vertex→agtype` and `edge→agtype`, and `pg_proc`
gains `vertex_to_agtype` / `edge_to_agtype`, none of which exist on 1.7.0.)
`graphid` is a **pass-by-value int64**. When the planner selects `agtype_in_operator`
without inserting the `graphid→agtype` cast, line 5052 hands that raw integer to
`pg_detoast_datum`, which dereferences it as a pointer.
The variable-bound list is what lets this through. With a *multi-element literal* list,
type resolution instead fails cleanly before execution:
```
-- 1.8.0
MATCH (n) WHERE id(n) IN [1,2] RETURN n
ERROR: not a common type: 16461
```
(The OID is assigned at `CREATE EXTENSION` time, so it differs per install. Here
`SELECT 16461::regtype` → `ag_catalog.agtype`; a second install of the same version
reported `16462` for the same error, which likewise resolves to `ag_catalog.agtype`.)
So the same underlying type change produces a clean error on one path and a segfault on
the other.
### Scope
Each probe run in its own session against the same seeded graph, on both versions built
identically. `v` denotes a variable bound by `WITH`.
| probe | query | 1.7.0 | 1.8.0 |
|---|---|---|---|
| id + IN + var-list | `WITH [1] AS v MATCH (n) WHERE id(n) IN v` | ok (0) | **SEGV** |
| id + IN + var-list, real ids | `MATCH (x:P) WITH collect(id(x)) AS v MATCH (n) WHERE id(n) IN v` | ok (2) | **SEGV** |
| id + IN + 1-elem literal | `MATCH (n) WHERE id(n) IN [1]` | ok (0) | ok (0) |
| id + IN + n-elem literal | `MATCH (n) WHERE id(n) IN [1,2]` | ok (0) | `ERROR: not a common type: 16461` |
| **prop** + IN + var-list | `WITH ['a'] AS v MATCH (n) WHERE n.name IN v` | ok (1) | ok (1) |
| id + **=** + var index | `WITH [1] AS v MATCH (n) WHERE id(n) = v[0]` | ok (0) | ok (0) |
| id + = + literal | `MATCH (n) WHERE id(n) = 1` | ok (0) | ok (0) |
| var-list alone | `WITH [1] AS v MATCH (n) RETURN n` | ok (2) | ok (2) |
| id + IN + var, **0 rows matched** | same, on a graph with no vertices | ok (0) | ok (0) |
| id + IN + var, **0 rows matched** | same, `MATCH (n:Absent)` | ok (0) | ok (0) |
Dropping any one ingredient avoids the crash: replacing `id()` with a property, replacing
`IN` with `=`, or replacing the variable with a literal list all run clean. The last two
rows show the operator is only reached once a row survives `MATCH`, which is why the
repro needs at least one vertex.
### Environment
Both legs built from the same `Dockerfile` — same base image, same toolchain, same flags
— so AGE version is the only variable:
- base: `postgres:18` (PostgreSQL 18.4, Debian), `aarch64`
- crashing: `release/PG18/1.8.0` @ `e43dc1a12b78fba4acef9835b2b10379b8d243b4`
- working: `release/PG18/1.7.0` @ `806fa2ebdb300b3e76ef30cdba61803babbf2683`
- built with `make COPT="-O0 -g3 -fno-omit-frame-pointer"` for the backtrace
`release/PG18/1.8.0` and the `PG18` branch head are currently the **same commit**, so this
is the state of the PG18 development tip, not just a tagged RC.
**`master` is affected too.** Built from the same Dockerfile (`--build-arg AGE_BRANCH=master`,
commit `801417404978823bd8732452c3f7959017584785`) on the same `postgres:18` base, the
snippet above crashes identically:
```
LOG: client backend (PID 96) was terminated by signal 11: Segmentation fault
DETAIL: Failed process was running: SELECT count(*) FROM cypher('gtest', ...
LOG: all server processes terminated; reinitializing
```
So this is not confined to the PG18 release lineage — a fix needs to land on `master` as well.
### Impact
`WITH [] AS node_ids MATCH (a) WHERE id(a) IN node_ids` is the natural way to fetch a
subgraph for a known vertex set, and it is what LightRAG's PostgreSQL graph backend
issues, so a single ordinary API call crashes the server. Docker Hub currently has no
`release_PG18_1.8.0` image, so this is not yet in users' hands.
Contributor guide
Research direction
Start in src/backend/utils/adt/agtype.c at agtype_in_operator and line 5052, then trace how graphid reaches the IN operator when the list is bound to a variable. Run the supplied SQL reproduction and scope probes against the affected and working versions. Done means the variable-bound id() IN list no longer terminates the backend and the reported non-crashing cases retain their expected results.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, postgresql
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100