Skip to content

Commit 689f75d

Browse files
committed
Fix limitations on what SQL commands can be issued to a walsender.
In logical replication mode, a WalSender is supposed to be able to execute any regular SQL command, as well as the special replication commands. Poor design of the replication-command parser caused it to fail in various cases, notably: * semicolons embedded in a command, or multiple SQL commands sent in a single message; * dollar-quoted literals containing odd numbers of single or double quote marks; * commands starting with a comment. The basic problem here is that we're trying to run repl_scanner.l across the entire input string even when it's not a replication command. Since repl_scanner.l does not understand all of the token types known to the core lexer, this is doomed to have failure modes. We certainly don't want to make repl_scanner.l as big as scan.l, so instead rejigger stuff so that we only lex the first token of a non-replication command. That will usually look like an IDENT to repl_scanner.l, though a comment would end up getting reported as a '-' or '/' single-character token. If the token is a replication command keyword, we push it back and proceed normally with repl_gram.y parsing. Otherwise, we can drop out of exec_replication_command() without examining the rest of the string. (It's still theoretically possible for repl_scanner.l to fail on the first token; but that could only happen if it's an unterminated single- or double-quoted string, in which case you'd have gotten largely the same error from the core lexer too.) In this way, repl_gram.y isn't involved at all in handling general SQL commands, so we can get rid of the SQLCmd node type. (In the back branches, we can't remove it because renumbering enum NodeTag would be an ABI break; so just leave it sit there unused.) I failed to resist the temptation to clean up some other sloppy coding in repl_scanner.l while at it. The only externally-visible behavior change from that is it now accepts \r and \f as whitespace, same as the core lexer. Per bug #17379 from Greg Rychlewski. Back-patch to all supported branches. Discussion: https://postgr.es/m/17379-6a5c6cfb3f1f5e77@postgresql.org
1 parent a8ce5c8 commit 689f75d

File tree

4 files changed

+97
-58
lines changed

4 files changed

+97
-58
lines changed

src/backend/replication/repl_gram.y

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
/* Result of the parsing is returned here */
2626
Node *replication_parse_result;
2727

28-
static SQLCmd *make_sqlcmd(void);
29-
3028

3129
/*
3230
* Bison doesn't allocate anything that needs to live across parser calls,
@@ -59,7 +57,6 @@ static SQLCmd *make_sqlcmd(void);
5957
%token <str> SCONST IDENT
6058
%token <uintval> UCONST
6159
%token <recptr> RECPTR
62-
%token T_WORD
6360

6461
/* Keyword tokens. */
6562
%token K_BASE_BACKUP
@@ -91,7 +88,7 @@ static SQLCmd *make_sqlcmd(void);
9188
%type <node> command
9289
%type <node> base_backup start_replication start_logical_replication
9390
create_replication_slot drop_replication_slot identify_system
94-
timeline_history show sql_cmd
91+
timeline_history show
9592
%type <list> base_backup_opt_list
9693
%type <defelt> base_backup_opt
9794
%type <uintval> opt_timeline
@@ -124,7 +121,6 @@ command:
124121
| drop_replication_slot
125122
| timeline_history
126123
| show
127-
| sql_cmd
128124
;
129125

130126
/*
@@ -400,25 +396,6 @@ plugin_opt_arg:
400396
| /* EMPTY */ { $$ = NULL; }
401397
;
402398

403-
sql_cmd:
404-
IDENT { $$ = (Node *) make_sqlcmd(); }
405-
;
406399
%%
407400

408-
static SQLCmd *
409-
make_sqlcmd(void)
410-
{
411-
SQLCmd *cmd = makeNode(SQLCmd);
412-
int tok;
413-
414-
/* Just move lexer to the end of command. */
415-
for (;;)
416-
{
417-
tok = yylex();
418-
if (tok == ';' || tok == 0)
419-
break;
420-
}
421-
return cmd;
422-
}
423-
424401
#include "repl_scanner.c"

src/backend/replication/repl_scanner.l

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ fprintf_to_ereport(const char *fmt, const char *msg)
3131
/* Handle to the buffer that the lexer uses internally */
3232
static YY_BUFFER_STATE scanbufhandle;
3333

34+
/* Pushed-back token (we only handle one) */
35+
static int repl_pushed_back_token;
36+
37+
/* Work area for collecting literals */
3438
static StringInfoData litbuf;
3539

3640
static void startlit(void);
@@ -51,7 +55,18 @@ static void addlitchar(unsigned char ychar);
5155
%option warn
5256
%option prefix="replication_yy"
5357

54-
%x xq xd
58+
/*
59+
* Exclusive states:
60+
* <xd> delimited identifiers (double-quoted identifiers)
61+
* <xq> standard single-quoted strings
62+
*/
63+
%x xd
64+
%x xq
65+
66+
space [ \t\n\r\f]
67+
68+
quote '
69+
quotestop {quote}
5570

5671
/* Extended quote
5772
* xqdouble implements embedded quote, ''''
@@ -69,11 +84,8 @@ xdstop {dquote}
6984
xddouble {dquote}{dquote}
7085
xdinside [^"]+
7186

72-
digit [0-9]+
73-
hexdigit [0-9A-Za-z]+
74-
75-
quote '
76-
quotestop {quote}
87+
digit [0-9]
88+
hexdigit [0-9A-Fa-f]
7789

7890
ident_start [A-Za-z\200-\377_]
7991
ident_cont [A-Za-z\200-\377_0-9\$]
@@ -82,6 +94,19 @@ identifier {ident_start}{ident_cont}*
8294

8395
%%
8496

97+
%{
98+
/* This code is inserted at the start of replication_yylex() */
99+
100+
/* If we have a pushed-back token, return that. */
101+
if (repl_pushed_back_token)
102+
{
103+
int result = repl_pushed_back_token;
104+
105+
repl_pushed_back_token = 0;
106+
return result;
107+
}
108+
%}
109+
85110
BASE_BACKUP { return K_BASE_BACKUP; }
86111
FAST { return K_FAST; }
87112
IDENTIFY_SYSTEM { return K_IDENTIFY_SYSTEM; }
@@ -108,14 +133,7 @@ NOEXPORT_SNAPSHOT { return K_NOEXPORT_SNAPSHOT; }
108133
USE_SNAPSHOT { return K_USE_SNAPSHOT; }
109134
WAIT { return K_WAIT; }
110135

111-
"," { return ','; }
112-
";" { return ';'; }
113-
"(" { return '('; }
114-
")" { return ')'; }
115-
116-
[\n] ;
117-
[\t] ;
118-
" " ;
136+
{space}+ { /* do nothing */ }
119137

120138
{digit}+ {
121139
yylval.uintval = strtoul(yytext, NULL, 10);
@@ -177,16 +195,18 @@ WAIT { return K_WAIT; }
177195
return IDENT;
178196
}
179197

198+
. {
199+
/* Any char not recognized above is returned as itself */
200+
return yytext[0];
201+
}
202+
180203
<xq,xd><<EOF>> { yyerror("unterminated quoted string"); }
181204

182205

183206
<<EOF>> {
184207
yyterminate();
185208
}
186209

187-
. {
188-
return T_WORD;
189-
}
190210
%%
191211

192212
/* LCOV_EXCL_STOP */
@@ -246,6 +266,7 @@ replication_scanner_init(const char *str)
246266

247267
/* Make sure we start in proper state */
248268
BEGIN(INITIAL);
269+
repl_pushed_back_token = 0;
249270
}
250271

251272
void
@@ -254,3 +275,34 @@ replication_scanner_finish(void)
254275
yy_delete_buffer(scanbufhandle);
255276
scanbufhandle = NULL;
256277
}
278+
279+
/*
280+
* Check to see if the first token of a command is a WalSender keyword.
281+
*
282+
* To keep repl_scanner.l minimal, we don't ask it to know every construct
283+
* that the core lexer knows. Therefore, we daren't lex more than the
284+
* first token of a general SQL command. That will usually look like an
285+
* IDENT token here, although some other cases are possible.
286+
*/
287+
bool
288+
replication_scanner_is_replication_command(void)
289+
{
290+
int first_token = replication_yylex();
291+
292+
switch (first_token)
293+
{
294+
case K_IDENTIFY_SYSTEM:
295+
case K_BASE_BACKUP:
296+
case K_START_REPLICATION:
297+
case K_CREATE_REPLICATION_SLOT:
298+
case K_DROP_REPLICATION_SLOT:
299+
case K_TIMELINE_HISTORY:
300+
case K_SHOW:
301+
/* Yes; push back the first token so we can parse later. */
302+
repl_pushed_back_token = first_token;
303+
return true;
304+
default:
305+
/* Nope; we don't bother to push back the token. */
306+
return false;
307+
}
308+
}

