Skip to content

Make the EXTRACT deparse injection test detect an injected table in any schema - #8821

Merged
ibrahim halatci (ihalatci) merged 1 commit into
mainfrom
followup-extract-deparse-assertions
Sep 4, 2026
Merged

ibrahim halatci (ihalatci) merged 1 commit into
mainfrom
followup-extract-deparse-assertions

Conversation

@ihalatci

Copy link
Copy Markdown
Contributor

DESCRIPTION: Make the EXTRACT deparse injection test detect an injected table in any schema

Follow-up to the non-blocking review comments on #8804. Test-only, no product code changes.

1. The injection check could not detect the injection it guards against

The assertion looked for the table the payload creates via
to_regclass('extract_deparse.injected') IS NULL. That only finds an injected
table if it lands in the test schema, and it never does.

The deparser fully qualifies task SQL on purpose - PushEmptySearchPath(),
"Set search_path to NIL so that all objects outside of pg_catalog will be
schema-prefixed"
(ruleutils_17.c:655, ruleutils_18.c:683,
ruleutils_19.c:689). Because names arrive fully qualified, Citus does not send
a SET search_path for SELECT task execution. So a successful
CREATE TABLE injected() on a worker is created in the worker session's default
search_path (public), not in extract_deparse.

The old assertion therefore returned true even if the injection had
succeeded
- a silent false negative in a security regression test.

Now the relation is looked up by name instead, so the test fails wherever the
injected table lands:

SELECT bool_and(result::int = 0) AS extract_field_injection_blocked
FROM run_command_on_workers($$
	SELECT count(*) FROM pg_class WHERE relname = 'injected'
$$);

This matches how the rest of the suite does worker-side existence checks, e.g.
citus_internal_distribute_object.sql (L181, L188, L190, L203, L210).

The same problem and fix apply to pg19.sql, which used pg19_repack.injected.

2. Added a positive control

The payload is expected to raise invalid_parameter_value, and the DO block
swallows it, so the only assertion was a negative one. If a planner change ever
stopped the expression being pushed down, nothing would run on a worker,
injected would be absent, and the test would keep passing without covering the
deparse path at all.

SELECT EXTRACT('year' FROM ts) = 2026 AS extract_field_pushdown_works
FROM extract_deparse_source
WHERE id = 1;

This also gives extract_deparse.sql real coverage on PG19, where the injection
block is a no-op by design (guarded on server_version_num < 190000) and the
PG19 case is covered by pg19.sql.

3. Not changed: the 190000 literal

The review asked whether a literal PG version is the convention. It is - there
is no macro or helper for version gating in regress tests. The only other use of
server_version_num under src/test/regress/sql/ is
multi_orderby_limit_pushdown.sql:186, which uses the identical idiom:

IF current_setting('server_version_num')::int >= 190000 THEN

Left as-is.

The injection check looked for the table the payload creates with
to_regclass('<schema>.injected'), but the deparser fully qualifies task
SQL on purpose (PushEmptySearchPath), so Citus never sets search_path
on a worker for a SELECT task. A successful injection therefore creates
the table in the worker's default search_path, not in the test schema,
and the assertion would still report success.

Look the relation up by name in pg_class instead, which is how the
other run_command_on_workers checks in the suite do it, so the test
fails wherever the injected table lands.

Also add a positive control. The payload is expected to raise
invalid_parameter_value and the DO block swallows it, so the only
assertion was a negative one: if a future change stopped the expression
from being pushed down, nothing would run on a worker and the test
would keep passing without covering the deparse path. Asserting that
EXTRACT with a valid field still returns the right value pins that
path. This also gives the file real coverage on PG19, where the
injection block is skipped in favour of pg19.sql.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7c6370b2-06fd-4491-bf92-ecb811d34518
@codecov

