>From f83b781b86e65263890cbe58347063cc1ceabcde Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Fri, 18 Sep 2015 14:17:13 +0300
Subject: [PATCH 1/1] Initial version of libpq mallocfail tester

---
 src/interfaces/libpq/test/Makefile     |  11 +-
 src/interfaces/libpq/test/mallocfail.c |  83 ++++++++
 src/interfaces/libpq/test/mallocfail.h |  23 ++
 src/interfaces/libpq/test/oom-test.c   | 377 +++++++++++++++++++++++++++++++++
 4 files changed, 493 insertions(+), 1 deletion(-)
 create mode 100644 src/interfaces/libpq/test/mallocfail.c
 create mode 100644 src/interfaces/libpq/test/mallocfail.h
 create mode 100644 src/interfaces/libpq/test/oom-test.c

diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index ab41dc3..92fbf7f 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -9,14 +9,23 @@ endif
 override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 override LDLIBS := $(libpq_pgport) $(LDLIBS)
 
-PROGS = uri-regress
+PROGS = uri-regress oom-test
 
 all: $(PROGS)
 
+# The malloc hooks in glibc are marked as deprecated. We want to use them
+# anyway, the alternatives are much more complicated, so just silence the
+# warnings.
+mallocfail.o: CPPFLAGS := -Wno-deprecated-declarations
+
+oom-test$(X): mallocfail.o
+
 installcheck: all
 	SRCDIR='$(top_srcdir)' SUBDIR='$(subdir)' \
 		   $(PERL) $(top_srcdir)/$(subdir)/regress.pl
 
 clean distclean maintainer-clean:
 	rm -f $(PROGS)
+	rm -f mallocfail.o oom-test.o
+	rm uri-regress.o
 	rm -f regress.out regress.diff