src/backend/replication/walsender.c

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,7 +1453,8 @@ exec_replication_command(const char *cmd_string)
14531453
*/
14541454
if (MyWalSnd->state == WALSNDSTATE_STOPPING)
14551455
ereport(ERROR,
1456-
(errmsg("cannot execute new commands while WAL sender is in stopping mode")));
1456+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
1457+
errmsg("cannot execute new commands while WAL sender is in stopping mode")));
14571458

14581459
/*
14591460
* CREATE_REPLICATION_SLOT ... LOGICAL exports a snapshot until the next
@@ -1464,41 +1465,49 @@ exec_replication_command(const char *cmd_string)
14641465
CHECK_FOR_INTERRUPTS();
14651466

14661467
/*
1467-
* Parse the command.
1468+
* Prepare to parse and execute the command.
14681469
*/
14691470
cmd_context = AllocSetContextCreate(CurrentMemoryContext,
14701471
"Replication command context",
14711472
ALLOCSET_DEFAULT_SIZES);
14721473
old_context = MemoryContextSwitchTo(cmd_context);
14731474

14741475
replication_scanner_init(cmd_string);
1475-
parse_rc = replication_yyparse();
1476-
if (parse_rc != 0)
1477-
ereport(ERROR,
1478-
(errcode(ERRCODE_SYNTAX_ERROR),
1479-
errmsg_internal("replication command parser returned %d",
1480-
parse_rc)));
1481-
replication_scanner_finish();
1482-
1483-
cmd_node = replication_parse_result;
14841476

14851477
/*
1486-
* If it's a SQL command, just clean up our mess and return false; the
1487-
* caller will take care of executing it.
1478+
* Is it a WalSender command?
14881479
*/
1489-
if (IsA(cmd_node, SQLCmd))
1480+
if (!replication_scanner_is_replication_command())
14901481
{
1491-
if (MyDatabaseId == InvalidOid)
1492-
ereport(ERROR,
1493-
(errmsg("cannot execute SQL commands in WAL sender for physical replication")));
1482+
/* Nope; clean up and get out. */
1483+
replication_scanner_finish();
14941484

14951485
MemoryContextSwitchTo(old_context);
14961486
MemoryContextDelete(cmd_context);
14971487

1488+
/* XXX this is a pretty random place to make this check */
1489+
if (MyDatabaseId == InvalidOid)
1490+
ereport(ERROR,
1491+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1492+
errmsg("cannot execute SQL commands in WAL sender for physical replication")));
1493+
14981494
/* Tell the caller that this wasn't a WalSender command. */
14991495
return false;
15001496
}
15011497

1498+
/*
1499+
* Looks like a WalSender command, so parse it.
1500+
*/
1501+
parse_rc = replication_yyparse();
1502+
if (parse_rc != 0)
1503+
ereport(ERROR,
1504+
(errcode(ERRCODE_SYNTAX_ERROR),
1505+
errmsg_internal("replication command parser returned %d",
1506+
parse_rc)));
1507+
replication_scanner_finish();
1508+
1509+
cmd_node = replication_parse_result;
1510+
15021511
/*
15031512
* Report query to various monitoring facilities. For this purpose, we
15041513
* report replication commands just like SQL commands.

src/include/replication/walsender_private.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ extern int replication_yylex(void);
121121
extern void replication_yyerror(const char *str) pg_attribute_noreturn();
122122
extern void replication_scanner_init(const char *query_string);
123123
extern void replication_scanner_finish(void);
124+
extern bool replication_scanner_is_replication_command(void);
124125

125126
extern Node *replication_parse_result;
126127

0 commit comments

Comments
 (0)