codecov Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.72%. Comparing base (92d0b92) to head (2ceef2f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8821      +/-   ##
==========================================
- Coverage   88.73%   88.72%   -0.01%     
==========================================
  Files         289      289              
  Lines       65013    65013              
  Branches     8203     8204       +1     
==========================================
- Hits        57691    57686       -5     
- Misses       4954     4955       +1     
- Partials     2368     2372       +4     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ihalatci
ibrahim halatci (ihalatci) merged commit 2cfa425 into main Sep 4, 2026
159 checks passed
@ihalatci
ibrahim halatci (ihalatci) deleted the followup-extract-deparse-assertions branch September 4, 2026 14:50
ibrahim halatci (ihalatci) added a commit that referenced this pull request Sep 7, 2026
…ase-14.0) (#8827)

DESCRIPTION: Backport extract_deparse security regression test hardening
to release-14.0

Backport of #8821 (merged to `main` as 2cfa425) to `release-14.0`.

The `extract_deparse` security regression test — which guards the
EXTRACT
identifier-quoting fix — had two silent-pass paths:

1. It checked `to_regclass('extract_deparse.injected') IS NULL`, but the
deparser
fully-qualifies task SQL and Citus sends no `SET search_path` for SELECT
task
execution, so a successful injection would create `injected` in the
worker's
   default `public` schema. The assertion therefore passed even when the
   injection succeeded. Replaced with a schema-agnostic
`count(*) FROM pg_class WHERE relname = 'injected'`, matching the
existing
   idiom in `citus_internal_distribute_object.sql`.

2. If a planner change stopped pushing the expression down, nothing
would run on
a worker and the test would still pass. Added a positive control
asserting the
   same expression with a valid field is still pushed down.

Test-only change, +22/-4 across `sql/extract_deparse.sql` and
`expected/extract_deparse.out`. The `pg19.*` half of #8821 is
intentionally
excluded — those files do not exist on this branch.

`extract_deparse` runs under `check-multi-1-create-citus` on this
branch, which
passed on PG16, PG17 and PG18 in the branch run (33888997131), and
passed 8/8 in
the flakyness shard.

Known pre-existing failures on this branch, unrelated to this change
(neither job
executes `extract_deparse`, which is not in `multi_schedule`):
- `Test Citus Lib N-1 / PG18 - check-multi` — fails on
`multi_insert_select` in
  every recent branch run (33168333398, 32951765361, 31358631548).
- `Test Citus SQL N-1 / PG18 - check-multi` — fails on `pg17` due to a
missing
  `ORDER BY` in a `pg_inherits` query, so partition rows come back in
  nondeterministic order.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7c6370b2-06fd-4491-bf92-ecb811d34518
ibrahim halatci (ihalatci) added a commit that referenced this pull request Sep 8, 2026
…ase-12.1) (#8826)

DESCRIPTION: Backport extract_deparse security regression test hardening
to release-12.1

Backport of #8821 (merged to `main` as 2cfa425) to `release-12.1`.

The `extract_deparse` security regression test — which guards the
EXTRACT
identifier-quoting fix — had two silent-pass paths:

1. It checked `to_regclass('extract_deparse.injected') IS NULL`, but the
deparser
fully-qualifies task SQL and Citus sends no `SET search_path` for SELECT
task
execution, so a successful injection would create `injected` in the
worker's
   default `public` schema. The assertion therefore passed even when the
   injection succeeded. Replaced with a schema-agnostic
`count(*) FROM pg_class WHERE relname = 'injected'`, matching the
existing
   idiom in `citus_internal_distribute_object.sql`.

2. If a planner change stopped pushing the expression down, nothing
would run on
a worker and the test would still pass. Added a positive control
asserting the
   same expression with a valid field is still pushed down.

Test-only change, +22/-4 across `sql/extract_deparse.sql` and
`expected/extract_deparse.out`. The `pg19.*` half of #8821 is
intentionally
excluded — those files do not exist on this branch.

Note: `extract_deparse` runs under `check-multi-1` on this branch
(`multi_1_schedule`), not `check-multi-1-create-citus` as on
release-13.2/14.0.

Prior CI on this branch (manual dispatch, run 33889067381): 95 jobs, 0
failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7c6370b2-06fd-4491-bf92-ecb811d34518
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants