MINOR: ring: add support for a backing-file

This mmaps a file which will serve as the backing-store for the ring's
contents. The idea is to provide a way to retrieve sensitive information
(last logs, debugging traces) even after the process stops and even after
a possible crash. Right now this was possible by connecting to the CLI
and dumping the contents of the ring live, but this is not handy and
consumes quite a bit of resources before it is needed.

With a backing file, the ring is effectively RAM-mapped file, so that
contents stored there are the same as those found in the file (the OS
doesn't guarantee immediate sync but if the process dies it will be OK).

Note that doing that on a filesystem backed by a physical device is a
bad idea, as it will induce slowdowns at high loads. It's really
important that the device is RAM-based.

Also, this may have security implications: if the file is corrupted by
another process, the storage area could be corrupted, causing haproxy
to crash or to overwrite its own memory. As such this should only be
used for debugging.
This commit is contained in:
Willy Tarreau 2022-08-11 16:38:20 +02:00
parent 6df10d872b
commit 0b8e9ceb12
3 changed files with 96 additions and 2 deletions

View File

@ -3580,6 +3580,33 @@ servers or traces.
ring <ringname>
Creates a new ring-buffer with name <ringname>.
backing-file <path>
This replaces the regular memory allocation by a RAM-mapped file to store the
ring. This can be useful for collecting traces or logs for post-mortem
analysis, without having to attach a slow client to the CLI. Newer contents
will automatically replace older ones so that the latest contents are always
available. The contents written to the ring will be visible in that file once
the process stops (most often they will even be seen very soon after but
there is no such guarantee since writes are not synchronous).
When this option is used, the total storage area is reduced by the size of
the "struct ring" that starts at the beginning of the area, and that is
required to recover the area's contents. The file will be created with the
starting user's ownership, with mode 0600 and will be of the size configured
by the "size" directive.
WARNING: there are stability and security implications in using this feature.
First, backing the ring to a slow device (e.g. physical hard drive) may cause
perceptible slowdowns during accesses, and possibly even panics if too many
threads compete for accesses. Second, an external process modifying the area
could cause the haproxy process to crash or to overwrite some of its own
memory with traces. Third, if the file system fills up before the ring,
writes to the ring may cause the process to crash.
The information present in this ring are structured and are NOT directly
readable using a text editor (even though most of it looks barely readable).
The output of this file is only intended for developers.
description <text>
The description is an optional description string of the ring. It will
appear on CLI. By default, <name> is reused to fill this field.

View File

@ -50,6 +50,7 @@ struct sink {
struct list sink_list; // position in the sink list
char *name; // sink name
char *desc; // sink description
char *store; // backing-store file when buffer
enum log_fmt fmt; // format expected by the sink
enum sink_type type; // type of storage
uint32_t maxlen; // max message length (truncated above)

View File

@ -18,6 +18,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <sys/mman.h>
#include <errno.h>
#include <fcntl.h>
#include <import/ist.h>
#include <haproxy/api.h>
#include <haproxy/applet.h>
@ -817,6 +821,12 @@ int cfg_parse_ring(const char *file, int linenum, char **args, int kwm)
goto err;
}
if (cfg_sink->store) {
ha_alert("parsing [%s:%d] : cannot resize an already mapped file, please specify 'size' before 'backing-file'.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL;
goto err;
}
if (size < cfg_sink->ctx.ring->buf.size) {
ha_warning("parsing [%s:%d] : ignoring new size '%llu' that is smaller than current size '%llu' for ring '%s'.\n",
file, linenum, (ullong)size, (ullong)cfg_sink->ctx.ring->buf.size, cfg_sink->name);
@ -831,6 +841,58 @@ int cfg_parse_ring(const char *file, int linenum, char **args, int kwm)
goto err;
}
}
else if (strcmp(args[0], "backing-file") == 0) {
/* This tries to mmap file <file> for size <size> and to use it as a backing store
* for ring <ring>. Existing data are delete. NULL is returned on error.
*/
const char *backing = args[1];
size_t size;
void *area;
int fd;
if (!cfg_sink || (cfg_sink->type != SINK_TYPE_BUFFER)) {
ha_alert("parsing [%s:%d] : 'backing-file' only usable with existing rings.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL;
goto err;
}
if (cfg_sink->store) {
ha_alert("parsing [%s:%d] : 'backing-file' already specified for ring '%s' (was '%s').\n", file, linenum, cfg_sink->name, cfg_sink->store);
err_code |= ERR_ALERT | ERR_FATAL;
goto err;
}
fd = open(backing, O_RDWR | O_CREAT, S_IRUSR|S_IWUSR);
if (fd < 0) {
ha_alert("parsing [%s:%d] : cannot open backing-file '%s' for ring '%s': %s.\n", file, linenum, backing, cfg_sink->name, strerror(errno));
err_code |= ERR_ALERT | ERR_FATAL;
goto err;
}
size = (cfg_sink->ctx.ring->buf.size + 4095UL) & -4096UL;
if (ftruncate(fd, size) != 0) {
close(fd);
ha_alert("parsing [%s:%d] : could not adjust size of backing-file for ring '%s': %s.\n", file, linenum, cfg_sink->name, strerror(errno));
err_code |= ERR_ALERT | ERR_FATAL;
goto err;
}
area = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (area == MAP_FAILED) {
close(fd);
ha_alert("parsing [%s:%d] : failed to use '%s' as a backing file for ring '%s': %s.\n", file, linenum, backing, cfg_sink->name, strerror(errno));
err_code |= ERR_ALERT | ERR_FATAL;
goto err;
}
/* we don't need the file anymore */
close(fd);
cfg_sink->store = strdup(backing);
/* never fails */
ring_free(cfg_sink->ctx.ring);
cfg_sink->ctx.ring = ring_make_from_area(area, size);
}
else if (strcmp(args[0],"server") == 0) {
err_code |= parse_server(file, linenum, args, cfg_sink->forward_px, NULL,
SRV_PARSE_PARSE_ADDR|SRV_PARSE_INITIAL_RESOLVE);
@ -1267,8 +1329,12 @@ static void sink_deinit()
struct sink *sink, *sb;
list_for_each_entry_safe(sink, sb, &sink_list, sink_list) {
if (sink->type == SINK_TYPE_BUFFER)
ring_free(sink->ctx.ring);
if (sink->type == SINK_TYPE_BUFFER) {
if (sink->store)
munmap(sink->ctx.ring->buf.area, sink->ctx.ring->buf.size);
else
ring_free(sink->ctx.ring);
}
LIST_DELETE(&sink->sink_list);
free(sink->name);
free(sink->desc);