summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fs/binfmt_elf.c12
-rw-r--r--fs/binfmt_elf_fdpic.c12
-rw-r--r--fs/coredump.c325
-rw-r--r--include/linux/binfmts.h3
-rw-r--r--include/linux/coredump.h33
-rw-r--r--include/uapi/linux/coredump.h79
-rw-r--r--tools/include/uapi/linux/coredump.h79
-rw-r--r--tools/testing/selftests/coredump/coredump_socket_protocol_test.c783
-rw-r--r--tools/testing/selftests/coredump/coredump_test.h31
-rw-r--r--tools/testing/selftests/coredump/coredump_test_helpers.c1171
-rw-r--r--tools/testing/selftests/coredump/coredump_test_helpers.h53
11 files changed, 2387 insertions, 194 deletions
diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index db32bb40a867..6b7ffac5d665 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -74,7 +74,7 @@ static int load_elf_binary(struct linux_binprm *bprm);
* don't even try.
*/
#ifdef CONFIG_ELF_CORE
-static int elf_core_dump(struct coredump_params *cprm);
+static bool elf_core_dump(struct coredump_params *cprm);
#else
#define elf_core_dump NULL
#endif
@@ -1987,9 +1987,9 @@ static void fill_extnum_info(struct elfhdr *elf, struct elf_shdr *shdr4extnum,
* and then they are actually written out. If we run out of core limit
* we just truncate.
*/
-static int elf_core_dump(struct coredump_params *cprm)
+static bool elf_core_dump(struct coredump_params *cprm)
{
- int has_dumped = 0;
+ bool ret = false;
int segs, i;
struct elfhdr elf;
loff_t offset = 0, dataoff;
@@ -2020,7 +2020,7 @@ static int elf_core_dump(struct coredump_params *cprm)
if (!fill_note_info(&elf, e_phnum, &info, cprm))
goto end_coredump;
- has_dumped = 1;
+ cprm->state |= COREDUMP_STATE_STARTED;
offset += sizeof(elf); /* ELF header */
offset += segs * sizeof(struct elf_phdr); /* Program headers */
@@ -2115,11 +2115,13 @@ static int elf_core_dump(struct coredump_params *cprm)
goto end_coredump;
}
+ ret = true;
+
end_coredump:
free_note_info(&info);
kfree(shdr4extnum);
kfree(phdr4note);
- return has_dumped;
+ return ret;
}
#endif /* CONFIG_ELF_CORE */
diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c
index 068c46875c74..005f0a084483 100644
--- a/fs/binfmt_elf_fdpic.c
+++ b/fs/binfmt_elf_fdpic.c
@@ -75,7 +75,7 @@ static int elf_fdpic_map_file_by_direct_mmap(struct elf_fdpic_params *,
struct file *, struct mm_struct *);
#ifdef CONFIG_ELF_CORE
-static int elf_fdpic_core_dump(struct coredump_params *cprm);
+static bool elf_fdpic_core_dump(struct coredump_params *cprm);
#endif
static struct linux_binfmt elf_fdpic_format = {
@@ -1477,9 +1477,9 @@ static bool elf_fdpic_dump_segments(struct coredump_params *cprm,
* and then they are actually written out. If we run out of core limit
* we just truncate.
*/
-static int elf_fdpic_core_dump(struct coredump_params *cprm)
+static bool elf_fdpic_core_dump(struct coredump_params *cprm)
{
- int has_dumped = 0;
+ bool ret = false;
int segs;
int i;
struct elfhdr *elf = NULL;
@@ -1536,7 +1536,7 @@ static int elf_fdpic_core_dump(struct coredump_params *cprm)
/* Set up header */
fill_elf_fdpic_header(elf, e_phnum);
- has_dumped = 1;
+ cprm->state |= COREDUMP_STATE_STARTED;
/*
* Set up the notes in similar form to SVR4 core dumps made
* with info from their /proc.
@@ -1656,6 +1656,8 @@ static int elf_fdpic_core_dump(struct coredump_params *cprm)
cprm->file->f_pos, offset);
}
+ ret = true;
+
end_coredump:
while (thread_list) {
tmp = thread_list;
@@ -1666,7 +1668,7 @@ end_coredump:
kfree(elf);
kfree(psinfo);
kfree(shdr4extnum);
- return has_dumped;
+ return ret;
}
#endif /* CONFIG_ELF_CORE */
diff --git a/fs/coredump.c b/fs/coredump.c
index 3e78941f281e..dcaf998e54f6 100644
--- a/fs/coredump.c
+++ b/fs/coredump.c
@@ -51,7 +51,6 @@
#include <net/sock.h>
#include <uapi/linux/pidfd.h>
#include <uapi/linux/un.h>
-#include <uapi/linux/coredump.h>
#include <linux/uaccess.h>
#include <asm/mmu_context.h>
@@ -68,6 +67,8 @@
static bool dump_vma_snapshot(struct coredump_params *cprm);
static void free_vma_snapshot(struct coredump_params *cprm);
+static void dump_end_record(struct coredump_params *cprm);
+static bool dump_flush_skip(struct coredump_params *cprm);
#define CORE_FILE_NOTE_SIZE_DEFAULT (4*1024*1024)
/* Define a reasonable max cap */
@@ -98,9 +99,7 @@ struct core_name {
char *corename __counted_by_ptr(size);
int used, size;
unsigned int core_pipe_limit;
- bool core_dumped;
enum coredump_type_t core_type;
- u64 mask;
};
static int expand_corename(struct core_name *cn, int size)
@@ -245,13 +244,12 @@ static bool coredump_parse(struct core_name *cn, struct coredump_params *cprm,
int pid_in_pattern = 0;
int err = 0;
- cn->mask = COREDUMP_KERNEL;
+ cprm->mask = COREDUMP_KERNEL;
if (core_pipe_limit)
- cn->mask |= COREDUMP_WAIT;
+ cprm->mask |= COREDUMP_WAIT;
cn->used = 0;
cn->corename = NULL;
cn->core_pipe_limit = 0;
- cn->core_dumped = false;
if (*pat_ptr == '|')
cn->core_type = COREDUMP_PIPE;
else if (*pat_ptr == '@')
@@ -550,13 +548,13 @@ static int coredump_wait(int exit_code, struct core_state *core_state)
return core_waiters;
}
-static void coredump_finish(bool core_dumped)
+static void coredump_finish(enum coredump_state state)
{
struct core_thread *curr, *next;
struct task_struct *task;
spin_lock_irq(&current->sighand->siglock);
- if (core_dumped && !__fatal_signal_pending(current))
+ if ((state & COREDUMP_STATE_STARTED) && !__fatal_signal_pending(current))
current->signal->group_exit_code |= 0x80;
next = current->signal->core_state->dumper.next;
current->signal->core_state = NULL;
@@ -664,7 +662,12 @@ static int umh_coredump_setup(struct subprocess_info *info, struct cred *new)
return 0;
}
+static_assert(sizeof(struct coredump_record_header) == COREDUMP_RECORD_HEADER_SIZE_VER0);
+
#ifdef CONFIG_UNIX
+/* af_unix halves the send buffer to size a single skb. */
+#define COREDUMP_SOCK_SNDBUF_MIN (3 * PAGE_SIZE)
+
static bool coredump_sock_connect(struct core_name *cn, struct coredump_params *cprm)
{
struct file *file __free(fput) = NULL;
@@ -690,6 +693,10 @@ static bool coredump_sock_connect(struct core_name *cn, struct coredump_params *
if (retval < 0)
return false;
+ /* Don't let a page-sized write split into several skbs. */
+ socket->sk->sk_sndbuf = max_t(int, socket->sk->sk_sndbuf,
+ COREDUMP_SOCK_SNDBUF_MIN);
+
file = sock_alloc_file(socket, 0, NULL);
if (IS_ERR(file))
return false;
@@ -752,6 +759,8 @@ static inline bool coredump_sock_send(struct file *file, struct coredump_req *re
return ret == sizeof(*req);
}
+static_assert(sizeof(struct coredump_req) == COREDUMP_REQ_SIZE_VER0);
+static_assert(sizeof(struct coredump_ack) == COREDUMP_ACK_SIZE_VER0);
static_assert(sizeof(enum coredump_mark) == sizeof(__u32));
static inline bool coredump_sock_mark(struct file *file, enum coredump_mark mark)
@@ -797,7 +806,8 @@ static bool coredump_sock_request(struct core_name *cn, struct coredump_params *
struct coredump_req req = {
.size = sizeof(struct coredump_req),
.mask = COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT,
+ COREDUMP_REJECT | COREDUMP_WAIT |
+ COREDUMP_RECORDS | COREDUMP_SPARSE,
.size_ack = sizeof(struct coredump_ack),
};
struct coredump_ack ack = {};
@@ -851,7 +861,26 @@ static bool coredump_sock_request(struct core_name *cn, struct coredump_params *
return false;
}
- cn->mask = ack.mask;
+ /* Records only describe a coredump the kernel writes. */
+ if ((ack.mask & COREDUMP_RECORDS) && !(ack.mask & COREDUMP_KERNEL)) {
+ coredump_sock_mark(cprm->file, COREDUMP_MARK_CONFLICTING);
+ return false;
+ }
+
+ /* Zero records only exist inside a record stream. */
+ if ((ack.mask & COREDUMP_SPARSE) && !(ack.mask & COREDUMP_RECORDS)) {
+ coredump_sock_mark(cprm->file, COREDUMP_MARK_CONFLICTING);
+ return false;
+ }
+
+ /* Record header scratch; a bvec can't point at the stack. */
+ if (ack.mask & COREDUMP_RECORDS) {
+ cprm->record_hdr = kmalloc_obj(*cprm->record_hdr);
+ if (!cprm->record_hdr)
+ return false;
+ }
+
+ cprm->mask = ack.mask;
return coredump_sock_mark(cprm->file, COREDUMP_MARK_REQACK);
}
@@ -1032,29 +1061,41 @@ static bool coredump_pipe(struct core_name *cn, struct coredump_params *cprm,
return true;
}
-static bool coredump_write(struct core_name *cn,
- struct coredump_params *cprm,
- const struct linux_binfmt *binfmt)
+static bool coredump_write(struct coredump_params *cprm,
+ const struct linux_binfmt *binfmt)
{
-
- if (dump_interrupted())
+ if (dump_interrupted()) {
+ cprm->state |= COREDUMP_STATE_TRUNCATED;
return true;
+ }
- if (!dump_vma_snapshot(cprm))
+ if (!dump_vma_snapshot(cprm)) {
+ cprm->state |= COREDUMP_STATE_TRUNCATED;
return false;
+ }
file_start_write(cprm->file);
- cn->core_dumped = binfmt->core_dump(cprm);
+ if (!binfmt->core_dump(cprm))
+ cprm->state |= COREDUMP_STATE_TRUNCATED;
/*
- * Ensures that file size is big enough to contain the current
- * file postion. This prevents gdb from complaining about
- * a truncated file if the last "write" to the file was
- * dump_skip.
+ * A trailing hole still has to land in the coredump. Seeking over
+ * it doesn't grow the file, so the last byte of it is written
+ * instead and gdb doesn't see a truncated file. Everything else
+ * puts the hole on the wire as it flushes it.
*/
if (cprm->to_skip) {
- cprm->to_skip--;
- dump_emit(cprm, "", 1);
+ bool flushed;
+
+ if (cprm->file->f_mode & FMODE_LSEEK) {
+ cprm->to_skip--;
+ flushed = dump_emit(cprm, "", 1);
+ } else {
+ flushed = dump_flush_skip(cprm);
+ }
+ if (!flushed)
+ cprm->state |= COREDUMP_STATE_TRUNCATED;
}
+ dump_end_record(cprm);
file_end_write(cprm->file);
free_vma_snapshot(cprm);
return true;
@@ -1069,7 +1110,8 @@ static void coredump_cleanup(struct core_name *cn, struct coredump_params *cprm)
atomic_dec(&core_pipe_count);
}
kfree(cn->corename);
- coredump_finish(cn->core_dumped);
+ kfree(cprm->record_hdr);
+ coredump_finish(cprm->state);
}
static inline bool coredump_skip(const struct coredump_params *cprm,
@@ -1115,24 +1157,24 @@ static void do_coredump(struct core_name *cn, struct coredump_params *cprm,
}
/* Don't even generate the coredump. */
- if (cn->mask & COREDUMP_REJECT)
+ if (cprm->mask & COREDUMP_REJECT)
return;
- if ((cn->mask & COREDUMP_KERNEL) && !coredump_write(cn, cprm, binfmt))
+ if ((cprm->mask & COREDUMP_KERNEL) && !coredump_write(cprm, binfmt))
return;
coredump_sock_shutdown(cprm->file);
/* Let the parent know that a coredump was generated. */
- if (cn->mask & COREDUMP_USERSPACE)
- cn->core_dumped = true;
+ if (cprm->mask & COREDUMP_USERSPACE)
+ cprm->state |= COREDUMP_STATE_STARTED;
/*
* When core_pipe_limit is set we wait for the coredump server
* or usermodehelper to finish before exiting so it can e.g.,
* inspect /proc/<pid>.
*/
- if (cn->mask & COREDUMP_WAIT) {
+ if (cprm->mask & COREDUMP_WAIT) {
switch (cn->core_type) {
case COREDUMP_PIPE:
wait_for_dump_helpers(cprm->file);
@@ -1197,60 +1239,181 @@ void vfs_coredump(const kernel_siginfo_t *siginfo)
* do on a core-file: use only these functions to write out all the
* necessary info.
*/
-static int __dump_emit(struct coredump_params *cprm, const void *addr, int nr)
+static bool dump_records(const struct coredump_params *cprm)
+{
+ return cprm->mask & COREDUMP_RECORDS;
+}
+
+static bool dump_sparse(const struct coredump_params *cprm)
+{
+ return cprm->mask & COREDUMP_SPARSE;
+}
+
+/* Describe the next @len bytes of the coredump. Returns the header size. */
+static size_t dump_record_init(struct coredump_params *cprm,
+ enum coredump_record_type type, u64 flags,
+ u64 len)
+{
+ if (!dump_records(cprm))
+ return 0;
+
+ *cprm->record_hdr = (struct coredump_record_header) {
+ .size = sizeof(*cprm->record_hdr),
+ .type = type,
+ .flags = flags,
+ .offset = cprm->pos,
+ .len = len,
+ };
+
+ return sizeof(*cprm->record_hdr);
+}
+
+/* Write @iter whole or fail. @len is what it advances the coredump by. */
+static bool dump_write_iter(struct coredump_params *cprm, struct iov_iter *iter,
+ size_t len)
{
struct file *file = cprm->file;
+ size_t count = iov_iter_count(iter);
loff_t pos = file->f_pos;
ssize_t n;
- if (cprm->written + nr > cprm->limit)
- return 0;
- if (dump_interrupted())
- return 0;
- n = __kernel_write(file, addr, nr, &pos);
- if (n != nr)
- return 0;
+ n = __kernel_write_iter(file, iter, &pos);
+ if (n != (ssize_t)count)
+ return false;
file->f_pos = pos;
- cprm->written += n;
- cprm->pos += n;
+ cprm->written += count;
+ cprm->pos += len;
+
+ return true;
+}
+
+/* One record, never more than a page. See __dump_emit(). */
+static bool dump_emit_chunk(struct coredump_params *cprm, const void *addr,
+ int nr)
+{
+ struct kvec kvec[2];
+ struct iov_iter iter;
+ unsigned int nseg = 0;
+ size_t hdrlen;
+
+ if (dump_interrupted())
+ return false;
+
+ hdrlen = dump_record_init(cprm, COREDUMP_RECORD_DATA, 0, nr);
+ if (hdrlen) {
+ kvec[nseg].iov_base = cprm->record_hdr;
+ kvec[nseg].iov_len = hdrlen;
+ nseg++;
+ }
+ kvec[nseg].iov_base = (void *)addr;
+ kvec[nseg].iov_len = nr;
+ nseg++;
+
+ iov_iter_kvec(&iter, ITER_SOURCE, kvec, nseg, hdrlen + nr);
+
+ return dump_write_iter(cprm, &iter, nr);
+}
+
+static bool __dump_emit(struct coredump_params *cprm, const void *addr, int nr)
+{
+ if (cprm->written + nr > cprm->limit)
+ return false;
- return 1;
+ while (nr) {
+ int chunk = min_t(int, nr, PAGE_SIZE);
+
+ if (!dump_emit_chunk(cprm, addr, chunk))
+ return false;
+
+ addr += chunk;
+ nr -= chunk;
+ }
+
+ return true;
+}
+
+/* Send a record that stands on its own: a header and nothing else. */
+static bool dump_emit_record(struct coredump_params *cprm,
+ enum coredump_record_type type, u64 flags, u64 len)
+{
+ struct kvec kvec;
+ struct iov_iter iter;
+ size_t hdrlen;
+
+ hdrlen = dump_record_init(cprm, type, flags, len);
+ if (!hdrlen)
+ return false;
+
+ kvec.iov_base = cprm->record_hdr;
+ kvec.iov_len = hdrlen;
+ iov_iter_kvec(&iter, ITER_SOURCE, &kvec, 1, hdrlen);
+
+ return dump_write_iter(cprm, &iter, len);
+}
+
+/* Close the record stream. Only a whole coredump gets an end record. */
+static void dump_end_record(struct coredump_params *cprm)
+{
+ if (cprm->state & COREDUMP_STATE_TRUNCATED)
+ return;
+
+ dump_emit_record(cprm, COREDUMP_RECORD_END, 0, 0);
}
-static int __dump_skip(struct coredump_params *cprm, size_t nr)
+static bool __dump_skip(struct coredump_params *cprm, size_t nr)
{
static char zeroes[PAGE_SIZE];
struct file *file = cprm->file;
+ if (dump_sparse(cprm)) {
+ /* Hand the server the length of the hole instead of the hole itself. */
+ if (dump_interrupted())
+ return false;
+ return dump_emit_record(cprm, COREDUMP_RECORD_ZERO, 0, nr);
+ }
+
if (file->f_mode & FMODE_LSEEK) {
if (dump_interrupted() || vfs_llseek(file, nr, SEEK_CUR) < 0)
- return 0;
+ return false;
cprm->pos += nr;
- return 1;
+ return true;
}
- while (nr > PAGE_SIZE) {
- if (!__dump_emit(cprm, zeroes, PAGE_SIZE))
- return 0;
- nr -= PAGE_SIZE;
+ while (nr) {
+ size_t chunk = min_t(size_t, nr, PAGE_SIZE);
+
+ if (!__dump_emit(cprm, zeroes, chunk))
+ return false;
+
+ nr -= chunk;
}
- return __dump_emit(cprm, zeroes, nr);
+ return true;
}
-int dump_emit(struct coredump_params *cprm, const void *addr, int nr)
+/* Flush the accumulated hole before writing data. */
+static bool dump_flush_skip(struct coredump_params *cprm)
{
if (cprm->to_skip) {
if (!__dump_skip(cprm, cprm->to_skip))
- return 0;
+ return false;
cprm->to_skip = 0;
}
+ return true;
+}
+
+bool dump_emit(struct coredump_params *cprm, const void *addr, int nr)
+{
+ if (!dump_flush_skip(cprm))
+ return false;
return __dump_emit(cprm, addr, nr);
}
EXPORT_SYMBOL(dump_emit);
void dump_skip_to(struct coredump_params *cprm, unsigned long pos)
{
+ if (WARN_ON_ONCE(pos < cprm->pos))
+ return;
cprm->to_skip = pos - cprm->pos;
}
EXPORT_SYMBOL(dump_skip_to);
@@ -1262,37 +1425,32 @@ void dump_skip(struct coredump_params *cprm, size_t nr)
EXPORT_SYMBOL(dump_skip);
#ifdef CONFIG_ELF_CORE
-static int dump_emit_page(struct coredump_params *cprm, struct page *page)
+static bool dump_emit_page(struct coredump_params *cprm, struct page *page)
{
- struct bio_vec bvec;
+ struct bio_vec bvec[2];
struct iov_iter iter;
- struct file *file = cprm->file;
- loff_t pos;
- ssize_t n;
+ unsigned int nseg = 0;
+ size_t hdrlen;
if (!page)
- return 0;
+ return false;
- if (cprm->to_skip) {
- if (!__dump_skip(cprm, cprm->to_skip))
- return 0;
- cprm->to_skip = 0;
- }
+ if (!dump_flush_skip(cprm))
+ return false;
if (cprm->written + PAGE_SIZE > cprm->limit)
- return 0;
+ return false;
if (dump_interrupted())
- return 0;
- pos = file->f_pos;
- bvec_set_page(&bvec, page, PAGE_SIZE, 0);
- iov_iter_bvec(&iter, ITER_SOURCE, &bvec, 1, PAGE_SIZE);
- n = __kernel_write_iter(cprm->file, &iter, &pos);
- if (n != PAGE_SIZE)
- return 0;
- file->f_pos = pos;
- cprm->written += PAGE_SIZE;
- cprm->pos += PAGE_SIZE;
+ return false;
- return 1;
+ /* Hand the record header to the same write as the page it describes. */
+ hdrlen = dump_record_init(cprm, COREDUMP_RECORD_DATA, 0, PAGE_SIZE);
+ if (hdrlen)
+ bvec_set_virt(&bvec[nseg++], cprm->record_hdr, hdrlen);
+ bvec_set_page(&bvec[nseg++], page, PAGE_SIZE, 0);
+
+ iov_iter_bvec(&iter, ITER_SOURCE, bvec, nseg, hdrlen + PAGE_SIZE);
+
+ return dump_write_iter(cprm, &iter, PAGE_SIZE);
}
/*
@@ -1324,18 +1482,19 @@ static inline struct page *dump_page_copy(struct page *src, struct page *dst)
}
#endif
-int dump_user_range(struct coredump_params *cprm, unsigned long start,
- unsigned long len)
+bool dump_user_range(struct coredump_params *cprm, unsigned long start,
+ unsigned long len)
{
unsigned long addr;
struct page *dump_page;
- int locked, ret;
+ int locked;
+ bool ret;
dump_page = dump_page_alloc();
if (!dump_page)
- return 0;
+ return false;
- ret = 0;
+ ret = false;
locked = 0;
for (addr = start; addr < start + len; addr += PAGE_SIZE) {
struct page *page;
@@ -1359,7 +1518,7 @@ int dump_user_range(struct coredump_params *cprm, unsigned long start,
mmap_read_unlock(current->mm);
locked = 0;
}
- int stop = !dump_emit_page(cprm, dump_page_copy(page, dump_page));
+ bool stop = !dump_emit_page(cprm, dump_page_copy(page, dump_page));
put_page(page);
if (stop)
goto out;
@@ -1378,7 +1537,7 @@ int dump_user_range(struct coredump_params *cprm, unsigned long start,
}
cond_resched();
}
- ret = 1;
+ ret = true;
out:
if (locked)
mmap_read_unlock(current->mm);
@@ -1388,14 +1547,14 @@ out:
}
#endif
-int dump_align(struct coredump_params *cprm, int align)
+bool dump_align(struct coredump_params *cprm, int align)
{
unsigned mod = (cprm->pos + cprm->to_skip) & (align - 1);
if (align & (align - 1))
- return 0;
+ return false;
if (mod)
cprm->to_skip += align - mod;
- return 1;
+ return true;
}
EXPORT_SYMBOL(dump_align);
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index f686a37f7a0a..2e87faf9a8c2 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -128,7 +128,8 @@ struct linux_binfmt {
struct module *module;
int (*load_binary)(struct linux_binprm *);
#ifdef CONFIG_COREDUMP
- int (*core_dump)(struct coredump_params *cprm);
+ /* Returns true if the whole coredump was written. */
+ bool (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
#endif
} __randomize_layout;
diff --git a/include/linux/coredump.h b/include/linux/coredump.h
index 7b38ee2e7913..b252bb2843b3 100644
--- a/include/linux/coredump.h
+++ b/include/linux/coredump.h
@@ -6,9 +6,20 @@
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/sched/coredump.h>
+#include <uapi/linux/coredump.h>
#include <asm/siginfo.h>
#ifdef CONFIG_COREDUMP
+/**
+ * enum coredump_state - what happened while the coredump was written
+ * @COREDUMP_STATE_STARTED: the dumper committed to writing a coredump
+ * @COREDUMP_STATE_TRUNCATED: the dumper stopped before it had written all of it
+ */
+enum coredump_state {
+ COREDUMP_STATE_STARTED = (1U << 0),
+ COREDUMP_STATE_TRUNCATED = (1U << 1),
+};
+
struct core_vma_metadata {
unsigned long start, end;
vm_flags_t flags;
@@ -26,7 +37,15 @@ struct coredump_params {
/* Snapshot of dumpable at dump start. */
enum task_dumpable dumpable;
int cpu;
+ /* COREDUMP_* options negotiated with the coredump server. */
+ u64 mask;
+ /* COREDUMP_STATE_* raised while the coredump is written. */
+ enum coredump_state state;
+ /* Record header scratch, NULL unless the coredump is a record stream. */
+ struct coredump_record_header *record_hdr;
+ /* Bytes handed to the file, record headers included. */
loff_t written;
+ /* Offset in the coredump, record headers excluded. */
loff_t pos;
loff_t to_skip;
int vma_count;
@@ -41,13 +60,13 @@ extern unsigned int core_file_note_size_limit;
* These are the only things you should do on a core-file: use only these
* functions to write out all the necessary info.
*/
-extern void dump_skip_to(struct coredump_params *cprm, unsigned long to);
-extern void dump_skip(struct coredump_params *cprm, size_t nr);
-extern int dump_emit(struct coredump_params *cprm, const void *addr, int nr);
-extern int dump_align(struct coredump_params *cprm, int align);
-int dump_user_range(struct coredump_params *cprm, unsigned long start,
- unsigned long len);
-extern void vfs_coredump(const kernel_siginfo_t *siginfo);
+void dump_skip_to(struct coredump_params *cprm, unsigned long to);
+void dump_skip(struct coredump_params *cprm, size_t nr);
+bool dump_emit(struct coredump_params *cprm, const void *addr, int nr);
+bool dump_align(struct coredump_params *cprm, int align);
+bool dump_user_range(struct coredump_params *cprm, unsigned long start,
+ unsigned long len);
+void vfs_coredump(const kernel_siginfo_t *siginfo);
/*
* Logging for the coredump code, ratelimited.
diff --git a/include/uapi/linux/coredump.h b/include/uapi/linux/coredump.h
index dc3789b78af0..f3771861ca48 100644
--- a/include/uapi/linux/coredump.h
+++ b/include/uapi/linux/coredump.h
@@ -11,12 +11,19 @@
* @COREDUMP_USERSPACE: userspace writes coredump
* @COREDUMP_REJECT: don't generate coredump
* @COREDUMP_WAIT: wait for coredump server
+ * @COREDUMP_RECORDS: send the coredump as a sequence of records instead of
+ * as a plain byte stream, see struct coredump_record_header;
+ * requires COREDUMP_KERNEL
+ * @COREDUMP_SPARSE: describe the holes in the coredump as zero records
+ * instead of transferring them; requires COREDUMP_RECORDS
*/
enum {
COREDUMP_KERNEL = (1ULL << 0),
COREDUMP_USERSPACE = (1ULL << 1),
COREDUMP_REJECT = (1ULL << 2),
COREDUMP_WAIT = (1ULL << 3),
+ COREDUMP_RECORDS = (1ULL << 4),
+ COREDUMP_SPARSE = (1ULL << 5),
};
/**
@@ -30,11 +37,11 @@ enum {
* member is set to the size of struct coredump_req and provides a hint
* to userspace how much data can be read. Userspace may use MSG_PEEK to
* peek the size of struct coredump_req and then choose to consume it in
- * one go. Userspace may also simply read a COREDUMP_ACK_SIZE_VER0
+ * one go. Userspace may also simply read a COREDUMP_REQ_SIZE_VER0
* request. If the size the kernel sends is larger userspace simply
* discards any remaining data.
*
- * The coredump_req->mask member is set to the currently know features.
+ * The coredump_req->mask member is set to the currently known features.
* Userspace may only set coredump_ack->mask to the bits raised by the
* kernel in coredump_req->mask.
*
@@ -101,4 +108,72 @@ enum coredump_mark {
__COREDUMP_MARK_MAX = (1U << 31),
};
+/**
+ * enum coredump_record_type - Type of a coredump record
+ *
+ * @COREDUMP_RECORD_DATA: the header is followed by ->len bytes of data
+ * @COREDUMP_RECORD_END: the coredump ends here, the header is not followed
+ * by any data and no further record is sent
+ * @COREDUMP_RECORD_ZERO: the header stands for ->len zero bytes and is not
+ * followed by any data
+ * @__COREDUMP_RECORD_TYPE_MAX: the maximum coredump record type value
+ */
+enum coredump_record_type {
+ COREDUMP_RECORD_DATA = 0U,
+ COREDUMP_RECORD_END = 1U,
+ COREDUMP_RECORD_ZERO = 2U,
+ __COREDUMP_RECORD_TYPE_MAX = (1U << 31),
+};
+
+/**
+ * struct coredump_record_header - header of a coredump record
+ * @size: size of struct coredump_record_header
+ * @type: one of enum coredump_record_type
+ * @flags: modifiers for this record
+ * @offset: offset in the coredump this record starts at
+ * @len: number of coredump bytes this record accounts for
+ *
+ * If the coredump server raises COREDUMP_RECORDS in coredump_ack->mask
+ * the kernel doesn't send the coredump as a plain byte stream. It sends
+ * a sequence of records instead. A COREDUMP_RECORD_DATA record is
+ * followed by @len bytes of actual coredump data. A
+ * COREDUMP_RECORD_ZERO record is followed by nothing and stands for
+ * @len zero bytes. A server that didn't raise COREDUMP_SPARSE never
+ * sees a zero record. Records arrive in order and leave no gaps. So
+ * @offset is the sum of the @len of all records before it.
+ *
+ * The last record is a COREDUMP_RECORD_END record. It is followed by
+ * nothing. Its @len is zero. Its @offset is the size of the coredump.
+ * The kernel only sends it once it has written the whole coredump. A
+ * server that hits end-of-file without having seen an end record must
+ * treat the coredump as incomplete.
+ *
+ * The @size member is set to the size of struct coredump_record_header
+ * the kernel knows and lets the header grow later. It comes first so it
+ * can be peeked. Userspace must consume @size bytes and discard
+ * anything beyond what it knows. It must refuse a @size smaller than
+ * COREDUMP_RECORD_HEADER_SIZE_VER0. @size covers the header alone.
+ * @offset and @len count coredump bytes.
+ *
+ * The @flags member carries modifiers that change how the record is to
+ * be interpreted. No flag is defined yet. Userspace must refuse a
+ * record carrying a flag or a type it doesn't know. Every new record
+ * type is raised in coredump_req->mask as a feature of its own. A
+ * server only ever sees the types it asked for.
+ *
+ * COREDUMP_RECORDS must be combined with COREDUMP_KERNEL, and
+ * COREDUMP_SPARSE with COREDUMP_RECORDS.
+ */
+struct coredump_record_header {
+ __u32 size;
+ __u32 type;
+ __u64 flags;
+ __u64 offset;
+ __u64 len;
+};
+
+enum {
+ COREDUMP_RECORD_HEADER_SIZE_VER0 = 32U, /* size of first published struct */
+};
+
#endif /* _UAPI_LINUX_COREDUMP_H */
diff --git a/tools/include/uapi/linux/coredump.h b/tools/include/uapi/linux/coredump.h
index dc3789b78af0..f3771861ca48 100644
--- a/tools/include/uapi/linux/coredump.h
+++ b/tools/include/uapi/linux/coredump.h
@@ -11,12 +11,19 @@
* @COREDUMP_USERSPACE: userspace writes coredump
* @COREDUMP_REJECT: don't generate coredump
* @COREDUMP_WAIT: wait for coredump server
+ * @COREDUMP_RECORDS: send the coredump as a sequence of records instead of
+ * as a plain byte stream, see struct coredump_record_header;
+ * requires COREDUMP_KERNEL
+ * @COREDUMP_SPARSE: describe the holes in the coredump as zero records
+ * instead of transferring them; requires COREDUMP_RECORDS
*/
enum {
COREDUMP_KERNEL = (1ULL << 0),
COREDUMP_USERSPACE = (1ULL << 1),
COREDUMP_REJECT = (1ULL << 2),
COREDUMP_WAIT = (1ULL << 3),
+ COREDUMP_RECORDS = (1ULL << 4),
+ COREDUMP_SPARSE = (1ULL << 5),
};
/**
@@ -30,11 +37,11 @@ enum {
* member is set to the size of struct coredump_req and provides a hint
* to userspace how much data can be read. Userspace may use MSG_PEEK to
* peek the size of struct coredump_req and then choose to consume it in
- * one go. Userspace may also simply read a COREDUMP_ACK_SIZE_VER0
+ * one go. Userspace may also simply read a COREDUMP_REQ_SIZE_VER0
* request. If the size the kernel sends is larger userspace simply
* discards any remaining data.
*
- * The coredump_req->mask member is set to the currently know features.
+ * The coredump_req->mask member is set to the currently known features.
* Userspace may only set coredump_ack->mask to the bits raised by the
* kernel in coredump_req->mask.
*
@@ -101,4 +108,72 @@ enum coredump_mark {
__COREDUMP_MARK_MAX = (1U << 31),
};
+/**
+ * enum coredump_record_type - Type of a coredump record
+ *
+ * @COREDUMP_RECORD_DATA: the header is followed by ->len bytes of data
+ * @COREDUMP_RECORD_END: the coredump ends here, the header is not followed
+ * by any data and no further record is sent
+ * @COREDUMP_RECORD_ZERO: the header stands for ->len zero bytes and is not
+ * followed by any data
+ * @__COREDUMP_RECORD_TYPE_MAX: the maximum coredump record type value
+ */
+enum coredump_record_type {
+ COREDUMP_RECORD_DATA = 0U,
+ COREDUMP_RECORD_END = 1U,
+ COREDUMP_RECORD_ZERO = 2U,
+ __COREDUMP_RECORD_TYPE_MAX = (1U << 31),
+};
+
+/**
+ * struct coredump_record_header - header of a coredump record
+ * @size: size of struct coredump_record_header
+ * @type: one of enum coredump_record_type
+ * @flags: modifiers for this record
+ * @offset: offset in the coredump this record starts at
+ * @len: number of coredump bytes this record accounts for
+ *
+ * If the coredump server raises COREDUMP_RECORDS in coredump_ack->mask
+ * the kernel doesn't send the coredump as a plain byte stream. It sends
+ * a sequence of records instead. A COREDUMP_RECORD_DATA record is
+ * followed by @len bytes of actual coredump data. A
+ * COREDUMP_RECORD_ZERO record is followed by nothing and stands for
+ * @len zero bytes. A server that didn't raise COREDUMP_SPARSE never
+ * sees a zero record. Records arrive in order and leave no gaps. So
+ * @offset is the sum of the @len of all records before it.
+ *
+ * The last record is a COREDUMP_RECORD_END record. It is followed by
+ * nothing. Its @len is zero. Its @offset is the size of the coredump.
+ * The kernel only sends it once it has written the whole coredump. A
+ * server that hits end-of-file without having seen an end record must
+ * treat the coredump as incomplete.
+ *
+ * The @size member is set to the size of struct coredump_record_header
+ * the kernel knows and lets the header grow later. It comes first so it
+ * can be peeked. Userspace must consume @size bytes and discard
+ * anything beyond what it knows. It must refuse a @size smaller than
+ * COREDUMP_RECORD_HEADER_SIZE_VER0. @size covers the header alone.
+ * @offset and @len count coredump bytes.
+ *
+ * The @flags member carries modifiers that change how the record is to
+ * be interpreted. No flag is defined yet. Userspace must refuse a
+ * record carrying a flag or a type it doesn't know. Every new record
+ * type is raised in coredump_req->mask as a feature of its own. A
+ * server only ever sees the types it asked for.
+ *
+ * COREDUMP_RECORDS must be combined with COREDUMP_KERNEL, and
+ * COREDUMP_SPARSE with COREDUMP_RECORDS.
+ */
+struct coredump_record_header {
+ __u32 size;
+ __u32 type;
+ __u64 flags;
+ __u64 offset;
+ __u64 len;
+};
+
+enum {
+ COREDUMP_RECORD_HEADER_SIZE_VER0 = 32U, /* size of first published struct */
+};
+
#endif /* _UAPI_LINUX_COREDUMP_H */
diff --git a/tools/testing/selftests/coredump/coredump_socket_protocol_test.c b/tools/testing/selftests/coredump/coredump_socket_protocol_test.c
index d9fa6239b5a9..daff908232a2 100644
--- a/tools/testing/selftests/coredump/coredump_socket_protocol_test.c
+++ b/tools/testing/selftests/coredump/coredump_socket_protocol_test.c
@@ -151,9 +151,7 @@ TEST_F(coredump, socket_request_kernel)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_kernel: check_coredump_req failed\n");
goto out;
}
@@ -301,9 +299,7 @@ TEST_F(coredump, socket_request_userspace)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_userspace: check_coredump_req failed\n");
goto out;
}
@@ -441,9 +437,7 @@ TEST_F(coredump, socket_request_reject)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_reject: check_coredump_req failed\n");
goto out;
}
@@ -581,9 +575,7 @@ TEST_F(coredump, socket_request_invalid_flag_combination)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_invalid_flag_combination: check_coredump_req failed\n");
goto out;
}
@@ -702,9 +694,7 @@ TEST_F(coredump, socket_request_unknown_flag)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_unknown_flag: check_coredump_req failed\n");
goto out;
}
@@ -822,9 +812,7 @@ TEST_F(coredump, socket_request_invalid_size_small)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_invalid_size_small: check_coredump_req failed\n");
goto out;
}
@@ -944,9 +932,7 @@ TEST_F(coredump, socket_request_invalid_size_large)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_request_invalid_size_large: check_coredump_req failed\n");
goto out;
}
@@ -1355,9 +1341,7 @@ TEST_F_TIMEOUT(coredump, socket_multiple_crashing_coredumps, 500)
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "check_coredump_req failed for fd %d\n", fd_coredump);
goto out;
}
@@ -1509,9 +1493,7 @@ TEST_F_TIMEOUT(coredump, socket_multiple_crashing_coredumps_epoll_workers, 500)
fprintf(stderr, "socket_multiple_crashing_coredumps_epoll_workers: read_coredump_req failed\n");
goto out;
}
- if (!check_coredump_req(&req, COREDUMP_ACK_SIZE_VER0,
- COREDUMP_KERNEL | COREDUMP_USERSPACE |
- COREDUMP_REJECT | COREDUMP_WAIT)) {
+ if (!check_coredump_req(&req)) {
fprintf(stderr, "socket_multiple_crashing_coredumps_epoll_workers: check_coredump_req failed\n");
goto out;
}
@@ -1591,4 +1573,751 @@ out:
wait_and_check_coredump_server(pid_coredump_server, _metadata, self);
}
+/*
+ * Reassemble a record stream and check that what comes out is an ELF
+ * core file. The records themselves are validated by recv_coredump_records().
+ */
+TEST_F(coredump, socket_request_sparse_reassemble)
+{
+ int fd_core_file, pidfd, status;
+ pid_t pid, pid_coredump_server;
+ struct pidfd_info info = {};
+ int ipc_sockets[2];
+ char c;
+
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 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_file = -1;
+ int exit_code = EXIT_FAILURE;
+ struct coredump_req req = {};
+
+ close(ipc_sockets[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;
+
+ fd_file = creat("/tmp/coredump.file", 0644);
+ if (fd_file < 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;
+
+ if (recv_coredump_records(fd_coredump, fd_file, NULL, NULL, -1) < 0)
+ goto out;
+
+ exit_code = EXIT_SUCCESS;
+out:
+ if (fd_file >= 0)
+ close(fd_file);
+ 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);
+ 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();
+
+ pidfd = sys_pidfd_open(pid, 0);
+ ASSERT_GE(pidfd, 0);
+
+ waitpid(pid, &status, 0);
+ ASSERT_TRUE(WIFSIGNALED(status));
+ ASSERT_TRUE(WCOREDUMP(status));
+
+ 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);
+
+ /* What the records reassemble into has to be an 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);
+}
+
+/*
+ * Crash a child with a mostly-unpopulated mapping and reassemble its
+ * record stream, reporting what crossed the socket and the coredump
+ * size the records describe. With @kill_peer the server kills the task
+ * once the coredump is under way so the kernel has to cut it short.
+ */
+static void check_record_dump(struct __test_metadata *const _metadata,
+ FIXTURE_DATA(coredump) *self, __u64 ack_mask,
+ bool kill_peer, ssize_t *received,
+ off_t *coredump_size)
+{
+ bool truncated = false;
+ int pidfd, status;
+ pid_t pid, pid_coredump_server;
+ struct pidfd_info info = {};
+ int ipc_sockets[2];
+ int pipefds[2];
+ 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_file = -1;
+ int exit_code = EXIT_FAILURE;
+ struct coredump_req req = {};
+ bool is_truncated = false;
+ 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 reassembled coredump is bigger than the mapping the
+ * child made, so keep it on the detached tmpfs and sparse.
+ */
+ fd_file = open_coredump_tmpfile(self->fd_tmpfs_detached);
+ if (fd_file < 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, ack_mask, 0))
+ goto out;
+
+ if (!read_marker(fd_coredump, COREDUMP_MARK_REQACK))
+ goto out;
+
+ ret = recv_coredump_records(fd_coredump, fd_file, &size, &is_truncated,
+ kill_peer ? fd_peer_pidfd : -1);
+ if (ret < 0)
+ 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;
+ if (write_nointr(pipefds[1], &is_truncated,
+ sizeof(is_truncated)) != sizeof(is_truncated))
+ goto out;
+
+ exit_code = EXIT_SUCCESS;
+out:
+ close(pipefds[1]);
+ if (fd_file >= 0)
+ close(fd_file);
+ 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_EQ(read_nointr(pipefds[0], received, sizeof(*received)),
+ sizeof(*received));
+ ASSERT_EQ(read_nointr(pipefds[0], coredump_size, sizeof(*coredump_size)),
+ sizeof(*coredump_size));
+ ASSERT_EQ(read_nointr(pipefds[0], &truncated, sizeof(truncated)),
+ sizeof(truncated));
+ EXPECT_EQ(close(pipefds[0]), 0);
+
+ wait_and_check_coredump_server(pid_coredump_server, _metadata, self);
+
+ if (kill_peer) {
+ /* The kernel gave up partway, so no end record closed the stream. */
+ ASSERT_TRUE(truncated);
+ ASSERT_FALSE(WCOREDUMP(status));
+ ASSERT_LT(*coredump_size, (off_t)SPARSE_MAPPING_SIZE);
+ return;
+ }
+
+ ASSERT_FALSE(truncated);
+ ASSERT_TRUE(WCOREDUMP(status));
+
+ ASSERT_TRUE(get_pidfd_info(pidfd, &info));
+ ASSERT_GT((info.mask & PIDFD_INFO_COREDUMP), 0);
+ ASSERT_GT((info.coredump_mask & PIDFD_COREDUMPED), 0);
+
+ /* The mapping is in the coredump, holes included. */
+ ASSERT_GT(*coredump_size, (off_t)SPARSE_MAPPING_SIZE);
+}
+
+/*
+ * A mapping that has been written to is dumped whole, including the parts
+ * of it that were never faulted in. With COREDUMP_SPARSE the holes stay
+ * off the wire.
+ */
+TEST_F(coredump, socket_request_sparse_hole)
+{
+ off_t coredump_size = 0;
+ ssize_t received = 0;
+
+ check_record_dump(_metadata, self,
+ COREDUMP_KERNEL | COREDUMP_RECORDS |
+ COREDUMP_SPARSE | COREDUMP_WAIT,
+ false, &received, &coredump_size);
+
+ /* The holes didn't have to go over the socket. */
+ ASSERT_LT(received, coredump_size / 8);
+}
+
+/*
+ * COREDUMP_RECORDS alone splits the stream into records but elides
+ * nothing: the holes cross the socket as data records.
+ */
+TEST_F(coredump, socket_request_records_hole)
+{
+ off_t coredump_size = 0;
+ ssize_t received = 0;
+
+ check_record_dump(_metadata, self,
+ COREDUMP_KERNEL | COREDUMP_RECORDS | COREDUMP_WAIT,
+ false, &received, &coredump_size);
+
+ /* Records alone elide nothing, so everything crossed the socket. */
+ ASSERT_GT(received, coredump_size);
+}
+
+/*
+ * A coredump the kernel gives up on halfway still ends in an end record,
+ * and that record says the coredump is incomplete. COREDUMP_SPARSE is left
+ * out on purpose: the holes have to cross the socket so the coredump is
+ * far larger than the socket buffer and the kernel is still writing it
+ * when the kill lands.
+ */
+TEST_F(coredump, socket_request_records_truncated)
+{
+ off_t coredump_size = 0;
+ ssize_t received = 0;
+
+ check_record_dump(_metadata, self,
+ COREDUMP_KERNEL | COREDUMP_RECORDS | COREDUMP_WAIT,
+ true, &received, &coredump_size);
+
+ /* The end record crossed the socket even though the task was killed. */
+ 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)
+{
+ int pidfd, status;
+ pid_t pid, pid_coredump_server;
+ struct pidfd_info info = {};
+ int ipc_sockets[2];
+ char c;
+
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 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 exit_code = EXIT_FAILURE;
+ struct coredump_req req = {};
+
+ close(ipc_sockets[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;
+
+ if (!read_coredump_req(fd_coredump, &req))
+ goto out;
+
+ if (!check_coredump_req(&req))
+ goto out;
+
+ if (!send_coredump_ack(fd_coredump, &req, ack_mask, 0))
+ goto out;
+
+ if (!read_marker(fd_coredump, COREDUMP_MARK_CONFLICTING))
+ goto out;
+
+ exit_code = EXIT_SUCCESS;
+out:
+ 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);
+ 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();
+
+ pidfd = sys_pidfd_open(pid, 0);
+ ASSERT_GE(pidfd, 0);
+
+ waitpid(pid, &status, 0);
+ ASSERT_TRUE(WIFSIGNALED(status));
+ ASSERT_FALSE(WCOREDUMP(status));
+
+ 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);
+}
+
+/* COREDUMP_RECORDS applies to a coredump the kernel writes, nothing else. */
+TEST_F(coredump, socket_request_records_without_kernel)
+{
+ check_conflicting_ack(_metadata, self, COREDUMP_USERSPACE | COREDUMP_RECORDS);
+}
+
+/* A zero record can't exist outside a record stream. */
+TEST_F(coredump, socket_request_sparse_without_records)
+{
+ check_conflicting_ack(_metadata, self, COREDUMP_KERNEL | COREDUMP_SPARSE);
+}
+
+/* What the server reports back about the coredump it decided to take. */
+struct stream_choice {
+ bool sparse;
+ ssize_t received;
+ off_t size;
+ ssize_t vm_size;
+};
+
+/*
+ * The kernel blocks in the coredump request until the ack arrives, so a
+ * coredump server gets to look at the task before it commits to a
+ * stream. Take the record stream only for a task whose mappings are
+ * worth it and the plain byte stream for everything else.
+ */
+static void check_stream_choice(struct __test_metadata *const _metadata,
+ FIXTURE_DATA(coredump) *self, bool big,
+ struct stream_choice *choice)
+{
+ int pidfd, status;
+ pid_t pid, pid_coredump_server;
+ struct pidfd_info info = {};
+ int ipc_sockets[2];
+ int pipefds[2];
+ 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_file = -1;
+ int exit_code = EXIT_FAILURE;
+ struct coredump_req req = {};
+ struct stream_choice got = {};
+ __u64 mask;
+
+ 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 reassembled coredump is bigger than the mapping the
+ * child made, so keep it on the detached tmpfs and sparse.
+ */
+ fd_file = open_coredump_tmpfile(self->fd_tmpfs_detached);
+ if (fd_file < 0)
+ goto out;
+
+ if (!read_coredump_req(fd_coredump, &req))
+ goto out;
+
+ if (!check_coredump_req(&req))
+ goto out;
+
+ /*
+ * Nothing is on the wire yet and the kernel is waiting for
+ * the ack, so there is all the time in the world to look at
+ * the task and decide what to ask it for.
+ */
+ got.vm_size = peer_vm_size(fd_peer_pidfd);
+ if (got.vm_size < 0)
+ goto out;
+ got.sparse = got.vm_size >= SPARSE_STREAM_THRESHOLD;
+
+ fprintf(stderr, "Peer maps %zd bytes, asking for %s\n",
+ got.vm_size,
+ got.sparse ? "a sparse record stream" : "a byte stream");
+
+ mask = COREDUMP_KERNEL | COREDUMP_WAIT;
+ if (got.sparse)
+ mask |= COREDUMP_RECORDS | COREDUMP_SPARSE;
+
+ if (!send_coredump_ack(fd_coredump, &req, mask, 0))
+ goto out;
+
+ if (!read_marker(fd_coredump, COREDUMP_MARK_REQACK))
+ goto out;
+
+ if (got.sparse) {
+ got.received = recv_coredump_records(fd_coredump, fd_file,
+ &got.size, NULL, -1);
+ } else {
+ got.received = recv_coredump_bytes(fd_coredump, fd_file);
+ got.size = got.received;
+ }
+ if (got.received < 0)
+ goto out;
+
+ /* Either way a debugger has to see an ordinary core file. */
+ if (!is_elf_core(fd_file))
+ goto out;
+
+ if (write_nointr(pipefds[1], &got, sizeof(got)) != sizeof(got))
+ goto out;
+
+ exit_code = EXIT_SUCCESS;
+out:
+ close(pipefds[1]);
+ if (fd_file >= 0)
+ close(fd_file);
+ 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(big ? SPARSE_MAPPING_SIZE : PAGE_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], choice, sizeof(*choice)),
+ sizeof(*choice));
+ 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);
+}
+
+/* A task with little mapped isn't worth a record stream. */
+TEST_F(coredump, socket_request_stream_choice_small)
+{
+ struct stream_choice choice = {};
+
+ check_stream_choice(_metadata, self, false, &choice);
+
+ ASSERT_LT(choice.vm_size, (ssize_t)SPARSE_STREAM_THRESHOLD);
+ ASSERT_FALSE(choice.sparse);
+ ASSERT_GT(choice.received, 0);
+}
+
+/* A task sitting on a big mapping is. */
+TEST_F(coredump, socket_request_stream_choice_large)
+{
+ struct stream_choice choice = {};
+
+ check_stream_choice(_metadata, self, true, &choice);
+
+ ASSERT_GE(choice.vm_size, (ssize_t)SPARSE_STREAM_THRESHOLD);
+ ASSERT_TRUE(choice.sparse);
+ ASSERT_GT(choice.size, (off_t)SPARSE_MAPPING_SIZE);
+
+ /* The holes didn't have to go over the socket. */
+ ASSERT_LT(choice.received, choice.size / 8);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/coredump/coredump_test.h b/tools/testing/selftests/coredump/coredump_test.h
index ed47f01fa53c..8d99b5cb2f12 100644
--- a/tools/testing/selftests/coredump/coredump_test.h
+++ b/tools/testing/selftests/coredump/coredump_test.h
@@ -3,18 +3,9 @@
#ifndef __COREDUMP_TEST_H
#define __COREDUMP_TEST_H
-#include <stdbool.h>
-#include <sys/types.h>
-#include <linux/coredump.h>
-
#include "../kselftest_harness.h"
-#include "../pidfd/pidfd.h"
-
-#ifndef PAGE_SIZE
-#define PAGE_SIZE 4096
-#endif
-#define NUM_THREAD_SPAWN 128
+#include "coredump_test_helpers.h"
/* Coredump fixture */
FIXTURE(coredump)
@@ -24,15 +15,6 @@ FIXTURE(coredump)
int fd_tmpfs_detached;
};
-/* Shared helper function declarations */
-void *do_nothing(void *arg);
-void crashing_child(void);
-int create_detached_tmpfs(void);
-int create_and_listen_unix_socket(const char *path);
-bool set_core_pattern(const char *pattern);
-int get_peer_pidfd(int fd);
-bool get_pidfd_info(int fd_peer_pidfd, struct pidfd_info *info);
-
/* Inline helper that uses harness types */
static inline void wait_and_check_coredump_server(pid_t pid_coredump_server,
struct __test_metadata *const _metadata,
@@ -45,15 +27,4 @@ static inline void wait_and_check_coredump_server(pid_t pid_coredump_server,
ASSERT_EQ(WEXITSTATUS(status), 0);
}
-/* Protocol helper function declarations */
-ssize_t recv_marker(int fd);
-bool read_marker(int fd, enum coredump_mark mark);
-bool read_coredump_req(int fd, struct coredump_req *req);
-bool send_coredump_ack(int fd, const struct coredump_req *req,
- __u64 mask, size_t size_ack);
-bool check_coredump_req(const struct coredump_req *req, size_t min_size,
- __u64 required_mask);
-int open_coredump_tmpfile(int fd_tmpfs_detached);
-void process_coredump_worker(int fd_coredump, int fd_peer_pidfd, int fd_core_file);
-
#endif /* __COREDUMP_TEST_H */
diff --git a/tools/testing/selftests/coredump/coredump_test_helpers.c b/tools/testing/selftests/coredump/coredump_test_helpers.c
index 2a20faf9cb0a..d7cc448eeaf4 100644
--- a/tools/testing/selftests/coredump/coredump_test_helpers.c
+++ b/tools/testing/selftests/coredump/coredump_test_helpers.c
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0
#include <assert.h>
+#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
+#include <link.h>
#include <linux/coredump.h>
#include <linux/fs.h>
#include <pthread.h>
@@ -13,6 +15,7 @@
#include <string.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
+#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
@@ -20,24 +23,15 @@
#include <unistd.h>
#include "../filesystems/wrappers.h"
-#include "../pidfd/pidfd.h"
-/* Forward declarations to avoid including harness header */
-struct __test_metadata;
+#include "coredump_test_helpers.h"
-/* Match the fixture definition from coredump_test.h */
-struct _fixture_coredump_data {
- char original_core_pattern[256];
- pid_t pid_coredump_server;
- int fd_tmpfs_detached;
-};
-
-#ifndef PAGE_SIZE
-#define PAGE_SIZE 4096
+#if __ELF_NATIVE_CLASS == 64
+#define COREDUMP_ELFCLASS ELFCLASS64
+#else
+#define COREDUMP_ELFCLASS ELFCLASS32
#endif
-#define NUM_THREAD_SPAWN 128
-
void *do_nothing(void *arg)
{
(void)arg;
@@ -59,6 +53,1075 @@ void crashing_child(void)
i = *(volatile int *)NULL;
}
+void crashing_child_sparse(size_t size)
+{
+ char *p;
+
+ /*
+ * Touch the first and the last page. This will cause the whole mapping
+ * to be dumped because it has been written to. Everything between
+ * those two pages is a hole though.
+ */
+ p = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
+ if (p != MAP_FAILED) {
+ p[0] = 'x';
+ p[size - 1] = 'x';
+ }
+
+ /* crash on purpose */
+ *(volatile int *)NULL = 0;
+}
+
+/* Sink a reassembled record stream is handed to, record by record. */
+struct coredump_record_sink {
+ /* @len bytes of coredump data that belong at @offset. */
+ int (*data)(void *ctx, const void *buf, size_t len, __u64 offset);
+ /* @len zero bytes that belong at @offset. */
+ int (*zero)(void *ctx, __u64 offset, __u64 len);
+ void *ctx;
+};
+
+/* Read @len bytes off the socket and hand them to @sink, if there is one. */
+static ssize_t recv_record_bytes(int fd_coredump, __u64 len,
+ const struct coredump_record_sink *sink,
+ __u64 offset)
+{
+ ssize_t received = 0;
+
+ while (len) {
+ char buffer[PAGE_SIZE];
+ size_t chunk = len < sizeof(buffer) ? len : sizeof(buffer);
+ ssize_t ret;
+
+ ret = recv(fd_coredump, buffer, chunk, MSG_WAITALL);
+ if (ret <= 0) {
+ fprintf(stderr, "%s: short read %zd: %m\n",
+ __func__, ret);
+ return -1;
+ }
+
+ if (sink && sink->data(sink->ctx, buffer, ret, offset + received))
+ return -1;
+
+ received += ret;
+ len -= ret;
+ }
+
+ return received;
+}
+
+/* Put the data where the records say it goes and leave the holes alone. */
+static int file_sink_data(void *ctx, const void *buf, size_t len, __u64 offset)
+{
+ int fd = *(int *)ctx;
+
+ if (pwrite(fd, buf, len, offset) != (ssize_t)len) {
+ fprintf(stderr, "%s: pwrite failed: %m\n", __func__);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int file_sink_zero(void *ctx, __u64 offset, __u64 len)
+{
+ /* Nothing has to be written for a hole. */
+ return 0;
+}
+
+/*
+ * Read a coredump strea and funnel it into @sink. Allow to pass in a
+ * @fd_peer_pidfd to simulate coredump truncation by killing it after having
+ * received a coredump record.
+ */
+static ssize_t __recv_coredump_records(int fd_coredump,
+ const struct coredump_record_sink *sink,
+ off_t *coredump_size, bool *truncated,
+ int fd_peer_pidfd)
+{
+ ssize_t received = 0;
+ off_t size = 0;
+ bool is_truncated = false;
+ bool ended = false;
+ char trailing;
+
+ while (!ended) {
+ struct coredump_record_header record = {};
+ size_t known_size;
+ ssize_t ret;
+
+ /* Peek the header size the way read_coredump_req() does. */
+ ret = recv(fd_coredump, &record, sizeof(record.size),
+ MSG_PEEK | MSG_WAITALL);
+ if (ret == 0) {
+ /* Nothing closed the stream, so the coredump was cut short. */
+ if (truncated) {
+ is_truncated = true;
+ break;
+ }
+ fprintf(stderr, "%s: stream ended without an end record\n",
+ __func__);
+ return -1;
+ }
+ if (ret != sizeof(record.size)) {
+ fprintf(stderr, "%s: short record peek %zd: %m\n",
+ __func__, ret);
+ return -1;
+ }
+
+ if (record.size < COREDUMP_RECORD_HEADER_SIZE_VER0) {
+ fprintf(stderr, "%s: header size %u below minimum %u\n",
+ __func__, record.size,
+ COREDUMP_RECORD_HEADER_SIZE_VER0);
+ return -1;
+ }
+
+ /* Consume as much of the header as we know about. */
+ known_size = record.size < sizeof(record) ? record.size : sizeof(record);
+ ret = recv(fd_coredump, &record, known_size, MSG_WAITALL);
+ if (ret != (ssize_t)known_size) {
+ fprintf(stderr, "%s: short record read %zd: %m\n",
+ __func__, ret);
+ return -1;
+ }
+ received += ret;
+
+ /*
+ * A flag changes what the record means, so refuse one we
+ * don't know rather than guess.
+ */
+ if (record.flags) {
+ fprintf(stderr, "%s: unknown header flags 0x%llx\n",
+ __func__, (unsigned long long)record.flags);
+ return -1;
+ }
+
+ /* Discard any part of the header we have no use for. */
+ ret = recv_record_bytes(fd_coredump, record.size - known_size,
+ NULL, 0);
+ if (ret < 0)
+ return -1;
+ received += ret;
+
+ /* Records are sent in order and they don't leave gaps. */
+ if (record.offset != (__u64)size) {
+ fprintf(stderr, "%s: record at %llu, expected %llu\n",
+ __func__, (unsigned long long)record.offset,
+ (unsigned long long)size);
+ return -1;
+ }
+
+ switch (record.type) {
+ case COREDUMP_RECORD_ZERO:
+ /* A hole. It comes with no data and needs none. */
+ if (sink->zero(sink->ctx, record.offset, record.len))
+ return -1;
+ break;
+ case COREDUMP_RECORD_DATA:
+ ret = recv_record_bytes(fd_coredump, record.len, sink,
+ record.offset);
+ if (ret < 0)
+ return -1;
+ received += ret;
+ if (fd_peer_pidfd >= 0) {
+ if (sys_pidfd_send_signal(fd_peer_pidfd, SIGKILL,
+ NULL, 0)) {
+ fprintf(stderr, "%s: kill failed: %m\n",
+ __func__);
+ return -1;
+ }
+ fd_peer_pidfd = -1;
+ }
+ break;
+ case COREDUMP_RECORD_END:
+ /* The coredump ends here and nothing follows it. */
+ if (record.len) {
+ fprintf(stderr, "%s: end record covers %llu bytes\n",
+ __func__,
+ (unsigned long long)record.len);
+ return -1;
+ }
+ ended = true;
+ break;
+ default:
+ fprintf(stderr, "%s: unknown record type %u\n",
+ __func__, record.type);
+ return -1;
+ }
+
+ size += record.len;
+ }
+
+ /* The end record is the last thing on the wire. */
+ if (recv(fd_coredump, &trailing, sizeof(trailing), MSG_DONTWAIT) > 0) {
+ fprintf(stderr, "%s: data after the end record\n", __func__);
+ return -1;
+ }
+
+ if (truncated)
+ *truncated = is_truncated;
+
+ *coredump_size = size;
+
+ fprintf(stderr, "Received %zd bytes for a %s coredump of %llu bytes\n",
+ received, is_truncated ? "truncated" : "complete",
+ (unsigned long long)size);
+ return received;
+}
+
+/* Reassemble a record stream into the coredump it describes. */
+ssize_t recv_coredump_records(int fd_coredump, int fd_core_file,
+ off_t *coredump_size, bool *truncated,
+ int fd_peer_pidfd)
+{
+ struct coredump_record_sink sink = {
+ .data = file_sink_data,
+ .zero = file_sink_zero,
+ .ctx = &fd_core_file,
+ };
+ ssize_t received;
+ off_t size = 0;
+
+ received = __recv_coredump_records(fd_coredump, &sink, &size, truncated,
+ fd_peer_pidfd);
+ if (received < 0)
+ return -1;
+
+ /*
+ * Nothing is written for a hole, so grow the file to the size the
+ * records describe in case the coredump ended in one.
+ */
+ if (ftruncate(fd_core_file, size) < 0) {
+ fprintf(stderr, "%s: ftruncate to %llu failed: %m\n",
+ __func__, (unsigned long long)size);
+ return -1;
+ }
+
+ if (coredump_size)
+ *coredump_size = size;
+
+ return received;
+}
+
+/* The ELF header of a native core file. */
+static bool is_core_ehdr(const ElfW(Ehdr) *ehdr)
+{
+ return !memcmp(ehdr->e_ident, ELFMAG, SELFMAG) &&
+ ehdr->e_ident[EI_CLASS] == COREDUMP_ELFCLASS &&
+ ehdr->e_type == ET_CORE;
+}
+
+/* Whatever the server ends up with has to be an ELF core file. */
+bool is_elf_core(int fd)
+{
+ ElfW(Ehdr) ehdr;
+
+ if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) {
+ fprintf(stderr, "%s: short read: %m\n", __func__);
+ return false;
+ }
+
+ if (!is_core_ehdr(&ehdr)) {
+ fprintf(stderr, "%s: not an ELF core file\n", __func__);
+ return false;
+ }
+
+ 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;
+}
+
+/* Read a plain coredump byte stream to end-of-file. */
+ssize_t recv_coredump_bytes(int fd_coredump, int fd_core_file)
+{
+ ssize_t received = 0;
+
+ for (;;) {
+ char buffer[PAGE_SIZE];
+ ssize_t ret = read_nointr(fd_coredump, buffer, sizeof(buffer));
+
+ if (ret < 0) {
+ fprintf(stderr, "%s: read failed: %m\n", __func__);
+ return -1;
+ }
+ if (ret == 0)
+ break;
+
+ if (write_nointr(fd_core_file, buffer, ret) != ret) {
+ fprintf(stderr, "%s: write failed: %m\n", __func__);
+ return -1;
+ }
+ received += ret;
+ }
+
+ fprintf(stderr, "Received %zd bytes of coredump\n", received);
+ return received;
+}
+
int create_detached_tmpfs(void)
{
int fd_context, fd_tmpfs;
@@ -101,6 +1164,7 @@ int create_and_listen_unix_socket(const char *path)
return fd;
out:
+ fprintf(stderr, "%s: %s: %m\n", __func__, path);
if (fd >= 0)
close(fd);
return -1;
@@ -153,6 +1217,37 @@ bool get_pidfd_info(int fd_peer_pidfd, struct pidfd_info *info)
return true;
}
+/*
+ * How much the peer has mapped. The task is parked in the coredump
+ * handshake, so its mm is still there to be looked at.
+ */
+ssize_t peer_vm_size(int fd_peer_pidfd)
+{
+ struct pidfd_info info = {};
+ unsigned long pages;
+ char path[64];
+ FILE *f;
+
+ if (!get_pidfd_info(fd_peer_pidfd, &info))
+ return -1;
+
+ snprintf(path, sizeof(path), "/proc/%d/statm", info.pid);
+ f = fopen(path, "r");
+ if (!f) {
+ fprintf(stderr, "%s: %s: %m\n", __func__, path);
+ return -1;
+ }
+
+ if (fscanf(f, "%lu", &pages) != 1) {
+ fprintf(stderr, "%s: %s: no size\n", __func__, path);
+ fclose(f);
+ return -1;
+ }
+ fclose(f);
+
+ return (ssize_t)pages * sysconf(_SC_PAGESIZE);
+}
+
/* Protocol helper functions */
ssize_t recv_marker(int fd)
@@ -200,7 +1295,7 @@ bool read_marker(int fd, enum coredump_mark mark)
bool read_coredump_req(int fd, struct coredump_req *req)
{
ssize_t ret;
- size_t field_size, user_size, ack_size, kernel_size, remaining_size;
+ size_t field_size, user_size, known_size, kernel_size, remaining_size;
memset(req, 0, sizeof(*req));
field_size = sizeof(req->size);
@@ -214,9 +1309,9 @@ bool read_coredump_req(int fd, struct coredump_req *req)
}
kernel_size = req->size;
- if (kernel_size < COREDUMP_ACK_SIZE_VER0) {
+ if (kernel_size < COREDUMP_REQ_SIZE_VER0) {
fprintf(stderr, "read_coredump_req: kernel_size %zu < min %d\n",
- kernel_size, COREDUMP_ACK_SIZE_VER0);
+ kernel_size, COREDUMP_REQ_SIZE_VER0);
return false;
}
if (kernel_size >= PAGE_SIZE) {
@@ -225,20 +1320,20 @@ bool read_coredump_req(int fd, struct coredump_req *req)
return false;
}
- /* Use the minimum of user and kernel size to read the full request. */
+ /* Consume as much of the request as we know about. */
user_size = sizeof(struct coredump_req);
- ack_size = user_size < kernel_size ? user_size : kernel_size;
- ret = recv(fd, req, ack_size, MSG_WAITALL);
- if (ret != ack_size)
+ known_size = user_size < kernel_size ? user_size : kernel_size;
+ ret = recv(fd, req, known_size, MSG_WAITALL);
+ if (ret != known_size)
return false;
fprintf(stderr, "Read coredump request with size %u and mask 0x%llx\n",
req->size, (unsigned long long)req->mask);
- if (user_size > kernel_size)
- remaining_size = user_size - kernel_size;
- else
+ if (kernel_size > user_size)
remaining_size = kernel_size - user_size;
+ else
+ remaining_size = 0;
if (PAGE_SIZE <= remaining_size)
return false;
@@ -250,7 +1345,7 @@ bool read_coredump_req(int fd, struct coredump_req *req)
if (remaining_size) {
char buffer[PAGE_SIZE];
- ret = recv(fd, buffer, sizeof(buffer), MSG_WAITALL);
+ ret = recv(fd, buffer, remaining_size, MSG_WAITALL);
if (ret != remaining_size)
return false;
fprintf(stderr, "Discarded %zu bytes of data after coredump request\n", remaining_size);
@@ -279,23 +1374,35 @@ bool send_coredump_ack(int fd, const struct coredump_req *req,
large_ack.ack.mask = mask;
large_ack.ack.size = size_ack;
ret = send(fd, &large_ack, size_ack, MSG_NOSIGNAL);
- if (ret != size_ack)
+ if (ret != size_ack) {
+ fprintf(stderr, "%s: short send %zd: %m\n", __func__, ret);
return false;
+ }
fprintf(stderr, "Sent coredump ack with size %zu and mask 0x%llx\n",
size_ack, (unsigned long long)mask);
return true;
}
-bool check_coredump_req(const struct coredump_req *req, size_t min_size,
- __u64 required_mask)
+/* Every option the kernel is expected to advertise in coredump_req->mask. */
+#define TEST_REQ_MASK_ALL \
+ (COREDUMP_KERNEL | COREDUMP_USERSPACE | \
+ COREDUMP_REJECT | COREDUMP_WAIT | \
+ COREDUMP_RECORDS | COREDUMP_SPARSE)
+
+bool check_coredump_req(const struct coredump_req *req)
{
- if (req->size < min_size)
+ if (req->size < COREDUMP_REQ_SIZE_VER0) {
+ fprintf(stderr, "%s: size %u below minimum %d\n",
+ __func__, req->size, COREDUMP_REQ_SIZE_VER0);
return false;
- if ((req->mask & required_mask) != required_mask)
- return false;
- if (req->mask & ~required_mask)
+ }
+ if (req->mask != TEST_REQ_MASK_ALL) {
+ fprintf(stderr, "%s: mask 0x%llx, expected 0x%llx\n",
+ __func__, (unsigned long long)req->mask,
+ (unsigned long long)TEST_REQ_MASK_ALL);
return false;
+ }
return true;
}
diff --git a/tools/testing/selftests/coredump/coredump_test_helpers.h b/tools/testing/selftests/coredump/coredump_test_helpers.h
new file mode 100644
index 000000000000..97ad5cfeae92
--- /dev/null
+++ b/tools/testing/selftests/coredump/coredump_test_helpers.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __COREDUMP_TEST_HELPERS_H
+#define __COREDUMP_TEST_HELPERS_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include <linux/coredump.h>
+
+#include "../pidfd/pidfd.h"
+
+#ifndef PAGE_SIZE
+#define PAGE_SIZE 4096
+#endif
+
+#define NUM_THREAD_SPAWN 128
+
+/* Size of the mostly unpopulated mapping the sparse coredump test maps. */
+#define SPARSE_MAPPING_SIZE (256 * 1024 * 1024)
+
+/* A task mapping at least this much is worth a record stream. */
+#define SPARSE_STREAM_THRESHOLD (SPARSE_MAPPING_SIZE / 2)
+
+/* Shared helper function declarations */
+void *do_nothing(void *arg);
+void crashing_child(void);
+void crashing_child_sparse(size_t size);
+ssize_t recv_coredump_records(int fd_coredump, int fd_core_file,
+ off_t *coredump_size, bool *truncated,
+ int fd_peer_pidfd);
+ssize_t recv_coredump_compact(int fd_coredump, int fd_object, int fd_reference,
+ off_t *coredump_size);
+ssize_t recv_coredump_bytes(int fd_coredump, int fd_core_file);
+ssize_t peer_vm_size(int fd_peer_pidfd);
+bool is_elf_core(int fd);
+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);
+int get_peer_pidfd(int fd);
+bool get_pidfd_info(int fd_peer_pidfd, struct pidfd_info *info);
+
+/* Protocol helper function declarations */
+ssize_t recv_marker(int fd);
+bool read_marker(int fd, enum coredump_mark mark);
+bool read_coredump_req(int fd, struct coredump_req *req);
+bool send_coredump_ack(int fd, const struct coredump_req *req,
+ __u64 mask, size_t size_ack);
+bool check_coredump_req(const struct coredump_req *req);
+int open_coredump_tmpfile(int fd_tmpfs_detached);
+void process_coredump_worker(int fd_coredump, int fd_peer_pidfd, int fd_core_file);
+
+#endif /* __COREDUMP_TEST_HELPERS_H */