From cf754f7414ba6159733ee9cdffde90a3cee3c32c Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 10 Aug 2026 06:37:50 -0700 Subject: [PATCH] Save/restore more lexer state when skipping text due to \if. When we implemented \if ... \endif in psql, we arranged to save/restore the lexer's parenthesis depth counter across any chunk of input that we're ignoring. At the time, that was sufficient, because no other part of PsqlScanState could need to be restored to its prior value. However, commit e717a9a18 and follow-ons added more state fields that ought to be restored to their prior values. A problem would only be observed if someone tries to \if out a portion of a CREATE FUNCTION/PROCEDURE command that is relevant to BEGIN/END matching, which seems like a pretty unusual usage, so the lack of field reports isn't surprising. Nonetheless it's a bug. To fix, replace the simple counter field in ConditionalStack entries with a pointer to a struct defined by psqlscan_int.h. (In the back branches, keep the old field and associated functions to minimize the risk of API/ABI breakage, even though it seems unlikely that any third-party code is using this. Making the new struct private to psqlscan-related code should prevent API/ABI issues for future additions of this type.) In itself this is only a minor bug fix, but it's prerequisite infrastructure for the fix for CVE-2026-6464, which will add another such field. Author: Tom Lane Reviewed-by: Noah Misch Backpatch-through: 14 Security: CVE-2026-6464 --- src/bin/psql/command.c | 16 +++++------- src/bin/psql/psqlscanslash.h | 5 ++-- src/bin/psql/psqlscanslash.l | 40 +++++++++++++++++++++++------ src/fe_utils/conditional.c | 28 ++++++++++++-------- src/include/fe_utils/conditional.h | 16 ++++++------ src/include/fe_utils/psqlscan.h | 3 +++ src/include/fe_utils/psqlscan_int.h | 20 +++++++++++++++ src/test/regress/expected/psql.out | 16 ++++++++++++ src/test/regress/sql/psql.sql | 11 ++++++++ src/tools/pgindent/typedefs.list | 1 + 10 files changed, 119 insertions(+), 37 deletions(-) diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index ee85c05a00d..f2a69bdbb25 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -3793,8 +3793,8 @@ is_branching_command(const char *cmd) * Prepare to possibly restore query buffer to its current state * (cf. discard_query_text). * - * We need to remember the length of the query buffer, and the lexer's - * notion of the parenthesis nesting depth. + * We need to remember the length of the query buffer, and assorted + * lexer internal state such as parenthesis nesting depth. */ static void save_query_text_state(PsqlScanState scan_state, ConditionalStack cstack, @@ -3802,8 +3802,8 @@ save_query_text_state(PsqlScanState scan_state, ConditionalStack cstack, { if (query_buf) conditional_stack_set_query_len(cstack, query_buf->len); - conditional_stack_set_paren_depth(cstack, - psql_scan_get_paren_depth(scan_state)); + conditional_stack_set_lex_state(cstack, + psql_scan_get_lex_state(scan_state)); } /* @@ -3812,9 +3812,7 @@ save_query_text_state(PsqlScanState scan_state, ConditionalStack cstack, * We must discard data that was appended to query_buf during an inactive * \if branch. We don't have to do anything there if there's no query_buf. * - * Also, reset the lexer state to the same paren depth there was before. - * (The rest of its state doesn't need attention, since we could not be - * inside a comment or literal or partial token.) + * Also, reset the lexer's state to what it was before. */ static void discard_query_text(PsqlScanState scan_state, ConditionalStack cstack, @@ -3828,8 +3826,8 @@ discard_query_text(PsqlScanState scan_state, ConditionalStack cstack, query_buf->len = new_len; query_buf->data[new_len] = '\0'; } - psql_scan_set_paren_depth(scan_state, - conditional_stack_get_paren_depth(cstack)); + psql_scan_set_lex_state(scan_state, + conditional_stack_get_lex_state(cstack)); } /* diff --git a/src/bin/psql/psqlscanslash.h b/src/bin/psql/psqlscanslash.h index d95a6ff2d1e..2ab08d5520b 100644 --- a/src/bin/psql/psqlscanslash.h +++ b/src/bin/psql/psqlscanslash.h @@ -31,9 +31,10 @@ extern char *psql_scan_slash_option(PsqlScanState state, extern void psql_scan_slash_command_end(PsqlScanState state); -extern int psql_scan_get_paren_depth(PsqlScanState state); +extern PsqlScanStateSave *psql_scan_get_lex_state(PsqlScanState state); -extern void psql_scan_set_paren_depth(PsqlScanState state, int depth); +extern void psql_scan_set_lex_state(PsqlScanState state, + const PsqlScanStateSave *lex_state); extern void dequote_downcase_identifier(char *str, bool downcase, int encoding); diff --git a/src/bin/psql/psqlscanslash.l b/src/bin/psql/psqlscanslash.l index 2d755752374..837158ae0d3 100644 --- a/src/bin/psql/psqlscanslash.l +++ b/src/bin/psql/psqlscanslash.l @@ -705,22 +705,46 @@ psql_scan_slash_command_end(PsqlScanState state) } /* - * Fetch current paren nesting depth + * Save current lexer state + * + * Relevant parts of the state are returned in a pg_malloc'd struct. + * It is caller's responsibility to free the struct eventually. */ -int -psql_scan_get_paren_depth(PsqlScanState state) +PsqlScanStateSave * +psql_scan_get_lex_state(PsqlScanState state) { - return state->paren_depth; + PsqlScanStateSave *lex_state = pg_malloc_object(PsqlScanStateSave); + StaticAssertDecl(sizeof(lex_state->init_idents) == sizeof(state->init_idents), + "init_idents array lengths must match"); + StaticAssertDecl(sizeof(lex_state->sub_idents) == sizeof(state->sub_idents), + "sub_idents array lengths must match"); + + lex_state->paren_depth = state->paren_depth; + lex_state->begin_depth = state->begin_depth; + lex_state->init_idents_count = state->init_idents_count; + memcpy(lex_state->init_idents, state->init_idents, + sizeof(lex_state->init_idents)); + lex_state->sub_idents_count = state->sub_idents_count; + memcpy(lex_state->sub_idents, state->sub_idents, + sizeof(lex_state->sub_idents)); + return lex_state; } /* - * Set paren nesting depth + * Restore lexer state to what it was when saved */ void -psql_scan_set_paren_depth(PsqlScanState state, int depth) +psql_scan_set_lex_state(PsqlScanState state, + const PsqlScanStateSave *lex_state) { - Assert(depth >= 0); - state->paren_depth = depth; + state->paren_depth = lex_state->paren_depth; + state->begin_depth = lex_state->begin_depth; + state->init_idents_count = lex_state->init_idents_count; + memcpy(state->init_idents, lex_state->init_idents, + sizeof(state->init_idents)); + state->sub_idents_count = lex_state->sub_idents_count; + memcpy(state->sub_idents, lex_state->sub_idents, + sizeof(state->sub_idents)); } /* diff --git a/src/fe_utils/conditional.c b/src/fe_utils/conditional.c index 537c76ed3cd..2d09d21ecb1 100644 --- a/src/fe_utils/conditional.c +++ b/src/fe_utils/conditional.c @@ -56,7 +56,7 @@ conditional_stack_push(ConditionalStack cstack, ifState new_state) p->if_state = new_state; p->query_len = -1; - p->paren_depth = -1; + p->lex_state = NULL; p->next = cstack->head; cstack->head = p; } @@ -73,6 +73,8 @@ conditional_stack_pop(ConditionalStack cstack) if (!p) return false; cstack->head = cstack->head->next; + if (p->lex_state) + free(p->lex_state); free(p); return true; } @@ -167,23 +169,29 @@ conditional_stack_get_query_len(ConditionalStack cstack) } /* - * Save current parenthesis nesting depth in topmost stack entry. + * Save current lexer state in topmost stack entry. + * + * The lexer state is presumed to be a single pg_malloc'd chunk. + * It will be freed automatically when the stack entry is popped. */ void -conditional_stack_set_paren_depth(ConditionalStack cstack, int depth) +conditional_stack_set_lex_state(ConditionalStack cstack, + PsqlScanStateSave *lex_state) { Assert(!conditional_stack_empty(cstack)); - cstack->head->paren_depth = depth; + if (cstack->head->lex_state) /* free old state, if any */ + free(cstack->head->lex_state); + cstack->head->lex_state = lex_state; } /* - * Fetch last-recorded parenthesis nesting depth from topmost stack entry. - * Will return -1 if no stack or it was never saved. + * Fetch last-recorded lexer state from topmost stack entry. + * Will return NULL if no stack or it was never saved. */ -int -conditional_stack_get_paren_depth(ConditionalStack cstack) +PsqlScanStateSave * +conditional_stack_get_lex_state(ConditionalStack cstack) { if (conditional_stack_empty(cstack)) - return -1; - return cstack->head->paren_depth; + return NULL; + return cstack->head->lex_state; } diff --git a/src/include/fe_utils/conditional.h b/src/include/fe_utils/conditional.h index 2ed796b5c50..96efd81a7c8 100644 --- a/src/include/fe_utils/conditional.h +++ b/src/include/fe_utils/conditional.h @@ -49,17 +49,16 @@ typedef enum ifState * query_len is used to determine what accumulated text to throw away at the * end of an inactive branch. (We could, perhaps, teach the lexer to not add * stuff to the query buffer in the first place when inside an inactive branch; - * but that would be very invasive.) We also need to save and restore the - * lexer's parenthesis nesting depth when throwing away text. (We don't need - * to save and restore any of its other state, such as comment nesting depth, - * because a backslash command could never appear inside a comment or SQL - * literal.) + * but that would be very invasive.) We also need to save and restore some + * lexer state, such as parenthesis nesting depth, when throwing away text. */ +typedef struct PsqlScanStateSave PsqlScanStateSave; /* opaque outside lexer */ + typedef struct IfStackElem { ifState if_state; /* current state, see enum above */ int query_len; /* length of query_buf at last branch start */ - int paren_depth; /* parenthesis depth at last branch start */ + PsqlScanStateSave *lex_state; /* lexer state at last branch start */ struct IfStackElem *next; /* next surrounding \if, if any */ } IfStackElem; @@ -95,8 +94,9 @@ extern void conditional_stack_set_query_len(ConditionalStack cstack, int len); extern int conditional_stack_get_query_len(ConditionalStack cstack); -extern void conditional_stack_set_paren_depth(ConditionalStack cstack, int depth); +extern void conditional_stack_set_lex_state(ConditionalStack cstack, + PsqlScanStateSave *lex_state); -extern int conditional_stack_get_paren_depth(ConditionalStack cstack); +extern PsqlScanStateSave *conditional_stack_get_lex_state(ConditionalStack cstack); #endif /* CONDITIONAL_H */ diff --git a/src/include/fe_utils/psqlscan.h b/src/include/fe_utils/psqlscan.h index abd44fa140b..f3e972e8892 100644 --- a/src/include/fe_utils/psqlscan.h +++ b/src/include/fe_utils/psqlscan.h @@ -26,6 +26,9 @@ /* Abstract type for lexer's internal state */ typedef struct PsqlScanStateData *PsqlScanState; +/* Abstract type for state save/restore */ +typedef struct PsqlScanStateSave PsqlScanStateSave; + /* Termination states for psql_scan() */ typedef enum { diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h index 8b0d153261b..ada1089f70d 100644 --- a/src/include/fe_utils/psqlscan_int.h +++ b/src/include/fe_utils/psqlscan_int.h @@ -132,6 +132,26 @@ typedef struct PsqlScanStateData void *cb_passthrough; } PsqlScanStateData; +/* + * Conditional scanning (\if ... \endif) needs to be able to reset the + * lexer's state to what it was at the beginning of a chunk of text that + * we choose to ignore. PsqlScanStateSave holds the values that need + * to be saved and restored. We assume that saving/restoring happens only + * while processing a backslash command, so we needn't save state that is + * concerned with comment or SQL literal processing: we won't be inside + * one of those. + */ +typedef struct PsqlScanStateSave +{ + int paren_depth; /* depth of nesting in parentheses */ + int begin_depth; /* depth of begin/end pairs */ + int init_idents_count; /* # identifiers since start of statement */ + char init_idents[4]; /* records the first few identifiers */ + int sub_idents_count; /* # identifiers since start of a CREATE + * SCHEMA element */ + char sub_idents[4]; /* records the first few of those identifiers */ +} PsqlScanStateSave; + /* * Functions exported by psqlscan.l, but only meant for use within diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 42635a56a06..7e67f795f25 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -4789,6 +4789,22 @@ invalid command \lo \echo 'should print #8-1' should print #8-1 \endif +-- test that begin/end matching ignores to-be-ignored text +create function silly_function(int) returns int +begin atomic select $1; +\if false +end +\endif +; +end; +\sf silly_function(int) +CREATE OR REPLACE FUNCTION public.silly_function(integer) + RETURNS integer + LANGUAGE sql +BEGIN ATOMIC + SELECT $1; +END +drop function silly_function(int); -- :{?...} defined variable test \set i 1 \if :{?i} diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e51767b3e2f..42eca3b8a6b 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1126,6 +1126,17 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \echo 'should print #8-1' \endif +-- test that begin/end matching ignores to-be-ignored text +create function silly_function(int) returns int +begin atomic select $1; +\if false +end +\endif +; +end; +\sf silly_function(int) +drop function silly_function(int); + -- :{?...} defined variable test \set i 1 \if :{?i} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 85d989f395d..eb3dd5481e6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2466,6 +2466,7 @@ PsqlScanQuoteType PsqlScanResult PsqlScanState PsqlScanStateData +PsqlScanStateSave PsqlSettings Publication PublicationActions