Add test for early backend startup errors

The new test tests the libpq fallback behavior on an early error,
which was fixed in the previous commit.

This adds an IS_INJECTION_POINT_ATTACHED() macro, to allow writing
injected test code alongside the normal source code. In principle, the
new test could've been implemented by an extra test module with a
callback that sets the FrontendProtocol global variable, but I think
it's more clear to have the test code right where the injection point
is, because it has pretty intimate knowledge of the surrounding
context it runs in.

Reviewed-by: Michael Paquier
Discussion: https://www.postgresql.org/message-id/CAOYmi%2Bnwvu21mJ4DYKUa98HdfM_KZJi7B1MhyXtnsyOO-PB6Ww%40mail.gmail.com
This commit is contained in:
Heikki Linnakangas 2024-07-26 15:12:21 +03:00
parent b9e5249c29
commit 20e0e7da9b
7 changed files with 118 additions and 1 deletions

View File

@ -3672,6 +3672,31 @@ custom_injection_callback(const char *name, const void *private_data)
logic.
</para>
<para>
An alternative way to define the action to take when an injection point
is reached is to add the testing code alongside the normal source
code. This can be useful if the action e.g. depends on local variables
that are not accessible to loaded modules. The
<function>IS_INJECTION_POINT_ATTACHED</function> macro can then be used
to check if an injection point is attached, for example:
<programlisting>
#ifdef USE_INJECTION_POINTS
if (IS_INJECTION_POINT_ATTACHED("before-foobar"))
{
/* change a local variable if injection point is attached */
local_var = 123;
/* also execute the callback */
INJECTION_POINT_CACHED("before-foobar");
}
#endif
</programlisting>
Note that the callback attached to the injection point will not be
executed by the <function>IS_INJECTION_POINT_ATTACHED</function>
macro. If you want to execute the callback, you must also call
<function>INJECTION_POINT_CACHED</function> like in the above example.
</para>
<para>
Optionally, it is possible to detach an injection point by calling:
<programlisting>

View File

@ -33,6 +33,7 @@
#include "tcop/backend_startup.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/injection_point.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
#include "utils/timeout.h"
@ -213,6 +214,21 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
remote_host)));
}
/* For testing client error handling */
#ifdef USE_INJECTION_POINTS
INJECTION_POINT("backend-initialize");
if (IS_INJECTION_POINT_ATTACHED("backend-initialize-v2-error"))
{
/*
* This simulates an early error from a pre-v14 server, which used the
* version 2 protocol for any errors that occurred before processing
* the startup packet.
*/
FrontendProtocol = PG_PROTOCOL(2, 0);
elog(FATAL, "protocol version 2 error triggered");
}
#endif
/*
* If we did a reverse lookup to name, we might as well save the results
* rather than possibly repeating the lookup during authentication.

View File

@ -570,3 +570,17 @@ InjectionPointCached(const char *name)
elog(ERROR, "Injection points are not supported by this build");
#endif
}
/*
* Test if an injection point is defined.
*/
bool
IsInjectionPointAttached(const char *name)
{
#ifdef USE_INJECTION_POINTS
return InjectionPointCacheRefresh(name) != NULL;
#else
elog(ERROR, "Injection points are not supported by this build");
return false; /* silence compiler */
#endif
}

View File

