Thread
-
Fix bug of COPY (on_error set_null)
Chao Li <li.evan.chao@gmail.com> — 2026-05-15T01:12:35Z
Hi, I just tested “Add COPY (column list) (on_error set_null) option”. While tracing a normal case, I found a mistake: In BeginCopyFrom(), cstate->domain_with_constraint is allocated using the length of the specified column list, and set using the index in that column list: ``` 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() * will cover most of the cases, but it does not verify domain * constraints. Therefore, for constrained domains, the null value * check must be performed during the initial string-to-datum * conversion (see CopyFromTextLikeOneRow()). */ cstate->domain_with_constraint = palloc0_array(bool, attr_count); <== allocate with length of column list from SQL 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); <= set with index of column list from SQL } ``` However, cstate->domain_with_constraint is read in CopyFromTextLikeOneRow(), where it is accessed using the actual attribute number: ``` /* Loop to read the user attributes on the line. */ foreach(cur, cstate->attnumlist) { int attnum = lfirst_int(cur); int m = attnum - 1; ... if (!cstate->domain_with_constraint[m] || ``` So, if the column list specified in SQL is shorter than the table’s actual attribute list, this may cause an out-of-bounds read. Based on this finding, it was easy to create a repro: ``` evantest=# CREATE DOMAIN dnn AS int NOT NULL; CREATE DOMAIN evantest=# CREATE TABLE t (a int, b dnn); CREATE TABLE evantest=# COPY t(b) FROM STDIN WITH (on_error set_null); Enter data to be copied followed by a newline. End with a backslash and a period on a line by itself, or an EOF signal. >> x >> \. NOTICE: in 1 row, columns were set to null due to data type incompatibility COPY 1 evantest=# select * from t; a | b ---+--- | (1 row) ``` Here, the table has two columns, and the COPY command only specifies column b. Since I gave invalid input for b, COPY tries to set it to NULL. However, b is a NOT NULL domain, so the COPY should fail. Instead, a row with NULL in b is successfully inserted, which proves this is a real bug. To fix the bug, there are two options: 1. In BeginCopyFrom(), allocate cstate->domain_with_constraint using the actual number of table attributes, and set it by actual attribute number. 2. In CopyFromTextLikeOneRow(), access cstate->domain_with_constraint using the index in the SQL column list. I chose option 1, because it keeps cstate->domain_with_constraint in the same form as other arrays such as cstate->typioparams[m], which makes the code easier to read. This may use a little extra memory, but since cstate->domain_with_constraint is a small boolean array and is short-lived, I don’t think that matters much. While here, I also noticed a few palloc calls that could be switched to palloc_array, so I changed them as well. Best regards, -- Chao Li (Evan) HighGo Software Co., Ltd. https://www.highgo.com/ -
Re: Fix bug of COPY (on_error set_null)
jian he <jian.universality@gmail.com> — 2026-05-16T05:17:24Z
On Fri, May 15, 2026 at 9:13 AM Chao Li <li.evan.chao@gmail.com> wrote: > > Hi, > > I just tested “Add COPY (column list) (on_error set_null) option”. While tracing a normal case, I found a mistake: > > In BeginCopyFrom(), cstate->domain_with_constraint is allocated using the length of the specified column list, and set using the index in that column list: > ``` > 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() > * will cover most of the cases, but it does not verify domain > * constraints. Therefore, for constrained domains, the null value > * check must be performed during the initial string-to-datum > * conversion (see CopyFromTextLikeOneRow()). > */ > cstate->domain_with_constraint = palloc0_array(bool, attr_count); <== allocate with length of column list from SQL > > 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); <= set with index of column list from SQL > } > ``` > > However, cstate->domain_with_constraint is read in CopyFromTextLikeOneRow(), where it is accessed using the actual attribute number: > ``` > /* Loop to read the user attributes on the line. */ > foreach(cur, cstate->attnumlist) > { > int attnum = lfirst_int(cur); > int m = attnum - 1; > > ... > if (!cstate->domain_with_constraint[m] || > ``` > > So, if the column list specified in SQL is shorter than the table’s actual attribute list, this may cause an out-of-bounds read. > Hi. This appears to be the same issue as reported here: https://postgr.es/m/CAHg+QDdej0c0gWJi2FnbirzhgzyZNPiTwC1P5B_-dSNCzq-91A@mail.gmail.com -
Re: Fix bug of COPY (on_error set_null)
Chao Li <li.evan.chao@gmail.com> — 2026-05-18T22:22:15Z
> On May 16, 2026, at 13:17, jian he <jian.universality@gmail.com> wrote: > > On Fri, May 15, 2026 at 9:13 AM Chao Li <li.evan.chao@gmail.com> wrote: >> >> Hi, >> >> I just tested “Add COPY (column list) (on_error set_null) option”. While tracing a normal case, I found a mistake: >> >> In BeginCopyFrom(), cstate->domain_with_constraint is allocated using the length of the specified column list, and set using the index in that column list: >> ``` >> 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() >> * will cover most of the cases, but it does not verify domain >> * constraints. Therefore, for constrained domains, the null value >> * check must be performed during the initial string-to-datum >> * conversion (see CopyFromTextLikeOneRow()). >> */ >> cstate->domain_with_constraint = palloc0_array(bool, attr_count); <== allocate with length of column list from SQL >> >> 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); <= set with index of column list from SQL >> } >> ``` >> >> However, cstate->domain_with_constraint is read in CopyFromTextLikeOneRow(), where it is accessed using the actual attribute number: >> ``` >> /* Loop to read the user attributes on the line. */ >> foreach(cur, cstate->attnumlist) >> { >> int attnum = lfirst_int(cur); >> int m = attnum - 1; >> >> ... >> if (!cstate->domain_with_constraint[m] || >> ``` >> >> So, if the column list specified in SQL is shorter than the table’s actual attribute list, this may cause an out-of-bounds read. >> > Hi. > > This appears to be the same issue as reported here: > https://postgr.es/m/CAHg+QDdej0c0gWJi2FnbirzhgzyZNPiTwC1P5B_-dSNCzq-91A@mail.gmail.com Thanks for pointing out that. I will review that patch. Best regards, -- Chao Li (Evan) HighGo Software Co., Ltd. https://www.highgo.com/