From ff9e5823164714e14cba5e70099c051bc2571e70 Mon Sep 17 00:00:00 2001 From: Satya Narlapuram Date: Thu, 16 Apr 2026 16:43:34 +0000 Subject: [PATCH] Fix COPY FROM ON_ERROR SET_NULL OOB read with partial column list domain_with_constraint[] was allocated with list_length(attnumlist) elements and indexed sequentially via foreach_current_index(), but copyfromparse.c accesses it via attnum - 1 (physical attribute index). With a partial column list targeting high-numbered columns, this caused an out-of-bounds read that bypassed domain NOT NULL checks, silently inserting NULL into NOT NULL domain columns. Fix by allocating with num_phys_attrs and indexing by attnum - 1, consistent with all other per-column arrays in BeginCopyFrom(). --- src/backend/commands/copyfrom.c | 8 ++------ src/test/regress/expected/copy2.out | 23 +++++++++++++++++++++++ src/test/regress/sql/copy2.sql | 19 +++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index 64ac3063..0087585b 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -1636,8 +1636,6 @@ BeginCopyFrom(ParseState *pstate, if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) { - int attr_count = list_length(cstate->attnumlist); - /* * When data type conversion fails and ON_ERROR is SET_NULL, we need * ensure that the input column allow null values. ExecConstraints() @@ -1646,15 +1644,13 @@ BeginCopyFrom(ParseState *pstate, * check must be performed during the initial string-to-datum * conversion (see CopyFromTextLikeOneRow()). */ - cstate->domain_with_constraint = palloc0_array(bool, attr_count); + cstate->domain_with_constraint = palloc0_array(bool, num_phys_attrs); foreach_int(attno, cstate->attnumlist) { - int i = foreach_current_index(attno); - Form_pg_attribute att = TupleDescAttr(tupDesc, attno - 1); - cstate->domain_with_constraint[i] = DomainHasConstraints(att->atttypid, NULL); + cstate->domain_with_constraint[attno - 1] = DomainHasConstraints(att->atttypid, NULL); } } diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 7600e523..2d6bfdd8 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -839,6 +839,28 @@ SELECT * FROM t_on_error_null ORDER BY a; 13 | 14 | NULL (3 rows) +-- Test on_error set_null with partial column list and domain NOT NULL. +-- Bug: domain_with_constraint[] was indexed by physical attnum, but +-- allocated with only as many elements as the COPY column list. +-- With a partial column list targeting a high-numbered column, this +-- caused an out-of-bounds read that bypassed the domain NOT NULL check. +CREATE TABLE t_on_error_null_partial ( + c1 text, c2 text, c3 text, c4 text, c5 text, + c6 text, c7 text, c8 text, c9 text, + c10 d_int_not_null -- attnum 10, domain NOT NULL +); +-- Partial column list: attnumlist = {1, 10}. Should fail because +-- the domain on c10 does not allow null, and 'bad' is not an integer. +COPY t_on_error_null_partial(c1, c10) FROM STDIN WITH (on_error set_null); -- fail +ERROR: domain d_int_not_null does not allow null values +DETAIL: ON_ERROR SET_NULL cannot be applied because column "c10" (domain d_int_not_null) does not accept null values. +CONTEXT: COPY t_on_error_null_partial, line 1, column c10: "bad" +SELECT count(*) AS "expect_0" FROM t_on_error_null_partial; + expect_0 +---------- + 0 +(1 row) + \pset null '' -- tests for on_error option with log_verbosity and null constraint via domain CREATE DOMAIN dcheck_ign_err2 varchar(15) NOT NULL; @@ -900,6 +922,7 @@ DROP VIEW instead_of_insert_tbl_view_2; DROP FUNCTION fun_instead_of_insert_tbl(); DROP TABLE check_ign_err; DROP TABLE t_on_error_null; +DROP TABLE t_on_error_null_partial; DROP DOMAIN d_int_not_null; DROP DOMAIN d_int_positive_maybe_null; DROP TABLE check_ign_err2; diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql index e0810109..287c8fb8 100644 --- a/src/test/regress/sql/copy2.sql +++ b/src/test/regress/sql/copy2.sql @@ -580,6 +580,24 @@ COPY t_on_error_null FROM STDIN WITH (on_error set_null, log_verbosity verbose); SELECT * FROM t_on_error_null ORDER BY a; +-- Test on_error set_null with partial column list and domain NOT NULL. +-- Bug: domain_with_constraint[] was indexed by physical attnum, but +-- allocated with only as many elements as the COPY column list. +-- With a partial column list targeting a high-numbered column, this +-- caused an out-of-bounds read that bypassed the domain NOT NULL check. +CREATE TABLE t_on_error_null_partial ( + c1 text, c2 text, c3 text, c4 text, c5 text, + c6 text, c7 text, c8 text, c9 text, + c10 d_int_not_null -- attnum 10, domain NOT NULL +); +-- Partial column list: attnumlist = {1, 10}. Should fail because +-- the domain on c10 does not allow null, and 'bad' is not an integer. +COPY t_on_error_null_partial(c1, c10) FROM STDIN WITH (on_error set_null); -- fail +hello bad +\. + +SELECT count(*) AS "expect_0" FROM t_on_error_null_partial; + \pset null '' -- tests for on_error option with log_verbosity and null constraint via domain @@ -652,6 +670,7 @@ DROP VIEW instead_of_insert_tbl_view_2; DROP FUNCTION fun_instead_of_insert_tbl(); DROP TABLE check_ign_err; DROP TABLE t_on_error_null; +DROP TABLE t_on_error_null_partial; DROP DOMAIN d_int_not_null; DROP DOMAIN d_int_positive_maybe_null; DROP TABLE check_ign_err2; -- 2.43.0