diff --git a/src/interfaces/libpq/test/mallocfail.c b/src/interfaces/libpq/test/mallocfail.c
new file mode 100644
index 0000000..e327e9c
--- /dev/null
+++ b/src/interfaces/libpq/test/mallocfail.c
@@ -0,0 +1,83 @@
+/*
+ * mallocfail.c
+ *		Helper functions for simulating malloc() failures.
+ *
+ * This uses glibc-specific malloc hooks to hijack calls to malloc().
+ * Call mallocfail_init() to initialize. After that, this module will call
+ * a function called "mallocfail_callback" on every malloc() call, which must
+ * be defined elsewhere. If the callback returns 1, we return NULL to the
+ * caller of malloc(), otherwise we let the allocation to succeed (the libc
+ * libc malloc() call might still fail if you get a genuine out-of-memory
+ * error).
+ *
+ * Portions Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/interfaces/libpq/test/mallocfail.c
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <malloc.h>
+
+#include "mallocfail.h"
+
+static void *(*orig_malloc) (size_t size, const void *caller);
+static void *(*orig_realloc) (void *ptr, size_t size, const void *caller);
+
+static void *mallocfail_malloc(size_t size, const void *caller);
+static void *mallocfail_realloc(void *ptr, size_t size, const void *caller);
+
+static void *
+mallocfail_malloc(size_t size, const void *caller)
+{
+	void	   *result;
+
+	if (mallocfail_callback("malloc"))
+		return NULL;
+
+	/* restore original hooks */
+	__malloc_hook = orig_malloc;
+	__realloc_hook = orig_realloc;
+
+	result = malloc(size);
+
+	/* restore myself */
+	__malloc_hook = mallocfail_malloc;
+	__realloc_hook = mallocfail_realloc;
+
+	return result;
+}
+
+static void *
+mallocfail_realloc(void *ptr, size_t size, const void *caller)
+{
+	void	   *result;
+
+	if (mallocfail_callback("realloc"))
+		return NULL;
+
+	/* restore original hooks */
+	__malloc_hook = orig_malloc;
+	__realloc_hook = orig_realloc;
+
+	result = realloc(ptr, size);
+
+	/* restore myself */
+	__malloc_hook = mallocfail_malloc;
+	__realloc_hook = mallocfail_realloc;
+
+	return result;
+}
+
+void
+mallocfail_init(void)
+{
+	orig_malloc = __malloc_hook;
+	orig_realloc = __realloc_hook;
+	__malloc_hook = mallocfail_malloc;
+	__realloc_hook = mallocfail_realloc;
+}
diff --git a/src/interfaces/libpq/test/mallocfail.h b/src/interfaces/libpq/test/mallocfail.h
new file mode 100644
index 0000000..727bf7e
--- /dev/null
+++ b/src/interfaces/libpq/test/mallocfail.h
@@ -0,0 +1,23 @@
+/*-------------------------------------------------------------------------
+ *
+ * mallocfail.h
+ *
+ *        Definitions for malloc() out-of-memory simulation
+ *
+ * Portions Copyright (c) 1996-2015, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/interfaces/libpq/test/mallocfail.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef MALLOCFAIL_H
+#define MALLOCFAIL_H
+
+/* defined in mallocfail.c */
+extern void mallocfail_init(void);
+
+/* defined in oomtest.c */
+extern int	mallocfail_callback(const char *funcname);
+
+#endif   /* MALLOCFAIL_H */
diff --git a/src/interfaces/libpq/test/oom-test.c b/src/interfaces/libpq/test/oom-test.c
new file mode 100644
index 0000000..fd40ee9
--- /dev/null
+++ b/src/interfaces/libpq/test/oom-test.c
@@ -0,0 +1,377 @@
+/*
+ * oom-test.c
+ *		A test program for libpq behaviour on out-of-memory
+ *
+ * This relies on a mechanism to intercept malloc() calls, see mallocfail.c.
+ * The intercepter must call our mallocfail_callback() function for every
+ * intercepted malloc() call, which will decide whether to let the call
+ * to succeed.
+ *
+ * Portions Copyright (c) 2012-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/interfaces/libpq/test/oom-test.c
+ */
+#include <libpq-fe.h>
+
+#include "c.h"
+
+#include "mallocfail.h"
+
+/* A global variable that counts malloc() calls issued so far */
+static int malloc_counter = 0;
+static bool mallocfail_enabled = false;
+
+/* If non-zero, cause this allocation to fail.  */
+static int malloc_fail_at = 0;
+
+/*
+ * Checks that a result set looks correct. We know exactly what the result sets
+ * used in these tests should look like; two rows and two columns:
+ *
+ * foo1	foo2
+ * bar1 bar2
+ */
+static bool
+check_result_set(PGresult *res)
+{
+	/*
+	 * If the result is not correct, print it out to stderr for debugging.
+	 */
+	if (PQntuples(res) != 2 || PQnfields(res) != 2 ||
+		strcmp(PQgetvalue(res, 0, 0), "foo1") != 0 ||
+		strcmp(PQgetvalue(res, 0, 1), "foo2") != 0 ||
+		strcmp(PQgetvalue(res, 1, 0), "bar1") != 0 ||
+		strcmp(PQgetvalue(res, 1, 1), "bar2") != 0)
+	{
+		int			row,
+					col;
+
+		fprintf(stderr, "unexpected result set with %d rows and %d cols:\n",
+				PQntuples(res), PQnfields(res));
+		for (row = 0; row < PQntuples(res); row++)
+		{
+			for (col = 0; col < PQnfields(res); col++)
+			{
+				if (col > 0)
+					fprintf(stderr, "\t");
+				fprintf(stderr, "%s", PQgetvalue(res, row, col));
+			}
+			fprintf(stderr, "\n");
+		}
+		return FALSE;
+	}
+	else
+	{
+		printf("Got correct result set\n");
+		return TRUE;
+	}
+}
+
+/*
+ * Check that a result indicates an out-of-memory error (and not some
+ * other kind of error).
+ */
+static bool
+check_error_is_oom(const char *funcname, PGresult *res)
+{
+	char	   *errmsg = PQresultErrorMessage(res);
+
+	/* XXX: This won't work with non-English locale. */
+	if (strncmp(errmsg, "out of memory", strlen("out of memory")) == 0)
+	{
+		/* got "out of memory", that's OK. */
+		printf("%s failed with OOM: %s\n", funcname, errmsg);
+		return TRUE;
+	}
+	else
+	{
+		/* any other error is not expected */
+		fprintf(stderr, "%s returned error: %s\n", funcname, errmsg);
+		return FALSE;
+	}
+}
+
+/*
+ * Test functions.
+ *
+ * The test driver, main/run_tests will call these repeatedly, with the
+ * malloc hook set up to fail at some calls.
+ */
+
+/* Test PQexec() */
+static bool
+test_PQexec(PGconn *conn)
+{
+	PGresult   *res;
+	bool		success;
+
+	mallocfail_enabled = true;
+	res = PQexec(conn, "values ('foo1', 'foo2'), ('bar1', 'bar2')");
+	mallocfail_enabled = false;
+
+	/*
+	 * There are two valid outcomes: 1. success 2. Ran out of memory
+	 */
+	if (PQresultStatus(res) == PGRES_TUPLES_OK)
+		success = check_result_set(res);
+	else if (PQresultStatus(res) == PGRES_FATAL_ERROR)
+		success = check_error_is_oom("PQexec", res);
+	else
+	{
+		fprintf(stderr, "PQexec returned unexpected result status: %s\n",
+				PQresStatus(PQresultStatus(res)));
+		success = FALSE;
+	}
+
+	PQclear(res);
+
+	return success;
+}
+
+
+/* Test PQprepare() + PQdescribePrepared() + PQexec */
+static bool
+test_PQprepare(PGconn *conn)
+{
+	PGresult   *res;
+	bool		success = TRUE;
+	char	   *param = "foo1";
+
+	/* 1. Call PQPrepare() to prepare an unnamed statement */
+	mallocfail_enabled = true;
+	res = PQprepare(conn, "",
+					"values ($1, 'foo2'), ('bar1', 'bar2')", 1, NULL);
+	mallocfail_enabled = false;
+
+	if (PQresultStatus(res) == PGRES_FATAL_ERROR)
+		success = check_error_is_oom("PQprepare", res);
+	else if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare returned unexpected result status: %s\n",
+				PQresStatus(PQresultStatus(res)));
+		success = FALSE;
+	}
+	PQclear(res);
+
+	if (!success)
+		return FALSE;
+
+	/* 2. Call PQdescribePrepared */
+	mallocfail_enabled = true;
+	res = PQdescribePrepared(conn, "");
+	mallocfail_enabled = false;
+
+	if (PQresultStatus(res) == PGRES_FATAL_ERROR)
+		success = check_error_is_oom("PQdescribePrepared", res);
+	else if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared returned unexpected result status: %s\n",
+				PQresStatus(PQresultStatus(res)));
+		success = FALSE;
+	}
+	PQclear(res);
+
+	if (!success)
+		return FALSE;
+
+
+	/* 2. Call PQexecPrepared */
+	mallocfail_enabled = true;
+	res = PQexecPrepared(conn, "", 1, (const char *const *) &param, NULL, NULL, 0);
+	mallocfail_enabled = false;
+
+	if (PQresultStatus(res) == PGRES_TUPLES_OK)
+		success = check_result_set(res);
+	else if (PQresultStatus(res) == PGRES_FATAL_ERROR)
+		success = check_error_is_oom("PQexecPrepared", res);
+	else
+	{
+		fprintf(stderr, "PQexecPrepared returned unexpected result status: %s\n",
+				PQresStatus(PQresultStatus(res)));
+		success = FALSE;
+	}
+	PQclear(res);
+
+	if (!success)
+		return FALSE;
+
+
+	return success;
+}
+
+
+/* Test COPY FROM STDIN */
+static bool
+test_COPY_FROM(PGconn *conn)
+{
+	PGresult   *res;
+	bool		success;
+	static bool	table_created = false;
+	char	   *rowdata[2] = { "foo1\t\foo2\n", "bar1\tbar2\n" };
+	int			rc;
+	int			i;
+
+	if (!table_created)
+	{
+		res = PQexec(conn, "CREATE TEMPORARY TABLE copytest (col1 text, col2 text) ON COMMIT DELETE ROWS");
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			fprintf(stderr, "failed to create temporary table: %s",
+					PQerrorMessage(conn));
+			return false;
+		}
+		table_created = true;
+	}
+
+	mallocfail_enabled = true;
+	res = PQexec(conn, "COPY copytest FROM STDIN");
+	mallocfail_enabled = false;
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+	{
+		fprintf(stderr, "unexpected result for COPY FROM STDIN: %s",
+				PQerrorMessage(conn));
+		return false;
+	}
+
+	mallocfail_enabled = true;
+	for (i = 0; i < 2; i++)
+	{
+		rc = PQputCopyData(conn, rowdata[i], strlen(rowdata[i]));
+		if (rc == -1)
+		{
+			mallocfail_enabled = false;
+			fprintf(stderr, "PQputCopyData failed: %s\n", PQerrorMessage(conn));
+			return false;
+		}
+	}
+
+	rc = PQputCopyEnd(conn, NULL);
+	mallocfail_enabled = false;
+	if (rc == -1)
+	{
+		fprintf(stderr, "PQputCopyEnd failed: %s\n", PQerrorMessage(conn));
+		return false;
+	}
+
+	mallocfail_enabled = true;
+	res = PQgetResult(conn);
+	mallocfail_enabled = false;
+
+	/*
+	 * There are two valid outcomes: 1. success 2. Ran out of memory
+	 */
+	if (PQresultStatus(res) == PGRES_COMMAND_OK)
+		success = true;
+	else if (PQresultStatus(res) == PGRES_FATAL_ERROR)
+		success = check_error_is_oom("PQexec", res);
+	else
+	{
+		fprintf(stderr, "PQexec returned unexpected result status: %s\n",
+				PQresStatus(PQresultStatus(res)));
+		success = false;
+	}
+
+	PQclear(res);
+
+	return success;
+}
+
+/*
+ * This function needs to be called for every malloc() call. It decides
+ * whether the call should be let to succeed, or we should simulate OOM and
+ * return NULL to the caller.
+ */
+int
+mallocfail_callback(const char *func)
+{
+	if (!mallocfail_enabled)
+		return 0;
+
+	malloc_counter++;
+	if (malloc_counter == malloc_fail_at)
+	{
+		printf("%s failing: %d!\n", func, malloc_counter);
+		return 1;
+	}
+	else
+	{
+		printf("%s called: %d\n", func, malloc_counter);
+		return 0;
+	}
+}
+
+/*
+ * Run a test function multiple times. In each iteration, cause one malloc()
+ * call to fail.
+ */
+static void
+run_testfunc(PGconn *conn, const char *testname, bool (*testfunc) (PGconn *conn))
+{
+	int			i;
+	int			nmallocs;
+
+	/*
+	 * Run the test once without any malloc() failures, to count the number of
+	 * malloc() calls it makes when run normally.
+	 */
+	malloc_counter = 0;
+	malloc_fail_at = 0;
+	if (!testfunc(conn))
+	{
+		fprintf(stderr, "%s pre-test failed!\n", testname);
+		return;
+	}
+	nmallocs = malloc_counter;
+
+	/*
+	 * If our callback isn't get called at all, the most likely reason is that
+	 * the failure injecting code was not loaded correctly.
+	 */
+	if (nmallocs == 0)
+	{
+		fprintf(stderr, "malloc() callback was not called in %s test.\n", testname);
+		return;
+	}
+	printf("%s called malloc() %d times\n", testname, nmallocs);
+
+	/*
+	 * Ok, now run the test again several times, causing a different malloc()
+	 * call to fail each time.
+	 */
+	for (i = 1; i <= nmallocs; i++)
+	{
+		printf("Running %s test, with failure at malloc call %d...\n", testname, i);
+		malloc_counter = 0;
+		malloc_fail_at = i;
+		testfunc(conn);
+		malloc_fail_at = 0;
+	}
+}
+
+int
+main(int argc, char *argv[])
+{
+	PGconn	   *conn;
+
+	if ((conn = PQconnectdb("")) == NULL)
+	{
+		fprintf(stderr, "PQconnectdb failed");
+		exit(1);
+	}
+
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "PQconnectdb failed: %s",
+				PQerrorMessage(conn));
+		exit(1);
+	}
+
+	mallocfail_init();
+
+	run_testfunc(conn, "PQexec", &test_PQexec);
+	run_testfunc(conn, "PQprepare", &test_PQprepare);
+	run_testfunc(conn, "COPY FROM STDIN", &test_COPY_FROM);
+
+	return 0;
+}
-- 
2.1.4

