exec: cleanup search_binary_handler()
Subsystems (2)
v2 cover exec: cleanup search_binary_handler()
To remind, by discussion with Zach
fs-binfmts-add-a-name-field-to-the-binfmt-struct.patch fs-binfmts-better-handling-of-binfmt-loops.patch fs-binfmts-whitespace-fixes-with-scripts-cleanfile.patch
should be dropped. Zach is working on the new version on top of these changes.
Changes:
2/8: add the comment about recursion_depth
4/8: nullify bprm->file to catch the potential use after free
Add the acks from Kees (thanks!)
Oleg.
fs/exec.c | 120 +++++++++++++++++++++++------------------------ include/linux/binfmts.h | 2 +- 2 files changed, 60 insertions(+), 62 deletions(-)
PATCH v2 1/8 exec: introduce exec_binprm() for "depth == 0" code
task_pid_nr_ns() and trace/ptrace code in the middle of the recursive search_binary_handler() looks confusing and imho annoying. We only need this code if "depth == 0", lets add a simple helper which calls search_binary_handler() and does trace_sched_process_exec() + ptrace_event().
The patch also moves the setting of task->did_exec, we need to do this only once.
Note: we can kill either task->did_exec or PF_FORKNOEXEC.
--- fs/exec.c | 36 ++++++++++++++++++++++-------------- 1 files changed, 22 insertions(+), 14 deletions(-)
fs/exec.c +22 -14
@@ -1373,7 +1373,6 @@ int search_binary_handler(struct linux_binprm *bprm)unsigned int depth = bprm->recursion_depth;int try,retval;struct linux_binfmt *fmt; -pid_t old_pid, old_vpid;/* This allows 4 levels of binfmt rewrites before failing hard. */ if (depth >5 )
@@ -1387,12 +1386,6 @@ int search_binary_handler(struct linux_binprm *bprm)if (retval)return retval; -/* Need to fetch pid before load_binary changes it */ - old_pid = current->pid; -rcu_read_lock (); - old_vpid =task_pid_nr_ns (current,task_active_pid_ns (current->parent)); -rcu_read_unlock (); - retval = -ENOENT ;for (try=0 ; try<2 ; try++) {read_lock (&binfmt_lock);
@@ -1407,16 +1400,11 @@ int search_binary_handler(struct linux_binprm *bprm) retval =fn (bprm); bprm->recursion_depth = depth;if (retval >=0 ) { -if (depth ==0 ) { -trace_sched_process_exec (current, old_pid, bprm); -ptrace_event (PTRACE_EVENT_EXEC , old_vpid); - }put_binfmt (fmt);allow_write_access (bprm->file);if (bprm->file)fput (bprm->file); bprm->file =NULL ; - current->did_exec =1 ;proc_exec_connector (current);return retval; }
@@ -1450,9 +1438,29 @@ int search_binary_handler(struct linux_binprm *bprm) }return retval; } -EXPORT_SYMBOL (search_binary_handler); +static int exec_binprm (struct linux_binprm *bprm) +{ +pid_t old_pid, old_vpid; +int ret; + +/* Need to fetch pid before load_binary changes it */ + old_pid = current->pid; +rcu_read_lock (); + old_vpid =task_pid_nr_ns (current,task_active_pid_ns (current->parent)); +rcu_read_unlock (); + + ret =search_binary_handler (bprm); +if (ret >=0 ) { +trace_sched_process_exec (current, old_pid, bprm); +ptrace_event (PTRACE_EVENT_EXEC , old_vpid); + current->did_exec =1 ; + } + +return ret; +} +/* * sys_execve() executes a new program. */
@@ -1541,7 +1549,7 @@ static int do_execve_common(const char *filename,if (retval <0 ) goto out; - retval =search_binary_handler (bprm); + retval =exec_binprm (bprm);if (retval <0 ) goto out;
Signature · 1 line
1.5.5.1
PATCH v2 2/8 exec: kill "int depth" in search_binary_handler()
In reply to Oleg NesterovNobody except search_binary_handler() should touch ->recursion_depth, "int depth" buys nothing but complicates the code, kill it.
Probably we should also kill "fn" and the !NULL check, ->load_binary should be always defined. And it can not go away after read_unlock() or this code is buggy anyway.
--- fs/exec.c | 9 ++++----- include/linux/binfmts.h | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-)
fs/exec.c +4 -5
@@ -1370,12 +1370,11 @@ EXPORT_SYMBOL(remove_arg_zero);*/ int search_binary_handler (struct linux_binprm *bprm) { -unsigned int depth = bprm->recursion_depth; -int try,retval; +int try, retval;struct linux_binfmt *fmt;/* This allows 4 levels of binfmt rewrites before failing hard. */ -if (depth >5 ) +if (bprm->recursion_depth >5 )return -ELOOP ; retval =security_bprm_check (bprm);
@@ -1396,9 +1395,9 @@ int search_binary_handler(struct linux_binprm *bprm)if (!try_module_get (fmt->module))continue ;read_unlock (&binfmt_lock); - bprm->recursion_depth = depth +1 ; + bprm->recursion_depth++; retval =fn (bprm); - bprm->recursion_depth = depth; + bprm->recursion_depth--;if (retval >=0 ) {put_binfmt (fmt);allow_write_access (bprm->file);
include/linux/binfmts.h +1 -1
@@ -31,7 +31,7 @@ struct linux_binprm {#ifdef __alpha__ unsigned int taso:1;#endif -unsigned int recursion_depth; +unsigned int recursion_depth;/* only for search_binary_handler() */ struct file * file;struct cred *cred;/* new credentials */ int unsafe;/* how unsafe this exec is (mask of LSM_UNSAFE_*) */
Signature · 1 line
1.5.5.1
PATCH v2 3/8 exec: proc_exec_connector() should be called only once
In reply to Oleg NesterovA separate one-liner with the minor fix.
PROC_EVENT_EXEC reports the "exec" event, but this message is sent at least twice if search_binary_handler() is called by ->load_binary() recursively, say, load_script().
Move it to exec_binprm(), this is "depth == 0" code too.
--- fs/exec.c | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-)
fs/exec.c +1 -1
@@ -1404,7 +1404,6 @@ int search_binary_handler(struct linux_binprm *bprm)if (bprm->file)fput (bprm->file); bprm->file =NULL ; -proc_exec_connector (current);return retval; }read_lock (&binfmt_lock);
@@ -1455,6 +1454,7 @@ static int exec_binprm(struct linux_binprm *bprm)trace_sched_process_exec (current, old_pid, bprm);ptrace_event (PTRACE_EVENT_EXEC , old_vpid); current->did_exec =1 ; +proc_exec_connector (current); }return ret;
Signature · 1 line
1.5.5.1
PATCH v2 4/8 exec: move allow_write_access/fput to exec_binprm()
In reply to Oleg NesterovWhen search_binary_handler() succeeds it does allow_write_access() and fput(), then it clears bprm->file to ensure the caller will not do the same.
We can simply move this code to exec_binprm() which is called only once. In fact we could move this to free_bprm() and remove the same code in do_execve_common's error path.
--- fs/exec.c | 10 ++++++---- 1 files changed, 6 insertions(+), 4 deletions(-)
fs/exec.c +6 -4
@@ -1400,10 +1400,6 @@ int search_binary_handler(struct linux_binprm *bprm) bprm->recursion_depth--;if (retval >=0 ) {put_binfmt (fmt); -allow_write_access (bprm->file); -if (bprm->file) -fput (bprm->file); - bprm->file =NULL ;return retval; }read_lock (&binfmt_lock);
@@ -1455,6 +1451,12 @@ static int exec_binprm(struct linux_binprm *bprm)ptrace_event (PTRACE_EVENT_EXEC , old_vpid); current->did_exec =1 ;proc_exec_connector (current); + +if (bprm->file) { +allow_write_access (bprm->file); +fput (bprm->file); + bprm->file =NULL ;/* to catch use-after-free */ + } }return ret;
Signature · 1 line
1.5.5.1
PATCH v2 5/8 exec: kill ->load_binary != NULL check in search_binary_handler()
In reply to Oleg Nesterovsearch_binary_handler() checks ->load_binary != NULL for no reason, this method should be always defined. Turn this check into WARN_ON() and move it into __register_binfmt().
Also, kill the function pointer. The current code looks confusing, as if ->load_binary can go away after read_unlock(&binfmt_lock). But we rely on module_get(fmt->module), this fmt can't be changed or unregistered, otherwise this code is buggy anyway.
--- fs/exec.c | 7 +++---- 1 files changed, 3 insertions(+), 4 deletions(-)
fs/exec.c +3 -4
@@ -74,6 +74,8 @@ static DEFINE_RWLOCK(binfmt_lock);void __register_binfmt (struct linux_binfmt * fmt,int insert) {BUG_ON (!fmt); +if (WARN_ON (!fmt->load_binary)) +return ;write_lock (&binfmt_lock); insert ?list_add (&fmt->lh, &formats) :list_add_tail (&fmt->lh, &formats);
@@ -1389,14 +1391,11 @@ int search_binary_handler(struct linux_binprm *bprm)for (try=0 ; try<2 ; try++) {read_lock (&binfmt_lock);list_for_each_entry (fmt, &formats, lh) { -int (*fn)(struct linux_binprm *) = fmt->load_binary; -if (!fn) -continue ;if (!try_module_get (fmt->module))continue ;read_unlock (&binfmt_lock); bprm->recursion_depth++; - retval =fn (bprm); + retval = fmt->load_binary (bprm); bprm->recursion_depth--;if (retval >=0 ) {put_binfmt (fmt);
Signature · 1 line
1.5.5.1
PATCH v2 6/8 exec: cleanup the CONFIG_MODULES logic
In reply to Oleg Nesterovsearch_binary_handler() uses "for (try=0; try<2; try++)" to avoid "goto" but the code looks too complicated and horrible imho. We still need to check "try == 0" before request_module() and add the additional "break" for !CONFIG_MODULES case.
Kill this loop and use a simple "bool need_retry" + "goto retry". The code looks much simpler and we do not even need ifdef's, gcc can optimize out the "if (need_retry)" block if !IS_ENABLED().
--- fs/exec.c | 68 +++++++++++++++++++++++++++--------------------------------- 1 files changed, 31 insertions(+), 37 deletions(-)
fs/exec.c +31 -37
@@ -1367,13 +1367,15 @@ out: }EXPORT_SYMBOL (remove_arg_zero); +#define printable (c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))/* * cycle the list of binary formats handler, until one recognizes the image */ int search_binary_handler (struct linux_binprm *bprm) { -int try, retval; +bool need_retry =IS_ENABLED (CONFIG_MODULES );struct linux_binfmt *fmt; +int retval;/* This allows 4 levels of binfmt rewrites before failing hard. */ if (bprm->recursion_depth >5 )
@@ -1388,47 +1390,39 @@ int search_binary_handler(struct linux_binprm *bprm)return retval; retval = -ENOENT ; -for (try=0 ; try<2 ; try++) { -read_lock (&binfmt_lock); -list_for_each_entry (fmt, &formats, lh) { -if (!try_module_get (fmt->module)) -continue ; -read_unlock (&binfmt_lock); - bprm->recursion_depth++; - retval = fmt->load_binary (bprm); - bprm->recursion_depth--; -if (retval >=0 ) { -put_binfmt (fmt); -return retval; - } -read_lock (&binfmt_lock); + retry: +read_lock (&binfmt_lock); +list_for_each_entry (fmt, &formats, lh) { +if (!try_module_get (fmt->module)) +continue ; +read_unlock (&binfmt_lock); + bprm->recursion_depth++; + retval = fmt->load_binary (bprm); + bprm->recursion_depth--; +if (retval >=0 ) {put_binfmt (fmt); -if (retval != -ENOEXEC || bprm->mm ==NULL ) -break ; -if (!bprm->file) { -read_unlock (&binfmt_lock); -return retval; - } +return retval; } -read_unlock (&binfmt_lock); -#ifdef CONFIG_MODULES -if (retval != -ENOEXEC || bprm->mm ==NULL ) { +read_lock (&binfmt_lock); +put_binfmt (fmt); +if (retval != -ENOEXEC || bprm->mm ==NULL )break ; - }else { -#define printable (c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e)) -if (printable (bprm->buf[0 ]) && -printable (bprm->buf[1 ]) && -printable (bprm->buf[2 ]) && -printable (bprm->buf[3 ])) -break ;/* -ENOEXEC */ -if (try) -break ;/* -ENOEXEC */ -request_module ("binfmt-%04x" , *(unsigned short *)(&bprm->buf[2 ])); +if (!bprm->file) { +read_unlock (&binfmt_lock); +return retval; } -#else -break ; -#endif } +read_unlock (&binfmt_lock); + +if (need_retry && retval == -ENOEXEC && bprm->mm) { +if (printable (bprm->buf[0 ]) &&printable (bprm->buf[1 ]) && +printable (bprm->buf[2 ]) &&printable (bprm->buf[3 ])) +return retval; +request_module ("binfmt-%04x" , *(ushort *)(bprm->buf +2 )); + need_retry = false; + goto retry; + } +return retval; }EXPORT_SYMBOL (search_binary_handler);
Signature · 1 line
1.5.5.1
PATCH v2 7/8 exec: don't retry if request_module() fails
In reply to Oleg NesterovA separate one-liner for better documentation.
It doesn't make sense to retry if request_module() fails to exec /sbin/modprobe, add the additional "request_module() < 0" check.
However, this logic still doesn't look exactly right:
1. It would be better to check "request_module() != 0", the user space modprobe process should report the correct exit code. But I didn't dare to add the user-visible change.
2. The whole ENOEXEC logic looks suboptimal. Suppose that we try to exec a "#!path-to-unsupported-binary" script. In this case request_module() + "retry" will be done twice: first by the "depth == 1" code, and then again by the "depth == 0" caller which doesn't make sense.
3. And note that in the case above bprm->buf was already changed by load_script()->prepare_binprm(), so this looks even more ugly.
--- fs/exec.c | 3 ++- 1 files changed, 2 insertions(+), 1 deletions(-)
fs/exec.c +2 -1
@@ -1418,7 +1418,8 @@ int search_binary_handler(struct linux_binprm *bprm)if (printable (bprm->buf[0 ]) &&printable (bprm->buf[1 ]) &&printable (bprm->buf[2 ]) &&printable (bprm->buf[3 ]))return retval; -request_module ("binfmt-%04x" , *(ushort *)(bprm->buf +2 )); +if (request_module ("binfmt-%04x" , *(ushort *)(bprm->buf +2 )) <0 ) +return retval; need_retry = false; goto retry; }
Signature · 1 line
1.5.5.1
PATCH v2 8/8 exec: cleanup the error handling in search_binary_handler()
In reply to Oleg NesterovThe error hanling and ret-from-loop look confusing and inconsistent.
- "retval >= 0" simply returns
- "!bprm->file" returns too but with read_unlock() because binfmt_lock was already re-acquired
- "retval != -ENOEXEC || bprm->mm == NULL" does "break" and relies on the same check after the main loop
Consolidate these checks into a single if/return statement.
need_retry still checks "retval == -ENOEXEC", but this and -ENOENT before the main loop are not needed. This is only for pathological and impossible list_empty(&formats) case.
It is not clear why do we check "bprm->mm == NULL", probably this should be removed.
--- fs/exec.c | 11 +++-------- 1 files changed, 3 insertions(+), 8 deletions(-)
fs/exec.c +3 -8
@@ -1399,22 +1399,17 @@ int search_binary_handler(struct linux_binprm *bprm) bprm->recursion_depth++; retval = fmt->load_binary (bprm); bprm->recursion_depth--; -if (retval >=0 ) { +if (retval >=0 || retval != -ENOEXEC || + bprm->mm ==NULL || bprm->file ==NULL ) {put_binfmt (fmt);return retval; }read_lock (&binfmt_lock);put_binfmt (fmt); -if (retval != -ENOEXEC || bprm->mm ==NULL ) -break ; -if (!bprm->file) { -read_unlock (&binfmt_lock); -return retval; - } }read_unlock (&binfmt_lock); -if (need_retry && retval == -ENOEXEC && bprm->mm) { +if (need_retry && retval == -ENOEXEC ) {if (printable (bprm->buf[0 ]) &&printable (bprm->buf[1 ]) &&printable (bprm->buf[2 ]) &&printable (bprm->buf[3 ]))return retval;
Signature · 1 line
1.5.5.1