* [PATCH] binfmt_elf: return the real error when program headers cannot be loaded
@ 2026-10-14 12:30 Konstantin Khorenko
2026-10-15 4:48 ` Kees Cook
0 siblings, 1 reply; 2+ messages in thread
From: Konstantin Khorenko @ 2026-10-14 12:30 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner, Jan Kara, Kees Cook,
Paul Burton
Cc: linux-fsdevel, linux-mm, linux-kernel, Konstantin Khorenko
load_elf_phdrs() returns NULL on any failure, and load_elf_binary()
turns that into -ENOEXEC for the executable and -ELIBBAD for its
interpreter. So a failed allocation or read of the program headers
makes glibc's execvp() run the ELF file with /bin/sh, and reports a
healthy ld.so as a corrupted shared library.
Return an ERR_PTR instead, as binfmt_elf_fdpic does:
-ENOEXEC bad e_phentsize, e_phnum or e_phoff, or the file ends
before the header table
-ENOMEM the allocation fails
read error kernel_read() fails
Malformed files keep getting -ENOEXEC and -ELIBBAD. kernel_read()
returns a short count when a later page fails, so read on until the
error or the end of the file shows. An e_phoff past LLONG_MAX - size
is rejected up front, as rw_verify_area() would turn it into -EINVAL.
Fixes: 6a8d38945cf4 ("binfmt_elf: Hoist ELF program header loading to a function")
Fixes: a9d9ef133f44 ("binfmt_elf: load interpreter program headers earlier")
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Problem
=======
load_elf_phdrs() returns NULL on any failure, and load_elf_binary()
turns that into a fixed error: -ENOEXEC for the executable, -ELIBBAD
for its interpreter. The real cause is lost:
cause really execve() returns
------------------------------------- ---------- ----------------
bad e_phentsize, e_phnum 0 or too big bad file ENOEXEC (right)
file ends before the header table bad file ENOEXEC (right)
kmalloc() of the table fails ENOMEM ENOEXEC (wrong)
kernel_read() fails EIO/ENOMEM ENOEXEC (wrong)
For the interpreter every row gives ELIBBAD.
Consequences
============
ENOEXEC says the file is not an executable, which is a property of the
file, not a transient failure:
- glibc's execvp() and execlp() take ENOEXEC as "not a binary" and run
the ELF file with /bin/sh, which reports a binary it cannot execute
instead of the ENOMEM or EIO.
- search_binary_handler() tries the next binary format instead of
returning the error.
- For the interpreter the caller gets ELIBBAD, "Accessing a corrupted
shared library", about an ld.so that is fine.
- A caller that would retry on ENOMEM or report EIO gives up on the
file instead.
When it happens
===============
- The allocation fails under fault injection. Since commit
8030790477e8 ("binfmt_elf: remove the 4k limitation of program header
size") the table may be up to 64K; above 32K that is an order-4
allocation on 4K-page kernels, which can fail on a fragmented system.
- The read fails with an I/O error, or with -ENOMEM when the page cache
cannot take the pages.
Where it came from
==================
- Until commit 6a8d38945cf4 ("binfmt_elf: Hoist ELF program header
loading to a function") load_elf_binary() returned -ENOMEM and the
read error. That commit documented an ERR_PTR return but returned
NULL; commit cfc46ca4fdca later fixed the comment, not the code.
- Commit a9d9ef133f44 ("binfmt_elf: load interpreter program headers
earlier") gave the interpreter's headers the same NULL check, and with
it -ELIBBAD.
Hence the two Fixes tags.
Testing
=======
With failslab on v7.3-rc7 (CONFIG_FAILSLAB,
CONFIG_FAULT_INJECTION_DEBUG_FS, failslab/ignore-gfp-wait = N): write n
to /proc/thread-self/fail-nth, then execvp("/bin/true"). At the n that
hits the program header allocation of /bin/true, /bin/sh (bash here)
gets the file:
/bin/true: /bin/true: cannot execute binary file (exit 126)
while the allocations around it give "execvp: Cannot allocate memory".
The reproducer below does the same with execve() for n = 1, 2, ...
until 50 execve() calls in a row return no error, on /bin/true
(dynamically linked):
kernel errors returned by execve()
7.3-rc7 ENOMEM 17, ENOEXEC 1, ELIBBAD 1, E2BIG 2
7.3-rc7 + this patch ENOMEM 19, E2BIG 2
- ENOEXEC is the executable's header array, ELIBBAD the interpreter's.
- The two E2BIG come from copy_string_kernel() of the file name, whose
first write to the new stack fails in get_arg_page(); that is a
separate issue, not touched here.
- failslab does not reach header tables above 8K, which go to the page
allocator, nor the read errors, so these rows show only the
allocation half.
- For the read: a copy of /bin/true with its header table moved across
offset 4096, served from a FUSE file with direct_io whose reads stop
at 4096 and fail with EIO past it. execve() returns EIO with the
patch; the old code turns the first, short read into ENOEXEC.
- A truncated header table, and an e_phoff of 2^63 that kernel_read()
would refuse with EINVAL, still give ENOEXEC for the executable and
ELIBBAD for the interpreter.
Reproducer (gcc -O2 -o exec_fail_nth exec_fail_nth.c; run as root):
// SPDX-License-Identifier: GPL-2.0
/*
* Fail the n-th fault injection point (here: slab allocations) of an
* execve() for n = 1, 2, ... until 50 execve() calls in a row return no
* error, and count the errors execve() returns. Needs CONFIG_FAILSLAB and
* CONFIG_FAULT_INJECTION_DEBUG_FS, and failslab set up to fail sleeping
* allocations:
* echo N > /sys/kernel/debug/failslab/ignore-gfp-wait
*
* usage: exec_fail_nth [program] (default /bin/true)
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
char *prog = argc > 1 ? argv[1] : "/bin/true";
char *args[] = { prog, NULL };
int counts[256] = { 0 }, clean = 0, n, e;
for (n = 1; clean < 50 && n < 100000; n++) {
int p[2];
pid_t pid;
if (pipe2(p, O_CLOEXEC))
return 2;
pid = fork();
if (pid < 0)
return 2;
if (!pid) {
char buf[16];
int fd = open("/proc/thread-self/fail-nth", O_WRONLY);
if (fd < 0 || write(fd, buf, sprintf(buf, "%d", n)) < 0)
_exit(3);
execve(prog, args, NULL);
e = errno;
write(p[1], &e, sizeof(e));
_exit(4);
}
close(p[1]);
if (read(p[0], &e, sizeof(e)) == sizeof(e)) {
counts[e & 255]++;
if (e == ENOEXEC || e == ELIBBAD)
printf("n=%d: execve() failed with %s\n", n, strerrorname_np(e));
clean = 0;
} else {
clean++;
}
close(p[0]);
if (waitpid(pid, &e, 0) == pid && WIFEXITED(e) && WEXITSTATUS(e) == 3) {
fprintf(stderr, "cannot write /proc/thread-self/fail-nth\n");
return 2;
}
}
printf("n = 1..%d, execve() errors:", n - 1);
for (e = 0; e < 256; e++)
if (counts[e])
printf(" %s=%d", strerrorname_np(e), counts[e]);
printf("\n");
if (n - 1 == clean) {
fprintf(stderr, "no allocation failed: is failslab/ignore-gfp-wait N?\n");
return 2;
}
return counts[ENOEXEC] || counts[ELIBBAD];
}
---
fs/binfmt_elf.c | 50 ++++++++++++++++++++++++++++++++------------------
1 file changed, 32 insertions(+), 18 deletions(-)
diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index 06d0df1053823..05e03f1c9f3f6 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -512,39 +512,47 @@ static unsigned long maximum_alignment(struct elf_phdr *cmds, int nr)
*
* Loads ELF program headers from the binary file elf_file, which has the ELF
* header pointed to by elf_ex, into a newly allocated array. The caller is
- * responsible for freeing the allocated data. Returns NULL upon failure.
+ * responsible for freeing the allocated data. Returns an ERR_PTR upon failure:
+ * -ENOEXEC for a malformed header or when the read reaches the end of the
+ * file, the allocation or read error otherwise.
*/
static struct elf_phdr *load_elf_phdrs(const struct elfhdr *elf_ex,
struct file *elf_file)
{
- struct elf_phdr *elf_phdata = NULL;
- int retval = -1;
- unsigned int size;
+ struct elf_phdr *elf_phdata;
+ unsigned int size, done;
+ loff_t pos;
+ ssize_t rv;
/*
* If the size of this structure has changed, then punt, since
* we will be doing the wrong thing.
*/
if (elf_ex->e_phentsize != sizeof(struct elf_phdr))
- goto out;
+ return ERR_PTR(-ENOEXEC);
/* Sanity check the number of program headers... */
- /* ...and their total size. */
+ /* ...their total size and their offset. */
size = sizeof(struct elf_phdr) * elf_ex->e_phnum;
- if (size == 0 || size > 65536)
- goto out;
+ if (size == 0 || size > 65536 || elf_ex->e_phoff > LLONG_MAX - size)
+ return ERR_PTR(-ENOEXEC);
elf_phdata = kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
- goto out;
-
- /* Read in the program headers */
- retval = elf_read(elf_file, elf_phdata, size, elf_ex->e_phoff);
+ return ERR_PTR(-ENOMEM);
-out:
- if (retval) {
- kfree(elf_phdata);
- elf_phdata = NULL;
+ /*
+ * Read in the program headers. A read that fails part way returns
+ * what it got, so read on to see the error or the end of the file.
+ */
+ pos = elf_ex->e_phoff;
+ for (done = 0; done < size; done += rv) {
+ rv = kernel_read(elf_file, (char *)elf_phdata + done,
+ size - done, &pos);
+ if (rv <= 0) {
+ kfree(elf_phdata);
+ return ERR_PTR(rv ? rv : -ENOEXEC);
+ }
}
return elf_phdata;
}
@@ -864,8 +872,10 @@ static int load_elf_binary(struct linux_binprm *bprm)
goto out;
elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
- if (!elf_phdata)
+ if (IS_ERR(elf_phdata)) {
+ retval = PTR_ERR(elf_phdata);
goto out;
+ }
elf_ppnt = elf_phdata;
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
@@ -968,8 +978,12 @@ static int load_elf_binary(struct linux_binprm *bprm)
/* Load the interpreter program headers */
interp_elf_phdata = load_elf_phdrs(interp_elf_ex,
interpreter);
- if (!interp_elf_phdata)
+ if (IS_ERR(interp_elf_phdata)) {
+ if (PTR_ERR(interp_elf_phdata) != -ENOEXEC)
+ retval = PTR_ERR(interp_elf_phdata);
+ interp_elf_phdata = NULL;
goto out_free_dentry;
+ }
/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
elf_property_phdata = NULL;
---
base-commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
change-id: 20261014-b4-prep-vstor-149847-binfmt_elf-enomem-load-elf-phdrs-395ffbfc33e2
Best regards,
--
Konstantin Khorenko <khorenko@virtuozzo.com>
^ permalink raw reply related [flat|nested] 2+ messages in thread* Re: [PATCH] binfmt_elf: return the real error when program headers cannot be loaded
2026-10-14 12:30 [PATCH] binfmt_elf: return the real error when program headers cannot be loaded Konstantin Khorenko
@ 2026-10-15 4:48 ` Kees Cook
0 siblings, 0 replies; 2+ messages in thread
From: Kees Cook @ 2026-10-15 4:48 UTC (permalink / raw)
To: Konstantin Khorenko
Cc: Alexander Viro, Christian Brauner, Jan Kara, Paul Burton,
linux-fsdevel, linux-mm, linux-kernel
On Wed, Oct 14, 2026 at 02:30:33PM +0200, Konstantin Khorenko wrote:
> Problem
> =======
>
> load_elf_phdrs() returns NULL on any failure, and load_elf_binary()
> turns that into a fixed error: -ENOEXEC for the executable, -ELIBBAD
> for its interpreter. The real cause is lost:
>
> cause really execve() returns
> ------------------------------------- ---------- ----------------
> bad e_phentsize, e_phnum 0 or too big bad file ENOEXEC (right)
> file ends before the header table bad file ENOEXEC (right)
> kmalloc() of the table fails ENOMEM ENOEXEC (wrong)
> kernel_read() fails EIO/ENOMEM ENOEXEC (wrong)
>
> For the interpreter every row gives ELIBBAD.
>
> Consequences
> ============
>
> ENOEXEC says the file is not an executable, which is a property of the
> file, not a transient failure:
>
> - glibc's execvp() and execlp() take ENOEXEC as "not a binary" and run
> the ELF file with /bin/sh, which reports a binary it cannot execute
> instead of the ENOMEM or EIO.
> - search_binary_handler() tries the next binary format instead of
> returning the error.
Isn't that what we want? If one handler can't load it, move on to find
one that does.
> - For the interpreter the caller gets ELIBBAD, "Accessing a corrupted
> shared library", about an ld.so that is fine.
> - A caller that would retry on ENOMEM or report EIO gives up on the
> file instead.
>
> When it happens
> ===============
>
> - The allocation fails under fault injection. Since commit
> 8030790477e8 ("binfmt_elf: remove the 4k limitation of program header
> size") the table may be up to 64K; above 32K that is an order-4
> allocation on 4K-page kernels, which can fail on a fragmented system.
I would not want to risk changing established userspace error message
interactions to make fault injection happy.
> - The read fails with an I/O error, or with -ENOMEM when the page cache
> cannot take the pages.
This is kind of a bad failure case -- I don't think the "right" error
code is going to help much there. ;)
>
> Where it came from
> ==================
>
> - Until commit 6a8d38945cf4 ("binfmt_elf: Hoist ELF program header
> loading to a function") load_elf_binary() returned -ENOMEM and the
> read error. That commit documented an ERR_PTR return but returned
> NULL; commit cfc46ca4fdca later fixed the comment, not the code.
> - Commit a9d9ef133f44 ("binfmt_elf: load interpreter program headers
> earlier") gave the interpreter's headers the same NULL check, and with
> it -ELIBBAD.
So it's been like this for 12 years? :)
> [...]
> kernel errors returned by execve()
> 7.3-rc7 ENOMEM 17, ENOEXEC 1, ELIBBAD 1, E2BIG 2
> 7.3-rc7 + this patch ENOMEM 19, E2BIG 2
... so there are _fewer_ distinct failure codes with the patch?
> + /*
> + * Read in the program headers. A read that fails part way returns
> + * what it got, so read on to see the error or the end of the file.
> + */
> + pos = elf_ex->e_phoff;
> + for (done = 0; done < size; done += rv) {
> + rv = kernel_read(elf_file, (char *)elf_phdata + done,
> + size - done, &pos);
> + if (rv <= 0) {
> + kfree(elf_phdata);
> + return ERR_PTR(rv ? rv : -ENOEXEC);
> + }
> }
This is a behavioral change mixed into the errno handling change to
support partial reads??
I could be convinced that errnos need to be fixed, but the rationale
here doesn't really make me think it's worth potential disruption to
userspace. I'd need a pretty comprehensive survey of what would change
there and then a clean patch that made zero other behavior changes...
-Kees
--
Kees Cook
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-15 4:48 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14 12:30 [PATCH] binfmt_elf: return the real error when program headers cannot be loaded Konstantin Khorenko
2026-10-15 4:48 ` Kees Cook
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox