MINOR: logs: startup-logs can use a shm for logging the reload

When compiled with USE_SHM_OPEN=1 the startup-logs are now able to use
an shm which is used to keep the logs when switching to mworker wait
mode. This allows to keep the failed reload logs.

When allocating the startup-logs at first start of the process, haproxy
will do a shm_open with a unique path using the PID of the process, the
file is unlink immediatly so we don't let unwelcomed files be. The fd
resulting from this shm is stored in the HAPROXY_STARTUPLOGS_FD
environment variable so it can be mmap again when switching to wait
mode.

When forking children, the process is copying the mmap to a a mallocated
ring so we never share the same memory section between the master and
the workers. When switching to wait mode, the shm is not used anymore as
it is also copied to a mallocated structure.

This allow to use the "show startup-logs" command over the master CLI,
to get the logs of the latest startup or reload. This way the logs of
the latest failed reload are also kept.

This is only activated on the linux-glibc target for now.
diff --git a/Makefile b/Makefile
index 65d8d0c..9123e02 100644
--- a/Makefile
+++ b/Makefile
@@ -57,6 +57,7 @@
 #   USE_MEMORY_PROFILING : enable the memory profiler. Linux-glibc only.
 #   USE_LIBATOMIC        : force to link with/without libatomic. Automatic.
 #   USE_PTHREAD_EMULATION: replace pthread's rwlocks with ours
+#   USE_SHM_OPEN         : use shm_open() for the startup-logs
 #
 # Options can be forced by specifying "USE_xxx=1" or can be disabled by using
 # "USE_xxx=" (empty string). The list of enabled and disabled options for a
@@ -345,7 +346,8 @@
            USE_CLOSEFROM USE_ZLIB USE_SLZ USE_CPU_AFFINITY USE_TFO USE_NS     \
            USE_DL USE_RT USE_DEVICEATLAS USE_51DEGREES USE_WURFL USE_SYSTEMD  \
            USE_OBSOLETE_LINKER USE_PRCTL USE_PROCCTL USE_THREAD_DUMP          \
-           USE_EVPORTS USE_OT USE_QUIC USE_PROMEX USE_MEMORY_PROFILING
+           USE_EVPORTS USE_OT USE_QUIC USE_PROMEX USE_MEMORY_PROFILING        \
+           USE_SHM_OPEN
 
 #### Target system options
 # Depending on the target platform, some options are set, as well as some
@@ -382,7 +384,7 @@
     USE_POLL USE_TPROXY USE_LIBCRYPT USE_DL USE_RT USE_CRYPT_H USE_NETFILTER  \
     USE_CPU_AFFINITY USE_THREAD USE_EPOLL USE_LINUX_TPROXY                    \
     USE_ACCEPT4 USE_LINUX_SPLICE USE_PRCTL USE_THREAD_DUMP USE_NS USE_TFO     \
-    USE_GETADDRINFO USE_BACKTRACE)
+    USE_GETADDRINFO USE_BACKTRACE USE_SHM_OPEN)
   INSTALL = install -v
 endif
 
diff --git a/include/haproxy/defaults.h b/include/haproxy/defaults.h
index a8e5e57..7b49928 100644
--- a/include/haproxy/defaults.h
+++ b/include/haproxy/defaults.h
@@ -100,7 +100,10 @@
 #define MAX_SYSLOG_LEN          1024
 #endif
 
-/* 64kB to archive startup-logs seems way more than enough */
+/* 64kB to archive startup-logs seems way more than enough
+ * /!\ Careful when changing this size, it is used in a shm when exec() from
+ * mworker to wait mode.
+ */
 #ifndef STARTUP_LOG_SIZE
 #define STARTUP_LOG_SIZE        65536
 #endif
diff --git a/include/haproxy/errors.h b/include/haproxy/errors.h
index e9ad588..81a4b2f 100644
--- a/include/haproxy/errors.h
+++ b/include/haproxy/errors.h
@@ -43,6 +43,7 @@
 
 #define ERR_CODE	(ERR_RETRYABLE|ERR_FATAL|ERR_ABORT)	/* mask */
 
+extern struct ring *startup_logs;
 
 /* These codes may be used by config parsing functions which detect errors and
  * which need to inform the upper layer about them. They are all prefixed with
@@ -123,6 +124,10 @@
 void qfprintf(FILE *out, const char *fmt, ...)
 	__attribute__ ((format(printf, 2, 3)));
 
+void startup_logs_init();
+struct ring *startup_logs_dup(struct ring *src);
+void startup_logs_free(struct ring *r);
+
 #endif /* _HAPROXY_ERRORS_H */
 
 /*
diff --git a/src/errors.c b/src/errors.c
index bd3c271..96ed60d 100644
--- a/src/errors.c
+++ b/src/errors.c
@@ -1,5 +1,9 @@
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <fcntl.h>
 #include <stdarg.h>
 #include <stdio.h>
+#include <stdlib.h>
 #include <syslog.h>
 
 #include <haproxy/api.h>
@@ -15,7 +19,10 @@
 
 /* A global buffer used to store all startup alerts/warnings. It will then be
  * retrieve on the CLI. */
-static struct ring *startup_logs = NULL;
+struct ring *startup_logs = NULL;
+#ifdef USE_SHM_OPEN
+static struct ring *shm_startup_logs = NULL;
+#endif
 
 /* A thread local buffer used to store all alerts/warnings. It can be used to
  * retrieve them for CLI commands after startup.
@@ -28,6 +35,185 @@
 #define USERMSGS_CTX_BUFSIZE   PATH_MAX
 static THREAD_LOCAL struct usermsgs_ctx usermsgs_ctx = { .str = BUF_NULL, };
 
+#ifdef USE_SHM_OPEN
+
+/* initialise an SHM for the startup logs and return its fd */
+static int startup_logs_new_shm()
+{
+	char *path = NULL;
+	int fd = -1;
+	int flags;
+
+	/* create a unique path per PID so we don't collide with another
+	   process */
+	memprintf(&path, "/haproxy_startup_logs_%d", getpid());
+	fd = shm_open(path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
+	if (fd == -1)
+		goto error;
+	shm_unlink(path);
+	ha_free(&path);
+
+	if (ftruncate(fd, STARTUP_LOG_SIZE) == -1)
+		goto error;
+
+	flags = fcntl(fd, F_GETFD);
+	if (flags == -1)
+		goto error;
+	flags &= ~FD_CLOEXEC;
+	flags = fcntl(fd, F_SETFD, flags);
+	if (flags == -1)
+		goto error;
+
+	return fd;
+error:
+	if (fd != -1) {
+		close(fd);
+		fd = -1;
+	}
+	return fd;
+}
+
+/* mmap a startup-logs from a <fd>.
+ * if <new> is set to one, initialize the buffer.
+ * Returns the ring.
+ */
+static struct ring *startup_logs_from_fd(int fd, int new)
+{
+	char *area;
+	struct ring *r = NULL;
+
+	if (fd == -1)
+		goto error;
+
+	area = mmap(NULL, STARTUP_LOG_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+	if (area == MAP_FAILED || area == NULL)
+		goto error;
+
+	if (new)
+		r = ring_make_from_area(area, STARTUP_LOG_SIZE);
+	else
+		r = ring_cast_from_area(area);
+
+	if (r == NULL)
+		goto error;
+
+	shm_startup_logs = r; /* save the ptr so we can unmap later */
+
+	return r;
+error:
+	return NULL;
+}
+
+/*
+ * Use a shm accross reexec of the master.
+ *
+ * During the startup of the master, a shm_open must be done and the FD saved
+ * into the HAPROXY_STARTUPLOGS_FD environment variable.
+ *
+ * When forking workers, the child must use a copy of the shm, not the shm itself.
+ *
+ * Once in wait mode, the shm must be copied and closed.
+ *
+ */
+void startup_logs_init()
+{
+	struct ring *r = NULL;
+	char *str_fd, *endptr;
+	int fd = -1;
+
+	str_fd = getenv("HAPROXY_STARTUPLOGS_FD");
+	if (str_fd) {
+		fd = strtol(str_fd, &endptr, 10);
+		if (*endptr != '\0')
+			goto error;
+		unsetenv("HAPROXY_STARTUPLOGS_FD");
+	}
+
+	/* during startup, or just after a reload.
+	 * Note: the WAIT_ONLY env variable must be
+	 * check in case of an early call  */
+	if (!(global.mode & MODE_MWORKER_WAIT) &&
+	    getenv("HAPROXY_MWORKER_WAIT_ONLY") == NULL) {
+		if (fd != -1)
+			close(fd);
+
+		fd = startup_logs_new_shm();
+		if (fd == -1)
+			goto error;
+
+		r = startup_logs_from_fd(fd, 1);
+		if (!r)
+			goto error;
+
+		memprintf(&str_fd, "%d", fd);
+		setenv("HAPROXY_STARTUPLOGS_FD", str_fd, 1);
+		ha_free(&str_fd);
+
+	} else {
+		/* in wait mode, copy the shm to an allocated buffer */
+		struct ring *prev = NULL;
+
+		if (fd == -1)
+			goto error;
+
+		prev = startup_logs_from_fd(fd, 0);
+		if (!prev)
+			goto error;
+
+		r = startup_logs_dup(prev);
+		if (!r)
+			goto error;
+		startup_logs_free(prev);
+		close(fd);
+	}
+
+	startup_logs = r;
+
+	return;
+error:
+	if (fd != -1)
+		close(fd);
+	/* couldn't get a mmap to work */
+	startup_logs = ring_new(STARTUP_LOG_SIZE);
+
+}
+
+#else /* ! USE_SHM_OPEN */
+
+void startup_logs_init()
+{
+	startup_logs = ring_new(STARTUP_LOG_SIZE);
+}
+
+#endif
+
+/* free the startup logs, unmap if it was an shm */
+void startup_logs_free(struct ring *r)
+{
+#ifdef USE_SHM_OPEN
+	if (r == shm_startup_logs)
+		munmap(r, STARTUP_LOG_SIZE);
+	else
+#endif /* ! USE_SHM_OPEN */
+		ring_free(r);
+}
+
+/* duplicate a startup logs which was previously allocated in a shm */
+struct ring *startup_logs_dup(struct ring *src)
+{
+	struct ring *dst = NULL;
+
+	/* must use the size of the previous buffer */
+	dst = ring_new(b_size(&src->buf));
+	if (!dst)
+		goto error;
+
+	b_reset(&dst->buf);
+	b_ncat(&dst->buf, &src->buf, b_data(&src->buf));
+error:
+	return dst;
+}
+
 /* Put msg in usermsgs_buf.
  *
  * The message should not be terminated by a newline because this function
@@ -200,7 +386,7 @@
 
 	if (global.mode & MODE_STARTING) {
 		if (unlikely(!startup_logs))
-			startup_logs = ring_new(STARTUP_LOG_SIZE);
+			startup_logs_init();
 
 		if (likely(startup_logs)) {
 			struct ist m[3];
@@ -361,7 +547,7 @@
 
 /* register cli keywords */
 static struct cli_kw_list cli_kws = {{ },{
-	{ { "show", "startup-logs",  NULL }, "show startup-logs                       : report logs emitted during HAProxy startup", cli_parse_show_startup_logs, NULL, NULL },
+	{ { "show", "startup-logs",  NULL }, "show startup-logs                       : report logs emitted during HAProxy startup", cli_parse_show_startup_logs, NULL, NULL, NULL, ACCESS_MASTER },
 	{{},}
 }};
 
diff --git a/src/haproxy.c b/src/haproxy.c
index a95fc21..8064970 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -1909,6 +1909,8 @@
 	struct pre_check_fct *prcf;
 	int ideal_maxconn;
 
+	startup_logs_init();
+
 	if (!init_trash_buffers(1)) {
 		ha_alert("failed to initialize trash buffers.\n");
 		exit(1);
@@ -3431,9 +3433,13 @@
 
 		/* the father launches the required number of processes */
 		if (!(global.mode & MODE_MWORKER_WAIT)) {
+			struct ring *tmp_startup_logs = NULL;
+
 			if (global.mode & MODE_MWORKER)
 				mworker_ext_launch_all();
 
+			/* at this point the worker must have his own startup_logs buffer */
+			tmp_startup_logs = startup_logs_dup(startup_logs);
 			ret = fork();
 			if (ret < 0) {
 				ha_alert("[%s.main()] Cannot fork.\n", argv[0]);
@@ -3441,6 +3447,8 @@
 				exit(1); /* there has been an error */
 			}
 			else if (ret == 0) { /* child breaks here */
+				startup_logs_free(startup_logs);
+				startup_logs = tmp_startup_logs;
 				/* This one must not be exported, it's internal! */
 				unsetenv("HAPROXY_MWORKER_REEXEC");
 				ha_random_jump96(1);