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:12 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.