When binfmt_misc dispatches a binary to an interpreter, the interpreter becomes bprm->file and begin_new_exec() labels mm->exe_file with it. For wine or qemu-user that is the point. For the transparent mode it defeats the point. The interpreter is an implementation detail and the process's identity is the binary. Relocatable programs that locate themselves via /proc/self/exe find the dynamic linker instead [1]. Userspace cannot repair this after the fact. The exe link is the one capability-gated field of PR_SET_MM_MAP, deliberately, because an uncapped exe swap lets any ptrace-capable process masquerade as an arbitrary executable. bprm->executable is the file execve() access-checked and kept open for AT_EXECFD. It is already the file would_dump() bases the dumpability decision on and the file bprm->execfd_creds derives credentials from. Label mm->exe_file with it when the dispatch is transparent and the identity is correct from the start. The label names precisely the file the caller passed to execve(). Write-denial moves along with the label. The result is exact parity with a direct execution. A concurrently written binary fails execve() with -ETXTBSY at open and a running one cannot be opened for writing. The interpreter consequently is not exe-pinned and matches the role it has in a native PT_INTERP exec. Nothing sets BINPRM_FLAGS_TRANSPARENT_INTERP yet; the transparent dispatch machinery in binfmt_misc follows and raises it from birth, so the label and the aux vector bit that announces it appear together. Link: https://inbox.sourceware.org/libc-alpha/87ik6fymha.fsf@oldenburg.str.redhat.com [1] Signed-off-by: Christian Brauner (Amutable) --- fs/exec.c | 33 ++++++++++++++++++++++++++++----- include/linux/binfmts.h | 4 +++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/fs/exec.c b/fs/exec.c index 6e540f4b43d8..45d416994682 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1101,6 +1101,17 @@ void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec) perf_event_comm(tsk, exec); } +/* + * The file the process presents as: its exe link and comm. A transparent + * dispatch presents as the binary, which is bprm->executable. + */ +static struct file *bprm_identity_file(const struct linux_binprm *bprm) +{ + if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP) + return bprm->executable; + return bprm->file; +} + /* * Calling this is the point of no return. None of the failures will be * seen by userspace since either the process is already taking a fatal @@ -1157,7 +1168,7 @@ int begin_new_exec(struct linux_binprm * bprm) * not visible until then. Doing it here also ensures * we don't race against replace_mm_exe_file(). */ - retval = set_mm_exe_file(bprm->mm, bprm->file); + retval = set_mm_exe_file(bprm->mm, bprm_identity_file(bprm)); if (retval) goto out; @@ -1247,6 +1258,8 @@ int begin_new_exec(struct linux_binprm * bprm) * Let's fix it up to be something reasonable. */ if (bprm->comm_from_dentry) { + struct file *comm_file = bprm_identity_file(bprm); + /* * Hold RCU lock to keep the name from being freed behind our back. * Use acquire semantics to make sure the terminating NUL from @@ -1256,7 +1269,7 @@ int begin_new_exec(struct linux_binprm * bprm) * detecting a concurrent rename and just want a terminated name. */ rcu_read_lock(); - __set_task_comm(me, smp_load_acquire(&bprm->file->f_path.dentry->d_name.name), + __set_task_comm(me, smp_load_acquire(&comm_file->f_path.dentry->d_name.name), true); rcu_read_unlock(); } else { @@ -1300,6 +1313,9 @@ int begin_new_exec(struct linux_binprm * bprm) retval = FD_ADD(0, bprm->executable); if (retval < 0) goto out_unlock; + /* mm->exe_file carries its own write denial now. */ + if (bprm->executable_denied) + exe_file_allow_write_access(bprm->executable); bprm->executable = NULL; bprm->execfd = retval; } @@ -1419,8 +1435,12 @@ static void free_bprm(struct linux_binprm *bprm) if (bprm->old_mm) exec_mm_put_old(bprm->old_mm); do_close_execat(bprm->file); - if (bprm->executable) + if (bprm->executable) { + /* A transparent dispatch still holds the write denial. */ + if (bprm->executable_denied) + exe_file_allow_write_access(bprm->executable); fput(bprm->executable); + } /* If a binfmt changed the interp, free it. */ if (bprm->interp != bprm->filename) kfree(bprm->interp); @@ -1746,8 +1766,11 @@ static int exec_binprm(struct linux_binprm *bprm) do_close_execat(exec); return -ENOEXEC; } - /* Only the reference is kept, for AT_EXECFD. */ - exe_file_allow_write_access(exec); + /* A transparent dispatch keeps the denial for mm->exe_file. */ + if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP) + bprm->executable_denied = 1; + else + exe_file_allow_write_access(exec); bprm->executable = exec; } else { do_close_execat(exec); diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index d4f64d143d1a..ba5037b69866 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -59,7 +59,9 @@ struct linux_binprm { * Set by user space to check executability according to the * caller's environment. */ - is_check:1; + is_check:1, + /* bprm->executable holds a write denial still to be released. */ + executable_denied:1; struct file *executable; /* Executable to pass to the interpreter */ struct file *interpreter; struct file *file; -- 2.53.0