Skip to content

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

Open
ibrahim halatci (ihalatci) wants to merge 1 commit into
mainfrom
followup-extract-deparse-assertions
Open

Make the EXTRACT deparse injection test detect an injected table in any schema#8821
ibrahim halatci (ihalatci) wants to merge 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.

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.

1 participant