Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
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



  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox