From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org
Cc: Jacob Lalonde <jalalonde@meta.com>,
Josef Bacik <josef@toxicpanda.com>, Jann Horn <jannh@google.com>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Jan Kara <jack@suse.cz>,
Andrew Morton <akpm@linux-foundation.org>,
David Hildenbrand <david@kernel.org>,
Lorenzo Stoakes <ljs@kernel.org>,
"Liam R. Howlett" <liam@infradead.org>,
Vlastimil Babka <vbabka@kernel.org>,
Mike Rapoport <rppt@kernel.org>,
Suren Baghdasaryan <surenb@google.com>,
Michal Hocko <mhocko@suse.com>,
Omar Sandoval <osandov@osandov.com>,
Jacob Lalonde <jalalonde@fb.com>, Shuah Khan <shuah@kernel.org>,
linux-kernel@vger.kernel.org, linux-mm@kvack.org,
linux-kselftest@vger.kernel.org, linuxppc-dev@lists.ozlabs.org,
"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH v2 21/22] selftests/coredump: simulate a blob store
Date: Thu, 20 Aug 2026 01:09:38 +0200 [thread overview]
Message-ID: <20260820-work-coredump-sparse-v2-21-ba32dd718c51@kernel.org> (raw)
In-Reply-To: <20260820-work-coredump-sparse-v2-0-ba32dd718c51@kernel.org>
A coredump server that uploads to a blob store must redescribe the
coredump and fixup the phdr.
A segment is split wherever a hole was left out and everything a segment
covers past p_filesz is zeroes anyway. So the blob store ends up with an
ordinary ELF core file that is missing nothing but holes. Nothing
downstream of the server has to learn a container format.
The coredump with its holes still in it is reassembled alongside the
object so the two can be compared.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
.../coredump/coredump_socket_protocol_test.c | 156 +++++
.../selftests/coredump/coredump_test_helpers.c | 765 +++++++++++++++++++++
.../selftests/coredump/coredump_test_helpers.h | 3 +
3 files changed, 924 insertions(+)
diff --git a/tools/testing/selftests/coredump/coredump_socket_protocol_test.c b/tools/testing/selftests/coredump/coredump_socket_protocol_test.c
index abf6e2c4c354..f33eaf2fa93d 100644
--- a/tools/testing/selftests/coredump/coredump_socket_protocol_test.c
+++ b/tools/testing/selftests/coredump/coredump_socket_protocol_test.c
@@ -1882,6 +1882,162 @@ TEST_F(coredump, socket_request_records_truncated)
ASSERT_GT(received, 0);
}
+/*
+ * A coredump server that uploads to a blob store can't upload a sparse
+ * file. It doesn't have to: it streams the data records into the object
+ * as they arrive, leaves the holes out, and uploads the corrected
+ * program header table last. What it ends up with is an ordinary ELF
+ * core file that describes the same memory as the coredump the records
+ * came from, minus the holes.
+ */
+TEST_F(coredump, socket_request_sparse_blob_upload)
+{
+ int fd_core_file, pidfd, status;
+ pid_t pid, pid_coredump_server;
+ struct pidfd_info info = {};
+ off_t coredump_size = 0;
+ ssize_t received = 0;
+ int ipc_sockets[2];
+ int pipefds[2];
+ struct stat st;
+ char c;
+
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0);
+ ASSERT_EQ(pipe(pipefds), 0);
+ ASSERT_TRUE(set_core_pattern("@@/tmp/coredump.socket"));
+
+ pid_coredump_server = fork();
+ ASSERT_GE(pid_coredump_server, 0);
+ if (pid_coredump_server == 0) {
+ int fd_server = -1, fd_coredump = -1, fd_peer_pidfd = -1;
+ int fd_object = -1, fd_reference = -1;
+ int exit_code = EXIT_FAILURE;
+ struct coredump_req req = {};
+ off_t size = 0;
+ ssize_t ret;
+
+ close(ipc_sockets[0]);
+ close(pipefds[0]);
+
+ fd_server = create_and_listen_unix_socket("/tmp/coredump.socket");
+ if (fd_server < 0)
+ goto out;
+
+ if (write_nointr(ipc_sockets[1], "1", 1) < 0)
+ goto out;
+
+ close(ipc_sockets[1]);
+
+ fd_coredump = accept4(fd_server, NULL, NULL, SOCK_CLOEXEC);
+ if (fd_coredump < 0)
+ goto out;
+
+ fd_peer_pidfd = get_peer_pidfd(fd_coredump);
+ if (fd_peer_pidfd < 0)
+ goto out;
+
+ /* The object is a plain file. It never sees a hole. */
+ fd_object = open("/tmp/coredump.file",
+ O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
+ if (fd_object < 0)
+ goto out;
+
+ /*
+ * The coredump with its holes still in it is bigger than
+ * the mapping the child made, so keep it on the detached
+ * tmpfs and sparse.
+ */
+ fd_reference = open_coredump_tmpfile(self->fd_tmpfs_detached);
+ if (fd_reference < 0)
+ goto out;
+
+ if (!read_coredump_req(fd_coredump, &req))
+ goto out;
+
+ if (!check_coredump_req(&req))
+ goto out;
+
+ if (!send_coredump_ack(fd_coredump, &req,
+ COREDUMP_KERNEL | COREDUMP_RECORDS |
+ COREDUMP_SPARSE | COREDUMP_WAIT, 0))
+ goto out;
+
+ if (!read_marker(fd_coredump, COREDUMP_MARK_REQACK))
+ goto out;
+
+ ret = recv_coredump_compact(fd_coredump, fd_object,
+ fd_reference, &size);
+ if (ret < 0)
+ goto out;
+
+ if (check_compact_coredump(fd_object, fd_reference))
+ goto out;
+
+ if (write_nointr(pipefds[1], &ret, sizeof(ret)) != sizeof(ret))
+ goto out;
+ if (write_nointr(pipefds[1], &size, sizeof(size)) != sizeof(size))
+ goto out;
+
+ exit_code = EXIT_SUCCESS;
+out:
+ close(pipefds[1]);
+ if (fd_reference >= 0)
+ close(fd_reference);
+ if (fd_object >= 0)
+ close(fd_object);
+ if (fd_peer_pidfd >= 0)
+ close(fd_peer_pidfd);
+ if (fd_coredump >= 0)
+ close(fd_coredump);
+ if (fd_server >= 0)
+ close(fd_server);
+ _exit(exit_code);
+ }
+ self->pid_coredump_server = pid_coredump_server;
+
+ EXPECT_EQ(close(ipc_sockets[1]), 0);
+ EXPECT_EQ(close(pipefds[1]), 0);
+ ASSERT_EQ(read_nointr(ipc_sockets[0], &c, 1), 1);
+ EXPECT_EQ(close(ipc_sockets[0]), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ crashing_child_sparse(SPARSE_MAPPING_SIZE);
+
+ pidfd = sys_pidfd_open(pid, 0);
+ ASSERT_GE(pidfd, 0);
+
+ waitpid(pid, &status, 0);
+ ASSERT_TRUE(WIFSIGNALED(status));
+ ASSERT_TRUE(WCOREDUMP(status));
+
+ ASSERT_EQ(read_nointr(pipefds[0], &received, sizeof(received)),
+ sizeof(received));
+ ASSERT_EQ(read_nointr(pipefds[0], &coredump_size, sizeof(coredump_size)),
+ sizeof(coredump_size));
+ EXPECT_EQ(close(pipefds[0]), 0);
+
+ ASSERT_TRUE(get_pidfd_info(pidfd, &info));
+ ASSERT_GT((info.mask & PIDFD_INFO_COREDUMP), 0);
+ ASSERT_GT((info.coredump_mask & PIDFD_COREDUMPED), 0);
+
+ wait_and_check_coredump_server(pid_coredump_server, _metadata, self);
+
+ /* The mapping is in the coredump, holes included. */
+ ASSERT_GT(coredump_size, (off_t)SPARSE_MAPPING_SIZE);
+
+ /* The object isn't sparse and doesn't carry them. */
+ ASSERT_EQ(stat("/tmp/coredump.file", &st), 0);
+ ASSERT_LT(st.st_size, coredump_size / 8);
+
+ /* And a debugger still sees an ordinary ELF core file. */
+ fd_core_file = open("/tmp/coredump.file", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(fd_core_file, 0);
+ ASSERT_TRUE(is_elf_core(fd_core_file));
+ EXPECT_EQ(close(fd_core_file), 0);
+}
+
/* Ack @ack_mask, expect the kernel to refuse it as conflicting. */
static void check_conflicting_ack(struct __test_metadata *const _metadata,
FIXTURE_DATA(coredump) *self, __u64 ack_mask)
diff --git a/tools/testing/selftests/coredump/coredump_test_helpers.c b/tools/testing/selftests/coredump/coredump_test_helpers.c
index 89f3954c5607..9346b8f688e2 100644
--- a/tools/testing/selftests/coredump/coredump_test_helpers.c
+++ b/tools/testing/selftests/coredump/coredump_test_helpers.c
@@ -330,6 +330,771 @@ bool is_elf_core(int fd)
return true;
}
+/*
+ * A coredump server that uploads to a blob store can't upload a sparse
+ * file and can't seek in the object it is uploading. It streams the data
+ * records into the object as they arrive, remembers the holes it left
+ * out, and uploads the program header table that describes the result
+ * last. What comes out is an ordinary ELF core file without the holes.
+ */
+
+/* A run of the coredump the object doesn't carry. */
+struct compact_hole {
+ __u64 offset;
+ __u64 len;
+};
+
+/* A program header of the object and where its bytes sat in the coredump. */
+struct compact_piece {
+ ElfW(Phdr) phdr;
+ __u64 src;
+};
+
+struct compact_ctx {
+ int fd_body; /* the object's payload, append only */
+ int fd_reference; /* the coredump with its holes, for the test */
+ unsigned char *head; /* everything ahead of the segment data */
+ size_t head_len;
+ size_t head_cap;
+ __u64 data_offset; /* where the segment data starts, 0 while unknown */
+ struct compact_hole *holes;
+ size_t nr_holes;
+ size_t holes_cap;
+ __u64 body_len;
+};
+
+/* Write @len bytes out, short writes and all. */
+static int compact_write(int fd, const void *buf, size_t len)
+{
+ const unsigned char *pos = buf;
+
+ while (len) {
+ ssize_t ret = write(fd, pos, len);
+
+ if (ret <= 0) {
+ fprintf(stderr, "%s: write failed: %m\n", __func__);
+ return -1;
+ }
+
+ pos += ret;
+ len -= ret;
+ }
+
+ return 0;
+}
+
+/* Keep @len bytes of the head, or @len zeroes if @buf is NULL. */
+static int compact_head_append(struct compact_ctx *ctx, const void *buf,
+ size_t len)
+{
+ if (ctx->head_len + len > ctx->head_cap) {
+ size_t cap = ctx->head_cap ? ctx->head_cap : PAGE_SIZE;
+ unsigned char *head;
+
+ while (cap < ctx->head_len + len)
+ cap *= 2;
+
+ head = realloc(ctx->head, cap);
+ if (!head) {
+ fprintf(stderr, "%s: out of memory\n", __func__);
+ return -1;
+ }
+ ctx->head = head;
+ ctx->head_cap = cap;
+ }
+
+ if (buf)
+ memcpy(ctx->head + ctx->head_len, buf, len);
+ else
+ memset(ctx->head + ctx->head_len, 0, len);
+ ctx->head_len += len;
+
+ return 0;
+}
+
+/* Remember a hole so the program header table can account for it later. */
+static int compact_keep_hole(struct compact_ctx *ctx, __u64 offset, __u64 len)
+{
+ if (ctx->nr_holes == ctx->holes_cap) {
+ size_t cap = ctx->holes_cap ? ctx->holes_cap * 2 : 64;
+ struct compact_hole *holes;
+
+ holes = realloc(ctx->holes, cap * sizeof(*holes));
+ if (!holes) {
+ fprintf(stderr, "%s: out of memory\n", __func__);
+ return -1;
+ }
+ ctx->holes = holes;
+ ctx->holes_cap = cap;
+ }
+
+ ctx->holes[ctx->nr_holes].offset = offset;
+ ctx->holes[ctx->nr_holes].len = len;
+ ctx->nr_holes++;
+
+ return 0;
+}
+
+/* The segment data starts where the first PT_LOAD points. */
+static int compact_probe(struct compact_ctx *ctx)
+{
+ const ElfW(Ehdr) *ehdr = (const ElfW(Ehdr) *)ctx->head;
+ const ElfW(Phdr) *phdr;
+ size_t i;
+
+ if (ctx->data_offset || ctx->head_len < sizeof(*ehdr))
+ return 0;
+
+ if (!is_core_ehdr(ehdr)) {
+ fprintf(stderr, "%s: not an ELF core file\n", __func__);
+ return -1;
+ }
+
+ if (ehdr->e_phoff != sizeof(*ehdr) ||
+ ehdr->e_phentsize != sizeof(ElfW(Phdr)) ||
+ ehdr->e_phnum == 0 || ehdr->e_phnum == PN_XNUM) {
+ fprintf(stderr, "%s: unhandled program header table\n", __func__);
+ return -1;
+ }
+
+ if (ctx->head_len < ehdr->e_phoff +
+ (size_t)ehdr->e_phnum * ehdr->e_phentsize)
+ return 0;
+
+ phdr = (const ElfW(Phdr) *)(ctx->head + ehdr->e_phoff);
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ if (phdr[i].p_type != PT_LOAD)
+ continue;
+ if (!ctx->data_offset || phdr[i].p_offset < ctx->data_offset)
+ ctx->data_offset = phdr[i].p_offset;
+ }
+
+ if (!ctx->data_offset) {
+ fprintf(stderr, "%s: coredump without a single segment\n",
+ __func__);
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * Take whatever of [@offset, @offset + @len) still belongs to the head.
+ * @buf is NULL for a hole. Returns how much was taken.
+ */
+static ssize_t compact_head_take(struct compact_ctx *ctx, const void *buf,
+ __u64 offset, __u64 len)
+{
+ __u64 chunk;
+
+ if (!len || (ctx->data_offset && offset >= ctx->data_offset))
+ return 0;
+
+ chunk = len;
+ if (ctx->data_offset && offset + chunk > ctx->data_offset)
+ chunk = ctx->data_offset - offset;
+
+ if (offset != ctx->head_len) {
+ fprintf(stderr, "%s: head has a gap at %llu\n", __func__,
+ (unsigned long long)offset);
+ return -1;
+ }
+
+ if (compact_head_append(ctx, buf, chunk))
+ return -1;
+
+ return chunk;
+}
+
+static int compact_data(void *arg, const void *buf, size_t len, __u64 offset)
+{
+ struct compact_ctx *ctx = arg;
+ const unsigned char *pos = buf;
+ ssize_t head;
+
+ /* Only the test needs a coredump with the holes still in it. */
+ if (pwrite(ctx->fd_reference, pos, len, offset) != (ssize_t)len) {
+ fprintf(stderr, "%s: pwrite failed: %m\n", __func__);
+ return -1;
+ }
+
+ /* The head has to be rewritten at the end, so hold on to it. */
+ head = compact_head_take(ctx, pos, offset, len);
+ if (head < 0)
+ return -1;
+ if (head && compact_probe(ctx))
+ return -1;
+
+ pos += head;
+ len -= head;
+ if (!len)
+ return 0;
+
+ /* Everything else goes into the object as it arrives. */
+ if (compact_write(ctx->fd_body, pos, len))
+ return -1;
+ ctx->body_len += len;
+
+ return 0;
+}
+
+static int compact_zero(void *arg, __u64 offset, __u64 len)
+{
+ struct compact_ctx *ctx = arg;
+ ssize_t head;
+
+ /* A hole in the head is alignment padding. Write it out. */
+ head = compact_head_take(ctx, NULL, offset, len);
+ if (head < 0)
+ return -1;
+
+ offset += head;
+ len -= head;
+ if (!len)
+ return 0;
+
+ /* This is what the object doesn't have to carry. */
+ return compact_keep_hole(ctx, offset, len);
+}
+
+/* Where @offset ends up in the object once the holes ahead of it are gone. */
+static __u64 compact_offset(const struct compact_ctx *ctx, __u64 body_start,
+ __u64 offset)
+{
+ __u64 elided = 0;
+ size_t i;
+
+ for (i = 0; i < ctx->nr_holes; i++) {
+ __u64 len = ctx->holes[i].len;
+
+ if (ctx->holes[i].offset >= offset)
+ break;
+ if (ctx->holes[i].offset + len > offset)
+ len = offset - ctx->holes[i].offset;
+ elided += len;
+ }
+
+ return body_start + (offset - ctx->data_offset) - elided;
+}
+
+/* A run of segment data that made it into the object. */
+static void compact_add_data(struct compact_piece *pieces, size_t *nr,
+ const ElfW(Phdr) *phdr, __u64 start, __u64 end)
+{
+ struct compact_piece *piece = &pieces[(*nr)++];
+
+ piece->phdr = *phdr;
+ piece->phdr.p_vaddr = phdr->p_vaddr + (start - phdr->p_offset);
+ piece->phdr.p_paddr = 0;
+ piece->phdr.p_filesz = end - start;
+ piece->phdr.p_memsz = end - start;
+ piece->src = start;
+}
+
+/*
+ * A run of @len bytes the object doesn't carry. It grows the piece in
+ * front of it if this segment already has one, because everything a
+ * segment covers past p_filesz is zeroes anyway.
+ */
+static void compact_add_zero(struct compact_piece *pieces, size_t *nr,
+ size_t first, const ElfW(Phdr) *phdr, __u64 vaddr,
+ __u64 len)
+{
+ struct compact_piece *piece;
+
+ if (*nr > first) {
+ pieces[*nr - 1].phdr.p_memsz += len;
+ return;
+ }
+
+ piece = &pieces[(*nr)++];
+ piece->phdr = *phdr;
+ piece->phdr.p_vaddr = vaddr;
+ piece->phdr.p_paddr = 0;
+ piece->phdr.p_filesz = 0;
+ piece->phdr.p_memsz = len;
+ piece->src = 0;
+}
+
+/* Split the segments at the holes and write out what the object became. */
+static int compact_build(struct compact_ctx *ctx, int fd_object)
+{
+ __u64 note_offset = 0, note_len = 0, note_new;
+ __u64 align = 0, head_len, body_start, pos;
+ size_t nr_old, nr_new = 0, note_piece = 0, i;
+ struct compact_piece *pieces;
+ char buffer[PAGE_SIZE];
+ const ElfW(Phdr) *old;
+ ElfW(Ehdr) ehdr;
+ int ret = -1;
+
+ if (!ctx->data_offset) {
+ fprintf(stderr, "%s: coredump without segment data\n", __func__);
+ return -1;
+ }
+
+ memcpy(&ehdr, ctx->head, sizeof(ehdr));
+ if (ehdr.e_shoff) {
+ fprintf(stderr, "%s: section headers are not handled\n",
+ __func__);
+ return -1;
+ }
+
+ old = (const ElfW(Phdr) *)(ctx->head + ehdr.e_phoff);
+ nr_old = ehdr.e_phnum;
+
+ pieces = calloc(nr_old + 2 * ctx->nr_holes + 1, sizeof(*pieces));
+ if (!pieces) {
+ fprintf(stderr, "%s: out of memory\n", __func__);
+ return -1;
+ }
+
+ for (i = 0; i < nr_old; i++) {
+ ElfW(Phdr) phdr = old[i];
+ __u64 end = phdr.p_offset + phdr.p_filesz;
+ __u64 cur = phdr.p_offset;
+ size_t first = nr_new, h;
+
+ /* The notes move because the table in front of them grows. */
+ if (phdr.p_type == PT_NOTE) {
+ if (note_len) {
+ fprintf(stderr, "%s: more than one note segment\n",
+ __func__);
+ goto out;
+ }
+ note_offset = phdr.p_offset;
+ note_len = phdr.p_filesz;
+ note_piece = nr_new;
+ pieces[nr_new].phdr = phdr;
+ pieces[nr_new++].src = 0;
+ continue;
+ }
+
+ if (phdr.p_type != PT_LOAD) {
+ if (phdr.p_filesz && phdr.p_offset < ctx->data_offset) {
+ fprintf(stderr, "%s: segment %zu is in the head\n",
+ __func__, i);
+ goto out;
+ }
+ pieces[nr_new].phdr = phdr;
+ pieces[nr_new++].src = phdr.p_offset;
+ continue;
+ }
+
+ if (!align)
+ align = phdr.p_align;
+
+ for (h = 0; h < ctx->nr_holes && cur < end; h++) {
+ __u64 start = ctx->holes[h].offset;
+ __u64 stop = start + ctx->holes[h].len;
+
+ if (stop <= cur)
+ continue;
+ if (start >= end)
+ break;
+
+ /* A hole can span more than this one segment. */
+ if (start < cur)
+ start = cur;
+ if (stop > end)
+ stop = end;
+
+ if (start > cur) {
+ compact_add_data(pieces, &nr_new, &phdr, cur,
+ start);
+ cur = start;
+ }
+ compact_add_zero(pieces, &nr_new, first, &phdr,
+ phdr.p_vaddr + (cur - phdr.p_offset),
+ stop - cur);
+ cur = stop;
+ }
+
+ if (cur < end)
+ compact_add_data(pieces, &nr_new, &phdr, cur, end);
+
+ /* Whatever the kernel didn't dump of this mapping. */
+ if (phdr.p_memsz > phdr.p_filesz)
+ compact_add_zero(pieces, &nr_new, first, &phdr,
+ phdr.p_vaddr + phdr.p_filesz,
+ phdr.p_memsz - phdr.p_filesz);
+ }
+
+ if (!note_len || note_offset + note_len > ctx->head_len) {
+ fprintf(stderr, "%s: notes aren't where they should be\n",
+ __func__);
+ goto out;
+ }
+
+ if (nr_new >= PN_XNUM) {
+ fprintf(stderr, "%s: %zu program headers don't fit\n", __func__,
+ nr_new);
+ goto out;
+ }
+
+ if (!align || (align & (align - 1)))
+ align = sysconf(_SC_PAGESIZE);
+
+ note_new = sizeof(ehdr) + (__u64)nr_new * sizeof(ElfW(Phdr));
+ head_len = note_new + note_len;
+ body_start = (head_len + align - 1) & ~(align - 1);
+
+ for (i = 0; i < nr_new; i++) {
+ struct compact_piece *piece = &pieces[i];
+
+ if (i == note_piece)
+ piece->phdr.p_offset = note_new;
+ else if (piece->phdr.p_filesz)
+ piece->phdr.p_offset = compact_offset(ctx, body_start,
+ piece->src);
+ else
+ piece->phdr.p_offset = 0;
+ }
+
+ /* Only now is the head known. That's why it is uploaded last. */
+ ehdr.e_phnum = nr_new;
+ if (compact_write(fd_object, &ehdr, sizeof(ehdr)))
+ goto out;
+
+ for (i = 0; i < nr_new; i++)
+ if (compact_write(fd_object, &pieces[i].phdr,
+ sizeof(pieces[i].phdr)))
+ goto out;
+
+ if (compact_write(fd_object, ctx->head + note_offset, note_len))
+ goto out;
+
+ /* Keep the segments aligned the way a debugger expects them. */
+ memset(buffer, 0, sizeof(buffer));
+ for (pos = head_len; pos < body_start; ) {
+ __u64 chunk = body_start - pos;
+
+ if (chunk > sizeof(buffer))
+ chunk = sizeof(buffer);
+ if (compact_write(fd_object, buffer, chunk))
+ goto out;
+ pos += chunk;
+ }
+
+ /* Putting the parts together is the blob store's job. Do it here. */
+ for (pos = 0; pos < ctx->body_len; ) {
+ ssize_t chunk = pread(ctx->fd_body, buffer, sizeof(buffer), pos);
+
+ if (chunk <= 0) {
+ fprintf(stderr, "%s: short read %zd: %m\n", __func__,
+ chunk);
+ goto out;
+ }
+ if (compact_write(fd_object, buffer, chunk))
+ goto out;
+ pos += chunk;
+ }
+
+ fprintf(stderr, "Object is %llu bytes in %zu program headers, %zu holes left out\n",
+ (unsigned long long)(body_start + ctx->body_len), nr_new,
+ ctx->nr_holes);
+ ret = 0;
+out:
+ free(pieces);
+ return ret;
+}
+
+/*
+ * Reassemble a record stream into an ELF core file that has no holes in
+ * it, the way a coredump server that uploads to a blob store has to. If
+ * @fd_reference is valid it gets the coredump the records describe,
+ * holes and all, so the test can compare the two.
+ */
+ssize_t recv_coredump_compact(int fd_coredump, int fd_object, int fd_reference,
+ off_t *coredump_size)
+{
+ struct compact_ctx ctx = {
+ .fd_body = -1,
+ .fd_reference = fd_reference,
+ };
+ struct coredump_record_sink sink = {
+ .data = compact_data,
+ .zero = compact_zero,
+ .ctx = &ctx,
+ };
+ ssize_t received;
+ off_t size = 0;
+ FILE *body;
+
+ body = tmpfile();
+ if (!body) {
+ fprintf(stderr, "%s: tmpfile failed: %m\n", __func__);
+ return -1;
+ }
+ ctx.fd_body = fileno(body);
+
+ /* An upload is appended to. Make sure nothing here can seek. */
+ if (fcntl(ctx.fd_body, F_SETFL, O_APPEND)) {
+ fprintf(stderr, "%s: F_SETFL failed: %m\n", __func__);
+ received = -1;
+ goto out;
+ }
+
+ received = __recv_coredump_records(fd_coredump, &sink, &size, NULL, -1);
+ if (received < 0)
+ goto out;
+
+ /*
+ * Nothing is written for a hole, so grow the reference to the size
+ * the records describe in case the coredump ended in one.
+ */
+ if (ftruncate(fd_reference, size) < 0) {
+ fprintf(stderr, "%s: ftruncate to %llu failed: %m\n",
+ __func__, (unsigned long long)size);
+ received = -1;
+ goto out;
+ }
+
+ if (compact_build(&ctx, fd_object)) {
+ received = -1;
+ goto out;
+ }
+
+ if (coredump_size)
+ *coredump_size = size;
+out:
+ fclose(body);
+ free(ctx.head);
+ free(ctx.holes);
+ return received;
+}
+
+/* Read the ELF header and the program header table of @fd. */
+static ElfW(Phdr) *read_phdrs(int fd, size_t *nr)
+{
+ ElfW(Ehdr) ehdr;
+ ElfW(Phdr) *phdr;
+ size_t size;
+
+ if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) {
+ fprintf(stderr, "%s: no ELF header: %m\n", __func__);
+ return NULL;
+ }
+
+ if (!is_core_ehdr(&ehdr) || !ehdr.e_phnum ||
+ ehdr.e_phentsize != sizeof(*phdr)) {
+ fprintf(stderr, "%s: not an ELF core file\n", __func__);
+ return NULL;
+ }
+
+ size = (size_t)ehdr.e_phnum * ehdr.e_phentsize;
+ phdr = malloc(size);
+ if (!phdr) {
+ fprintf(stderr, "%s: out of memory\n", __func__);
+ return NULL;
+ }
+
+ if (pread(fd, phdr, size, ehdr.e_phoff) != (ssize_t)size) {
+ fprintf(stderr, "%s: short program header table: %m\n", __func__);
+ free(phdr);
+ return NULL;
+ }
+
+ *nr = ehdr.e_phnum;
+ return phdr;
+}
+
+/* The segment @vaddr falls into. */
+static const ElfW(Phdr) *find_segment(const ElfW(Phdr) *phdr, size_t nr,
+ __u64 vaddr)
+{
+ size_t i;
+
+ for (i = 0; i < nr; i++) {
+ if (phdr[i].p_type != PT_LOAD)
+ continue;
+ if (vaddr >= phdr[i].p_vaddr &&
+ vaddr < phdr[i].p_vaddr + phdr[i].p_memsz)
+ return &phdr[i];
+ }
+
+ return NULL;
+}
+
+/* The next stretch of memory the segments cover, split ones merged back. */
+static bool next_range(const ElfW(Phdr) *phdr, size_t nr, size_t *i,
+ __u64 *start, __u64 *end)
+{
+ while (*i < nr && phdr[*i].p_type != PT_LOAD)
+ (*i)++;
+
+ if (*i >= nr)
+ return false;
+
+ *start = phdr[*i].p_vaddr;
+ *end = phdr[*i].p_vaddr + phdr[*i].p_memsz;
+ (*i)++;
+
+ while (*i < nr) {
+ if (phdr[*i].p_type != PT_LOAD) {
+ (*i)++;
+ continue;
+ }
+ if (phdr[*i].p_vaddr != *end)
+ break;
+ *end = phdr[*i].p_vaddr + phdr[*i].p_memsz;
+ (*i)++;
+ }
+
+ return true;
+}
+
+/* Compare @len bytes at @offset against @len bytes at @offset_ref. */
+static int compare_range(int fd, __u64 offset, int fd_ref, __u64 offset_ref,
+ __u64 len)
+{
+ char buffer[PAGE_SIZE], buffer_ref[PAGE_SIZE];
+
+ while (len) {
+ size_t chunk = len < sizeof(buffer) ? len : sizeof(buffer);
+
+ if (pread(fd, buffer, chunk, offset) != (ssize_t)chunk ||
+ pread(fd_ref, buffer_ref, chunk, offset_ref) != (ssize_t)chunk) {
+ fprintf(stderr, "%s: short read at %llu: %m\n",
+ __func__, (unsigned long long)offset);
+ return -1;
+ }
+
+ if (memcmp(buffer, buffer_ref, chunk)) {
+ fprintf(stderr, "%s: %llu differs from %llu\n", __func__,
+ (unsigned long long)offset,
+ (unsigned long long)offset_ref);
+ return -1;
+ }
+
+ offset += chunk;
+ offset_ref += chunk;
+ len -= chunk;
+ }
+
+ return 0;
+}
+
+/* The @len bytes at @offset the object left out have to have been zeroes. */
+static int check_zero_range(int fd, __u64 offset, __u64 len)
+{
+ static const char zeroes[PAGE_SIZE];
+ char buffer[PAGE_SIZE];
+
+ while (len) {
+ size_t chunk = len < sizeof(buffer) ? len : sizeof(buffer);
+
+ if (pread(fd, buffer, chunk, offset) != (ssize_t)chunk) {
+ fprintf(stderr, "%s: short read at %llu: %m\n",
+ __func__, (unsigned long long)offset);
+ return -1;
+ }
+
+ if (memcmp(buffer, zeroes, chunk)) {
+ fprintf(stderr, "%s: %llu isn't a hole\n", __func__,
+ (unsigned long long)offset);
+ return -1;
+ }
+
+ offset += chunk;
+ len -= chunk;
+ }
+
+ return 0;
+}
+
+/*
+ * The object has to describe the same memory as the coredump it was built
+ * from, and it has to describe it correctly.
+ */
+int check_compact_coredump(int fd_object, int fd_reference)
+{
+ ElfW(Phdr) *object = NULL, *reference = NULL;
+ size_t nr_object, nr_reference, i;
+ size_t io = 0, ir = 0;
+ int ret = -1;
+
+ object = read_phdrs(fd_object, &nr_object);
+ reference = read_phdrs(fd_reference, &nr_reference);
+ if (!object || !reference)
+ goto out;
+
+ /* Nothing may have been dropped and nothing may have been added. */
+ for (;;) {
+ __u64 start = 0, end = 0, start_ref = 0, end_ref = 0;
+ bool has, has_ref;
+
+ has = next_range(object, nr_object, &io, &start, &end);
+ has_ref = next_range(reference, nr_reference, &ir, &start_ref,
+ &end_ref);
+ if (!has && !has_ref)
+ break;
+
+ if (has != has_ref || start != start_ref || end != end_ref) {
+ fprintf(stderr, "%s: object covers 0x%llx-0x%llx, coredump 0x%llx-0x%llx\n",
+ __func__, (unsigned long long)start,
+ (unsigned long long)end,
+ (unsigned long long)start_ref,
+ (unsigned long long)end_ref);
+ goto out;
+ }
+ }
+
+ for (i = 0; i < nr_object; i++) {
+ const ElfW(Phdr) *segment;
+ __u64 offset, dumped;
+
+ if (object[i].p_type != PT_LOAD || !object[i].p_memsz)
+ continue;
+
+ segment = find_segment(reference, nr_reference,
+ object[i].p_vaddr);
+ if (!segment) {
+ fprintf(stderr, "%s: 0x%llx isn't in the coredump\n",
+ __func__,
+ (unsigned long long)object[i].p_vaddr);
+ goto out;
+ }
+
+ offset = object[i].p_vaddr - segment->p_vaddr;
+ dumped = offset < segment->p_filesz ?
+ segment->p_filesz - offset : 0;
+
+ /* What the object carries is what the coredump had. */
+ if (object[i].p_filesz > dumped) {
+ fprintf(stderr, "%s: object carries %llu bytes the coredump doesn't have\n",
+ __func__,
+ (unsigned long long)(object[i].p_filesz - dumped));
+ goto out;
+ }
+
+ if (compare_range(fd_object, object[i].p_offset, fd_reference,
+ segment->p_offset + offset,
+ object[i].p_filesz))
+ goto out;
+
+ /* And what it left out was a hole. */
+ if (object[i].p_memsz > object[i].p_filesz &&
+ dumped > object[i].p_filesz) {
+ __u64 left_out = dumped - object[i].p_filesz;
+
+ if (left_out > object[i].p_memsz - object[i].p_filesz)
+ left_out = object[i].p_memsz - object[i].p_filesz;
+
+ if (check_zero_range(fd_reference,
+ segment->p_offset + offset +
+ object[i].p_filesz, left_out))
+ goto out;
+ }
+ }
+
+ ret = 0;
+out:
+ free(object);
+ free(reference);
+ return ret;
+}
+
int create_detached_tmpfs(void)
{
int fd_context, fd_tmpfs;
diff --git a/tools/testing/selftests/coredump/coredump_test_helpers.h b/tools/testing/selftests/coredump/coredump_test_helpers.h
index fe0a88a71b05..00d695b67b3f 100644
--- a/tools/testing/selftests/coredump/coredump_test_helpers.h
+++ b/tools/testing/selftests/coredump/coredump_test_helpers.h
@@ -26,6 +26,9 @@ ssize_t recv_coredump_records(int fd_coredump, int fd_core_file,
off_t *coredump_size, bool *truncated,
int fd_peer_pidfd);
bool is_elf_core(int fd);
+ssize_t recv_coredump_compact(int fd_coredump, int fd_object, int fd_reference,
+ off_t *coredump_size);
+int check_compact_coredump(int fd_object, int fd_reference);
int create_detached_tmpfs(void);
int create_and_listen_unix_socket(const char *path);
bool set_core_pattern(const char *pattern);
--
2.53.0
next prev parent reply other threads:[~2026-08-19 23:11 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-19 23:09 [PATCH v2 00/22] coredump: allow to create sparse coredumps on the coredump socket Christian Brauner
2026-08-19 23:09 ` [PATCH v2 01/22] powerpc/spufs: don't dump more than the note supports Christian Brauner
2026-08-19 23:09 ` [PATCH v2 02/22] coredump: refuse negative skips Christian Brauner
2026-08-19 23:09 ` [PATCH v2 03/22] coredump: set the minimum send buffer size Christian Brauner
2026-08-19 23:09 ` [PATCH v2 04/22] selftests/coredump: discard the right amount after the coredump request Christian Brauner
2026-08-19 23:09 ` [PATCH v2 05/22] selftests/coredump: collapse the expected request check into the helper Christian Brauner
2026-08-19 23:09 ` [PATCH v2 06/22] selftests/coredump: add a separate helper header Christian Brauner
2026-08-19 23:09 ` [PATCH v2 07/22] coredump: pin the protocol struct sizes Christian Brauner
2026-08-19 23:09 ` [PATCH v2 08/22] coredump: move the negotiated mask into struct coredump_params Christian Brauner
2026-08-19 23:09 ` [PATCH v2 09/22] coredump: deduplicate the to_skip flush Christian Brauner
2026-08-19 23:09 ` [PATCH v2 10/22] coredump: make the dump helper return bool Christian Brauner
2026-08-19 23:09 ` [PATCH v2 11/22] coredump: always chunk writes Christian Brauner
2026-08-19 23:09 ` [PATCH v2 12/22] coredump: clean up coredump state handling Christian Brauner
2026-08-19 23:09 ` [PATCH v2 13/22] coredump: add COREDUMP_RECORDS to the coredump socket protocol Christian Brauner
2026-08-19 23:09 ` [PATCH v2 14/22] coredump: add COREDUMP_SPARSE " Christian Brauner
2026-08-19 23:09 ` [PATCH v2 15/22] tools: sync coredump.h header Christian Brauner
2026-08-19 23:09 ` [PATCH v2 16/22] coredump: send the coredump in records if requested Christian Brauner
2026-08-19 23:09 ` [PATCH v2 17/22] coredump: describe the holes when COREDUMP_SPARSE is negotiated Christian Brauner
2026-08-19 23:09 ` [PATCH v2 18/22] selftests/coredump: test COREDUMP_RECORDS and COREDUMP_SPARSE Christian Brauner
2026-08-19 23:09 ` [PATCH v2 19/22] selftests/coredump: hand the record stream to a sink Christian Brauner
2026-08-19 23:09 ` [PATCH v2 20/22] selftests/coredump: put a hole in the middle of a sparse mapping Christian Brauner
2026-08-19 23:09 ` Christian Brauner [this message]
2026-08-19 23:09 ` [PATCH v2 22/22] selftests/coredump: show how to inspect the task to decide how the coredump should be sent Christian Brauner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260820-work-coredump-sparse-v2-21-ba32dd718c51@kernel.org \
--to=brauner@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=david@kernel.org \
--cc=jack@suse.cz \
--cc=jalalonde@fb.com \
--cc=jalalonde@meta.com \
--cc=jannh@google.com \
--cc=josef@toxicpanda.com \
--cc=liam@infradead.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=linuxppc-dev@lists.ozlabs.org \
--cc=ljs@kernel.org \
--cc=mhocko@suse.com \
--cc=osandov@osandov.com \
--cc=rppt@kernel.org \
--cc=shuah@kernel.org \
--cc=surenb@google.com \
--cc=vbabka@kernel.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox