Add test for early backend startup errors
authorHeikki Linnakangas <[email protected]>
Fri, 26 Jul 2024 12:12:21 +0000 (15:12 +0300)
committerHeikki Linnakangas <[email protected]>
Fri, 26 Jul 2024 12:12:21 +0000 (15:12 +0300)
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

doc/src/sgml/xfunc.sgml
src/backend/tcop/backend_startup.c
src/backend/utils/misc/injection_point.c
src/include/utils/injection_point.h
src/interfaces/libpq/Makefile
src/interfaces/libpq/meson.build
src/interfaces/libpq/t/005_negotiate_encryption.pl

index 7e92e898460f7acb6681b58946af044da43c6f01..5b584a4f14446b4eb9ab5f537cb740a006940457 100644 (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>
index a2f94b105048cf8edff12e53d4a3daeafd50602a..b840d95e4d1e1fbb6411dd5302cd4ec093775a32 100644 (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.
index 8ab5bc6327621e62d95ed586b194dded259e445d..80bc32b0e08eda7eddfa1f5f6c26b0bcbf759956 100644 (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
+}
index a5b4a1f0f9f5d0596519ce3abd5e6575fe7b08c5..b4fc677c9b435faeb26edb4b70af65f5054ee81e 100644 (file)
 #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
index b36a7657648a737174d39183cfc8fc791052e7db..27f8499d8a7ef445802ba12014efdb0204761db4 100644 (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"
 
index ed2a4048d18e21d9b9d8bc30ea76788d6c59d1cf..7623aeadab740baeb0df43735bdecb674c551f3d 100644 (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',
index 251c405926b1b58ec98242a38480fc2afcf63666..5fbab969008be747317ef98b38797e62c3557eb8 100644 (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 "-"