@ -18,10 +18,12 @@
#define INJECTION_POINT_LOAD(name) InjectionPointLoad(name)
#define INJECTION_POINT(name) InjectionPointRun(name)
#define INJECTION_POINT_CACHED(name) InjectionPointCached(name)
#define IS_INJECTION_POINT_ATTACHED(name) IsInjectionPointAttached(name)
#else
#define INJECTION_POINT_LOAD(name) ((void) name)
#define INJECTION_POINT(name) ((void) name)
#define INJECTION_POINT_CACHED(name) ((void) name)
#define IS_INJECTION_POINT_ATTACHED(name) (false)
#endif
/*
@ -41,6 +43,7 @@ extern void InjectionPointAttach(const char *name,
extern void InjectionPointLoad(const char *name);
extern void InjectionPointRun(const char *name);
extern void InjectionPointCached(const char *name);
extern bool IsInjectionPointAttached(const char *name);
extern bool InjectionPointDetach(const char *name);
#ifdef EXEC_BACKEND

View File

@ -9,11 +9,13 @@
#
#-------------------------------------------------------------------------
EXTRA_INSTALL=src/test/modules/injection_points
subdir = src/interfaces/libpq
top_builddir = ../../..
include $(top_builddir)/src/Makefile.global
export with_ssl with_gssapi with_krb_srvnam
export with_ssl with_gssapi with_krb_srvnam enable_injection_points
PGFILEDESC = "PostgreSQL Access Library"

View File

@ -121,6 +121,7 @@ tests += {
't/005_negotiate_encryption.pl',
],
'env': {
'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
'with_ssl': ssl_library,
'with_gssapi': gssapi.found() ? 'yes' : 'no',
'with_krb_srvnam': 'postgres',

View File

@ -90,6 +90,8 @@ my $kerberos_enabled =
$ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bkerberos\b/;
my $ssl_supported = $ENV{with_ssl} eq 'openssl';
my $injection_points_supported = $ENV{enable_injection_points} eq 'yes';
###
### Prepare test server for GSSAPI and SSL authentication, with a few
### different test users and helper functions. We don't actually
@ -155,6 +157,10 @@ $node->safe_psql('postgres', 'CREATE USER ssluser;');
$node->safe_psql('postgres', 'CREATE USER nossluser;');
$node->safe_psql('postgres', 'CREATE USER gssuser;');
$node->safe_psql('postgres', 'CREATE USER nogssuser;');
if ($injection_points_supported != 0)
{
$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
}
my $unixdir = $node->safe_psql('postgres', 'SHOW unix_socket_directories;');
chomp($unixdir);
@ -312,6 +318,29 @@ nossluser . disable postgres connect, authok
['disable'], \@all_sslmodes, \@all_sslnegotiations,
parse_table($test_table));
if ($injection_points_supported != 0)
{
$node->safe_psql(
'postgres',
"SELECT injection_points_attach('backend-initialize', 'error');",
connstr => "user=localuser host=$unixdir");
connect_test(
$node,
"user=testuser sslmode=prefer",
'connect, backenderror -> fail');
$node->restart;
$node->safe_psql(
'postgres',
"SELECT injection_points_attach('backend-initialize-v2-error', 'error');",
connstr => "user=localuser host=$unixdir");
connect_test(
$node,
"user=testuser sslmode=prefer",
'connect, v2error -> fail');
$node->restart;
}
# Disable SSL again
$node->adjust_conf('postgresql.conf', 'ssl', 'off');
$node->reload;
@ -393,6 +422,29 @@ nogssuser disable disable postgres connect, authok
test_matrix($node, [ 'testuser', 'gssuser', 'nogssuser' ],
\@all_gssencmodes, $sslmodes, $sslnegotiations,
parse_table($test_table));
if ($injection_points_supported != 0)
{
$node->safe_psql(
'postgres',
"SELECT injection_points_attach('backend-initialize', 'error');",
connstr => "user=localuser host=$unixdir");
connect_test(
$node,
"user=testuser gssencmode=prefer sslmode=disable",
'connect, backenderror, reconnect, backenderror -> fail');
$node->restart;
$node->safe_psql(
'postgres',
"SELECT injection_points_attach('backend-initialize-v2-error', 'error');",
connstr => "user=localuser host=$unixdir");
connect_test(
$node,
"user=testuser gssencmode=prefer sslmode=disable",
'connect, v2error -> fail');
$node->restart;
}
}
###
@ -738,6 +790,10 @@ sub parse_log_events
push @events, "gssreject" if $line =~ /GSSENCRequest rejected/;
push @events, "authfail" if $line =~ /no pg_hba.conf entry/;
push @events, "authok" if $line =~ /connection authenticated/;
push @events, "backenderror"
if $line =~ /error triggered for injection point backend-/;
push @events, "v2error"
if $line =~ /protocol version 2 error triggered/;
}
# No events at all is represented by "-"