From: David Hildenbrand <hidden> Date: 2021-08-12 08:44:22
This series is based on v5.14-rc5 and corresponds code-wise to the
previously sent RFC [1] (the RFC still applied cleanly).
This series removes all in-tree usage of MAP_DENYWRITE from the kernel
and removes VM_DENYWRITE. We stopped supporting MAP_DENYWRITE for
user space applications a while ago because of the chance for DoS.
The last renaming user is binfmt binary loading during exec and
legacy library loading via uselib().
With this change, MAP_DENYWRITE is effectively ignored throughout the
kernel. Although the net change is small, I think the cleanup in mmap()
is quite nice.
There are some (minor) user-visible changes with this series:
1. We no longer deny write access to shared libaries loaded via legacy
uselib(); this behavior matches modern user space e.g., via dlopen().
2. We no longer deny write access to the elf interpreter after exec
completed, treating it just like shared libraries (which it often is).
3. We always deny write access to the file linked via /proc/pid/exe:
sys_prctl(PR_SET_MM_EXE_FILE) will fail if write access to the file
cannot be denied, and write access to the file will remain denied
until the link is effectivel gone (exec, termination,
PR_SET_MM_EXE_FILE) -- just as if exec'ing the file.
I was wondering if we really care about permanently disabling write access
to the executable, or if it would be good enough to just disable write
access while loading the new executable during exec; but I don't know
the history of that -- and it somewhat makes sense to deny write access
at least to the main executable. With modern user space -- dlopen() -- we
can effectively modify the content of shared libraries while being used.
There is a related problem [2] with overlayfs, that should at least partly
be tackled by this series. I don't quite understand the interaction of
overlayfs and deny_write_access()/allow_write_access() at exec time:
If we end up denying write access to the wrong file and not to the
realfile, that would be fundamentally broken. We would have to reroute
our deny_write_access()/ allow_write_access() calls for the exec file to
the realfile -- but I leave figuring out the details to overlayfs guys, as
that would be a related but different issue.
RFC -> v1:
- "binfmt: remove in-tree usage of MAP_DENYWRITE"
-- Add a note that this should fix part of a problem with overlayfs
[1] https://lore.kernel.org/r/20210423131640.20080-1-david@redhat.com/
[2] https://lore.kernel.org/r/YNHXzBgzRrZu1MrD@miu.piliscsaba.redhat.com/
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Thomas Gleixner <redacted>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Borislav Petkov <bp@alien8.de>
Cc: "H. Peter Anvin" <hpa@zytor.com>
Cc: Alexander Viro <viro@zeniv.linux.org.uk>
Cc: Alexey Dobriyan <redacted>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Arnaldo Carvalho de Melo <acme@kernel.org>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: Alexander Shishkin <alexander.shishkin@linux.intel.com>
Cc: Jiri Olsa <redacted>
Cc: Namhyung Kim <namhyung@kernel.org>
Cc: Petr Mladek <pmladek@suse.com>
Cc: Sergey Senozhatsky <redacted>
Cc: Andy Shevchenko <andriy.shevchenko@linux.intel.com>
Cc: Rasmus Villemoes <linux@rasmusvillemoes.dk>
Cc: Kees Cook <redacted>
Cc: "Eric W. Biederman" <redacted>
Cc: Greg Ungerer <gerg@linux-m68k.org>
Cc: Geert Uytterhoeven <geert@linux-m68k.org>
Cc: Mike Rapoport <rppt@kernel.org>
Cc: Vlastimil Babka <redacted>
Cc: Vincenzo Frascino <vincenzo.frascino@arm.com>
Cc: Chinwen Chang <redacted>
Cc: Michel Lespinasse <redacted>
Cc: Catalin Marinas <catalin.marinas@arm.com>
Cc: "Matthew Wilcox (Oracle)" <willy@infradead.org>
Cc: Huang Ying <redacted>
Cc: Jann Horn <jannh@google.com>
Cc: Feng Tang <redacted>
Cc: Kevin Brodsky <redacted>
Cc: Michael Ellerman <mpe@ellerman.id.au>
Cc: Shawn Anastasio <redacted>
Cc: Steven Price <steven.price@arm.com>
Cc: Nicholas Piggin <npiggin@gmail.com>
Cc: Christian Brauner <redacted>
Cc: Jens Axboe <axboe@kernel.dk>
Cc: Gabriel Krisman Bertazi <redacted>
Cc: Peter Xu <peterx@redhat.com>
Cc: Suren Baghdasaryan <surenb@google.com>
Cc: Shakeel Butt <redacted>
Cc: Marco Elver <elver@google.com>
Cc: Daniel Jordan <daniel.m.jordan@oracle.com>
Cc: Nicolas Viennot <redacted>
Cc: Thomas Cedeno <redacted>
Cc: Collin Fijalkovich <redacted>
Cc: Michal Hocko <mhocko@suse.com>
Cc: Miklos Szeredi <miklos@szeredi.hu>
Cc: Chengguang Xu <redacted>
Cc: "Christian König" <ckoenig.leichtzumerken@gmail.com>
Cc: linux-unionfs@vger.kernel.org
Cc: linux-api@vger.kernel.org
Cc: x86@kernel.org
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-mm@kvack.org
David Hildenbrand (7):
binfmt: don't use MAP_DENYWRITE when loading shared libraries via
uselib()
kernel/fork: factor out atomcially replacing the current MM exe_file
kernel/fork: always deny write access to current MM exe_file
binfmt: remove in-tree usage of MAP_DENYWRITE
mm: remove VM_DENYWRITE
mm: ignore MAP_DENYWRITE in ksys_mmap_pgoff()
fs: update documentation of get_write_access() and friends
arch/x86/ia32/ia32_aout.c | 8 ++--
fs/binfmt_aout.c | 7 ++--
fs/binfmt_elf.c | 6 +--
fs/binfmt_elf_fdpic.c | 2 +-
fs/proc/task_mmu.c | 1 -
include/linux/fs.h | 19 +++++----
include/linux/mm.h | 3 +-
include/linux/mman.h | 4 +-
include/trace/events/mmflags.h | 1 -
kernel/events/core.c | 2 -
kernel/fork.c | 75 ++++++++++++++++++++++++++++++----
kernel/sys.c | 33 +--------------
lib/test_printf.c | 5 +--
mm/mmap.c | 29 ++-----------
mm/nommu.c | 2 -
15 files changed, 98 insertions(+), 99 deletions(-)
base-commit: 36a21d51725af2ce0700c6ebcb6b9594aac658a6
--
2.31.1
From: David Hildenbrand <hidden> Date: 2021-08-12 08:44:40
uselib() is the legacy systemcall for loading shared libraries.
Nowadays, applications use dlopen() to load shared libraries, completely
implemented in user space via mmap().
For example, glibc uses MAP_COPY to mmap shared libraries. While this
maps to MAP_PRIVATE | MAP_DENYWRITE on Linux, Linux ignores any
MAP_DENYWRITE specification from user space in mmap.
With this change, all remaining in-tree users of MAP_DENYWRITE use it
to map an executable. We will be able to open shared libraries loaded
via uselib() writable, just as we already can via dlopen() from user
space.
This is one step into the direction of removing MAP_DENYWRITE from the
kernel. This can be considered a minor user space visible change.
Signed-off-by: David Hildenbrand <redacted>
---
arch/x86/ia32/ia32_aout.c | 2 +-
fs/binfmt_aout.c | 2 +-
fs/binfmt_elf.c | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
@@ -293,7 +293,7 @@ static int load_aout_library(struct file *file)/* Now use mmap to map the library into memory. */error=vm_mmap(file,start_addr,ex.a_text+ex.a_data,PROT_READ|PROT_WRITE|PROT_EXEC,-MAP_FIXED|MAP_PRIVATE|MAP_DENYWRITE|MAP_32BIT,+MAP_FIXED|MAP_PRIVATE|MAP_32BIT,N_TXTOFF(ex));retval=error;if(error!=start_addr)
@@ -309,7 +309,7 @@ static int load_aout_library(struct file *file)/* Now use mmap to map the library into memory. */error=vm_mmap(file,start_addr,ex.a_text+ex.a_data,PROT_READ|PROT_WRITE|PROT_EXEC,-MAP_FIXED|MAP_PRIVATE|MAP_DENYWRITE,+MAP_FIXED|MAP_PRIVATE;N_TXTOFF(ex));retval=error;if(error!=start_addr)
From: David Hildenbrand <hidden> Date: 2021-08-12 08:44:59
Let's factor the main logic out into atomic_set_mm_exe_file(), such that
all mm->exe_file logic is contained in kernel/fork.c.
While at it, perform some simple cleanups that are possible now that
we're simplifying the individual functions.
Signed-off-by: David Hildenbrand <redacted>
---
include/linux/mm.h | 2 ++
kernel/fork.c | 35 +++++++++++++++++++++++++++++++++--
kernel/sys.c | 33 +--------------------------------
3 files changed, 36 insertions(+), 34 deletions(-)
@@ -1170,6 +1170,37 @@ void set_mm_exe_file(struct mm_struct *mm, struct file *new_exe_file)fput(old_exe_file);}+intatomic_set_mm_exe_file(structmm_struct*mm,structfile*new_exe_file)+{+structvm_area_struct*vma;+structfile*old_exe_file;+intret=0;++/* Forbid mm->exe_file change if old file still mapped. */+old_exe_file=get_mm_exe_file(mm);+if(old_exe_file){+mmap_read_lock(mm);+for(vma=mm->mmap;vma&&!ret;vma=vma->vm_next){+if(!vma->vm_file)+continue;+if(path_equal(&vma->vm_file->f_path,+&old_exe_file->f_path))+ret=-EBUSY;+}+mmap_read_unlock(mm);+fput(old_exe_file);+if(ret)+returnret;+}++/* set the new file, lockless */+get_file(new_exe_file);+old_exe_file=xchg(&mm->exe_file,new_exe_file);+if(old_exe_file)+fput(old_exe_file);+return0;+}+/***get_mm_exe_file-acquireareferencetothemm'sexecutablefile*
@@ -1869,40 +1868,10 @@ static int prctl_set_mm_exe_file(struct mm_struct *mm, unsigned int fd)if(err)gotoexit;-/*-*Forbidmm->exe_filechangeifoldfilestillmapped.-*/-exe_file=get_mm_exe_file(mm);-err=-EBUSY;-if(exe_file){-structvm_area_struct*vma;--mmap_read_lock(mm);-for(vma=mm->mmap;vma;vma=vma->vm_next){-if(!vma->vm_file)-continue;-if(path_equal(&vma->vm_file->f_path,-&exe_file->f_path))-gotoexit_err;-}--mmap_read_unlock(mm);-fput(exe_file);-}--err=0;-/* set the new file, lockless */-get_file(exe.file);-old_exe=xchg(&mm->exe_file,exe.file);-if(old_exe)-fput(old_exe);+err=atomic_set_mm_exe_file(mm,exe.file);exit:fdput(exe);returnerr;-exit_err:-mmap_read_unlock(mm);-fput(exe_file);-gotoexit;}/*
From: David Hildenbrand <hidden> Date: 2021-08-12 08:45:19
We want to remove VM_DENYWRITE only currently only used when mapping the
executable during exec. During exec, we already deny_write_access() the
executable, however, after exec completes the VMAs mapped
with VM_DENYWRITE effectively keeps write access denied via
deny_write_access().
Let's deny write access when setting the MM exe_file. With this change, we
can remove VM_DENYWRITE for mapping executables.
This represents a minor user space visible change:
sys_prctl(PR_SET_MM_EXE_FILE) can now fail if the file is already
opened writable. Also, after sys_prctl(PR_SET_MM_EXE_FILE), the file
cannot be opened writable. Note that we can already fail with -EACCES if
the file doesn't have execute permissions.
Signed-off-by: David Hildenbrand <redacted>
---
kernel/fork.c | 39 ++++++++++++++++++++++++++++++++++-----
1 file changed, 34 insertions(+), 5 deletions(-)
@@ -476,6 +476,7 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,{structvm_area_struct*mpnt,*tmp,*prev,**pprev;structrb_node**rb_link,*rb_parent;+structfile*exe_file;intretval;unsignedlongcharge;LIST_HEAD(uf);
@@ -493,7 +494,10 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,mmap_write_lock_nested(mm,SINGLE_DEPTH_NESTING);/* No ordering required: file already has been exposed. */-RCU_INIT_POINTER(mm->exe_file,get_mm_exe_file(oldmm));+exe_file=get_mm_exe_file(oldmm);+RCU_INIT_POINTER(mm->exe_file,exe_file);+if(exe_file)+deny_write_access(exe_file);mm->total_vm=oldmm->total_vm;mm->data_vm=oldmm->data_vm;
@@ -1194,10 +1211,22 @@ int atomic_set_mm_exe_file(struct mm_struct *mm, struct file *new_exe_file)}/* set the new file, lockless */+ret=deny_write_access(new_exe_file);+if(ret)+return-EACCES;get_file(new_exe_file);+old_exe_file=xchg(&mm->exe_file,new_exe_file);-if(old_exe_file)+if(old_exe_file){+/*+*Don'tracewithdup_mmap()gettingthefileanddisallowing+*writeaccesswhilesomeonemightopenthefilewritable.+*/+mmap_read_lock(mm);+allow_write_access(old_exe_file);fput(old_exe_file);+mmap_read_unlock(mm);+}return0;}
From: David Hildenbrand <hidden> Date: 2021-08-12 08:45:41
At exec time when we mmap the new executable via MAP_DENYWRITE we have it
opened via do_open_execat() and already deny_write_access()'ed the file
successfully. Once exec completes, we allow_write_acces(); however,
we set mm->exe_file in begin_new_exec() via set_mm_exe_file() and
also deny_write_access() as long as mm->exe_file remains set. We'll
effectively deny write access to our executable via mm->exe_file
until mm->exe_file is changed -- when the process is removed, on new
exec, or via sys_prctl(PR_SET_MM_EXE_FILE).
Let's remove all usage of MAP_DENYWRITE, it's no longer necessary for
mm->exe_file.
In case of an elf interpreter, we'll now only deny write access to the file
during exec. This is somewhat okay, because the interpreter behaves
(and sometime is) a shared library; all shared libraries, especially the
ones loaded directly in user space like via dlopen() won't ever be mapped
via MAP_DENYWRITE, because we ignore that from user space completely;
these shared libraries can always be modified while mapped and executed.
Let's only special-case the main executable, denying write access while
being executed by a process. This can be considered a minor user space
visible change.
While this is a cleanup, it also fixes part of a problem reported with
VM_DENYWRITE on overlayfs, as VM_DENYWRITE is effectively unused with
this patch and will be removed next:
"Overlayfs did not honor positive i_writecount on realfile for
VM_DENYWRITE mappings." [1]
[1] https://lore.kernel.org/r/YNHXzBgzRrZu1MrD@miu.piliscsaba.redhat.com/
Reported-by: Chengguang Xu <redacted>
Signed-off-by: David Hildenbrand <redacted>
---
arch/x86/ia32/ia32_aout.c | 6 ++----
fs/binfmt_aout.c | 5 ++---
fs/binfmt_elf.c | 4 ++--
fs/binfmt_elf_fdpic.c | 2 +-
4 files changed, 7 insertions(+), 10 deletions(-)
@@ -1788,22 +1784,12 @@ unsigned long mmap_region(struct file *file, unsigned long addr,vma->vm_pgoff=pgoff;if(file){-if(vm_flags&VM_DENYWRITE){-error=deny_write_access(file);-if(error)-gotofree_vma;-}if(vm_flags&VM_SHARED){error=mapping_map_writable(file->f_mapping);if(error)-gotoallow_write_and_free_vma;+gotofree_vma;}-/* ->mmap() can change vma->vm_file, but must guarantee that-*vma_link()belowcandenywrite-accessifVM_DENYWRITEisset-*andmapwritablyifVM_SHAREDisset.Thisusuallymeansthe-*newfilemustnothavebeenexposedtouser-space,yet.-*/vma->vm_file=get_file(file);error=call_mmap(file,vma);if(error)
@@ -1860,13 +1846,9 @@ unsigned long mmap_region(struct file *file, unsigned long addr,vma_link(mm,vma,prev,rb_link,rb_parent);/* Once vma denies write, undo our temporary denial count */-if(file){unmap_writable:-if(vm_flags&VM_SHARED)-mapping_unmap_writable(file->f_mapping);-if(vm_flags&VM_DENYWRITE)-allow_write_access(file);-}+if(file&&vm_flags&VM_SHARED)+mapping_unmap_writable(file->f_mapping);file=vma->vm_file;out:perf_event_mmap(vma);
@@ -1906,9 +1888,6 @@ unsigned long mmap_region(struct file *file, unsigned long addr,charged=0;if(vm_flags&VM_SHARED)mapping_unmap_writable(file->f_mapping);-allow_write_and_free_vma:-if(vm_flags&VM_DENYWRITE)-allow_write_access(file);free_vma:vm_area_free(vma);unacct_error:
From: David Hildenbrand <hidden> Date: 2021-08-12 08:46:19
Let's also remove masking off MAP_DENYWROTE from ksys_mmap_pgoff():
the last in-tree occurrence of MAP_DENYWRITE is now in LEGACY_MAP_MASK,
which accepts the flag e.g., for MAP_SHARED_VALIDATE; however, the flag
is ignored throughout the kernel now.
Add a comment to LEGACY_MAP_MASK stating that MAP_DENYWRITE is ignored.
Signed-off-by: David Hildenbrand <redacted>
---
include/linux/mman.h | 3 ++-
mm/mmap.c | 2 --
mm/nommu.c | 2 --
3 files changed, 2 insertions(+), 5 deletions(-)
@@ -1626,8 +1626,6 @@ unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,returnPTR_ERR(file);}-flags&=~MAP_DENYWRITE;-retval=vm_mmap_pgoff(file,addr,len,prot,flags,pgoff);out_fput:if(file)
@@ -1296,8 +1296,6 @@ unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,gotoout;}-flags&=~MAP_DENYWRITE;-retval=vm_mmap_pgoff(file,addr,len,prot,flags,pgoff);if(file)
From: David Hildenbrand <hidden> Date: 2021-08-12 08:46:39
As VM_DENYWRITE does no longer exists, let's spring-clean the
documentation of get_write_access() and friends.
Signed-off-by: David Hildenbrand <redacted>
---
include/linux/fs.h | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)
From: Christian Brauner <hidden> Date: 2021-08-12 09:18:00
On Thu, Aug 12, 2021 at 10:43:43AM +0200, David Hildenbrand wrote:
Let's factor the main logic out into atomic_set_mm_exe_file(), such that
all mm->exe_file logic is contained in kernel/fork.c.
While at it, perform some simple cleanups that are possible now that
we're simplifying the individual functions.
Signed-off-by: David Hildenbrand <redacted>
---
Looks good.
Acked-by: Christian Brauner <redacted>
@@ -1170,6 +1170,37 @@ void set_mm_exe_file(struct mm_struct *mm, struct file *new_exe_file)fput(old_exe_file);}+intatomic_set_mm_exe_file(structmm_struct*mm,structfile*new_exe_file)+{+structvm_area_struct*vma;+structfile*old_exe_file;+intret=0;++/* Forbid mm->exe_file change if old file still mapped. */+old_exe_file=get_mm_exe_file(mm);+if(old_exe_file){+mmap_read_lock(mm);+for(vma=mm->mmap;vma&&!ret;vma=vma->vm_next){+if(!vma->vm_file)+continue;+if(path_equal(&vma->vm_file->f_path,+&old_exe_file->f_path))+ret=-EBUSY;+}+mmap_read_unlock(mm);+fput(old_exe_file);+if(ret)+returnret;+}++/* set the new file, lockless */+get_file(new_exe_file);+old_exe_file=xchg(&mm->exe_file,new_exe_file);+if(old_exe_file)+fput(old_exe_file);+return0;+}+/***get_mm_exe_file-acquireareferencetothemm'sexecutablefile*
@@ -1869,40 +1868,10 @@ static int prctl_set_mm_exe_file(struct mm_struct *mm, unsigned int fd)if(err)gotoexit;-/*-*Forbidmm->exe_filechangeifoldfilestillmapped.-*/-exe_file=get_mm_exe_file(mm);-err=-EBUSY;-if(exe_file){-structvm_area_struct*vma;--mmap_read_lock(mm);-for(vma=mm->mmap;vma;vma=vma->vm_next){-if(!vma->vm_file)-continue;-if(path_equal(&vma->vm_file->f_path,-&exe_file->f_path))-gotoexit_err;-}--mmap_read_unlock(mm);-fput(exe_file);-}--err=0;-/* set the new file, lockless */-get_file(exe.file);-old_exe=xchg(&mm->exe_file,exe.file);-if(old_exe)-fput(old_exe);+err=atomic_set_mm_exe_file(mm,exe.file);exit:fdput(exe);returnerr;-exit_err:-mmap_read_unlock(mm);-fput(exe_file);-gotoexit;}/*
From: Christian Brauner <hidden> Date: 2021-08-12 10:06:02
[+Cc Andrei]
On Thu, Aug 12, 2021 at 10:43:44AM +0200, David Hildenbrand wrote:
We want to remove VM_DENYWRITE only currently only used when mapping the
executable during exec. During exec, we already deny_write_access() the
executable, however, after exec completes the VMAs mapped
with VM_DENYWRITE effectively keeps write access denied via
deny_write_access().
Let's deny write access when setting the MM exe_file. With this change, we
can remove VM_DENYWRITE for mapping executables.
This represents a minor user space visible change:
sys_prctl(PR_SET_MM_EXE_FILE) can now fail if the file is already
opened writable. Also, after sys_prctl(PR_SET_MM_EXE_FILE), the file
Just for completeness, this also affects PR_SET_MM_MAP when exe_fd is
set.
cannot be opened writable. Note that we can already fail with -EACCES if
the file doesn't have execute permissions.
Signed-off-by: David Hildenbrand <redacted>
---
The biggest user I know and that I'm involved in is CRIU which heavily
uses PR_SET_MM_MAP (with a fallback to PR_SET_MM_EXE_FILE on older
kernels) during restore. Afair, criu opens the exe fd as an O_PATH
during dump and thus will use the same flag during restore when
opening it. So that should be fine.
However, if I understand the consequences of this change correctly, a
problem could be restoring workloads that hold a writable fd open to
their exe file at dump time which would mean that during restore that fd
would be reopened writable causing CRIU to fail when setting the exe
file for the task to be restored.
Which honestly, no idea how many such workloads exist. (I know at least
of runC and LXC need to sometimes reopen to rexec themselves (weird bug
to protect against attacking the exe file) and thus re-open
/proc/self/exe but read-only.)
@@ -476,6 +476,7 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,{structvm_area_struct*mpnt,*tmp,*prev,**pprev;structrb_node**rb_link,*rb_parent;+structfile*exe_file;intretval;unsignedlongcharge;LIST_HEAD(uf);
@@ -493,7 +494,10 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,mmap_write_lock_nested(mm,SINGLE_DEPTH_NESTING);/* No ordering required: file already has been exposed. */-RCU_INIT_POINTER(mm->exe_file,get_mm_exe_file(oldmm));+exe_file=get_mm_exe_file(oldmm);+RCU_INIT_POINTER(mm->exe_file,exe_file);+if(exe_file)+deny_write_access(exe_file);mm->total_vm=oldmm->total_vm;mm->data_vm=oldmm->data_vm;
@@ -1194,10 +1211,22 @@ int atomic_set_mm_exe_file(struct mm_struct *mm, struct file *new_exe_file)}/* set the new file, lockless */+ret=deny_write_access(new_exe_file);+if(ret)+return-EACCES;get_file(new_exe_file);+old_exe_file=xchg(&mm->exe_file,new_exe_file);-if(old_exe_file)+if(old_exe_file){+/*+*Don'tracewithdup_mmap()gettingthefileanddisallowing+*writeaccesswhilesomeonemightopenthefilewritable.+*/+mmap_read_lock(mm);+allow_write_access(old_exe_file);fput(old_exe_file);+mmap_read_unlock(mm);+}return0;}
From: David Hildenbrand <hidden> Date: 2021-08-12 10:13:56
On 12.08.21 12:05, Christian Brauner wrote:
[+Cc Andrei]
On Thu, Aug 12, 2021 at 10:43:44AM +0200, David Hildenbrand wrote:
quoted
We want to remove VM_DENYWRITE only currently only used when mapping the
executable during exec. During exec, we already deny_write_access() the
executable, however, after exec completes the VMAs mapped
with VM_DENYWRITE effectively keeps write access denied via
deny_write_access().
Let's deny write access when setting the MM exe_file. With this change, we
can remove VM_DENYWRITE for mapping executables.
This represents a minor user space visible change:
sys_prctl(PR_SET_MM_EXE_FILE) can now fail if the file is already
opened writable. Also, after sys_prctl(PR_SET_MM_EXE_FILE), the file
Just for completeness, this also affects PR_SET_MM_MAP when exe_fd is
set.
Correct.
quoted
cannot be opened writable. Note that we can already fail with -EACCES if
the file doesn't have execute permissions.
Signed-off-by: David Hildenbrand <redacted>
---
The biggest user I know and that I'm involved in is CRIU which heavily
uses PR_SET_MM_MAP (with a fallback to PR_SET_MM_EXE_FILE on older
kernels) during restore. Afair, criu opens the exe fd as an O_PATH
during dump and thus will use the same flag during restore when
opening it. So that should be fine.
Yes.
However, if I understand the consequences of this change correctly, a
problem could be restoring workloads that hold a writable fd open to
their exe file at dump time which would mean that during restore that fd
would be reopened writable causing CRIU to fail when setting the exe
file for the task to be restored.
If it's their exe file, then the existing VM_DENYWRITE handling would
have forbidden these workloads to open the fd of their exe file
writable, right? At least before doing any
PR_SET_MM_MAP/PR_SET_MM_EXE_FILE. But that should rule out quite a lot
of cases we might be worried about, right?
Which honestly, no idea how many such workloads exist. (I know at least
of runC and LXC need to sometimes reopen to rexec themselves (weird bug
to protect against attacking the exe file) and thus re-open
/proc/self/exe but read-only.)
@@ -476,6 +476,7 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,{structvm_area_struct*mpnt,*tmp,*prev,**pprev;structrb_node**rb_link,*rb_parent;+structfile*exe_file;intretval;unsignedlongcharge;LIST_HEAD(uf);
@@ -493,7 +494,10 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,mmap_write_lock_nested(mm,SINGLE_DEPTH_NESTING);/* No ordering required: file already has been exposed. */-RCU_INIT_POINTER(mm->exe_file,get_mm_exe_file(oldmm));+exe_file=get_mm_exe_file(oldmm);+RCU_INIT_POINTER(mm->exe_file,exe_file);+if(exe_file)+deny_write_access(exe_file);mm->total_vm=oldmm->total_vm;mm->data_vm=oldmm->data_vm;
@@ -1194,10 +1211,22 @@ int atomic_set_mm_exe_file(struct mm_struct *mm, struct file *new_exe_file)}/* set the new file, lockless */+ret=deny_write_access(new_exe_file);+if(ret)+return-EACCES;get_file(new_exe_file);+old_exe_file=xchg(&mm->exe_file,new_exe_file);-if(old_exe_file)+if(old_exe_file){+/*+*Don'tracewithdup_mmap()gettingthefileanddisallowing+*writeaccesswhilesomeonemightopenthefilewritable.+*/+mmap_read_lock(mm);+allow_write_access(old_exe_file);fput(old_exe_file);+mmap_read_unlock(mm);+}return0;}
From: Christian Brauner <hidden> Date: 2021-08-12 12:32:58
On Thu, Aug 12, 2021 at 12:13:44PM +0200, David Hildenbrand wrote:
On 12.08.21 12:05, Christian Brauner wrote:
quoted
[+Cc Andrei]
On Thu, Aug 12, 2021 at 10:43:44AM +0200, David Hildenbrand wrote:
quoted
We want to remove VM_DENYWRITE only currently only used when mapping the
executable during exec. During exec, we already deny_write_access() the
executable, however, after exec completes the VMAs mapped
with VM_DENYWRITE effectively keeps write access denied via
deny_write_access().
Let's deny write access when setting the MM exe_file. With this change, we
can remove VM_DENYWRITE for mapping executables.
This represents a minor user space visible change:
sys_prctl(PR_SET_MM_EXE_FILE) can now fail if the file is already
opened writable. Also, after sys_prctl(PR_SET_MM_EXE_FILE), the file
Just for completeness, this also affects PR_SET_MM_MAP when exe_fd is
set.
Correct.
quoted
quoted
cannot be opened writable. Note that we can already fail with -EACCES if
the file doesn't have execute permissions.
Signed-off-by: David Hildenbrand <redacted>
---
The biggest user I know and that I'm involved in is CRIU which heavily
uses PR_SET_MM_MAP (with a fallback to PR_SET_MM_EXE_FILE on older
kernels) during restore. Afair, criu opens the exe fd as an O_PATH
during dump and thus will use the same flag during restore when
opening it. So that should be fine.
Yes.
quoted
However, if I understand the consequences of this change correctly, a
problem could be restoring workloads that hold a writable fd open to
their exe file at dump time which would mean that during restore that fd
would be reopened writable causing CRIU to fail when setting the exe
file for the task to be restored.
If it's their exe file, then the existing VM_DENYWRITE handling would have
forbidden these workloads to open the fd of their exe file writable, right?
Yes.
At least before doing any PR_SET_MM_MAP/PR_SET_MM_EXE_FILE. But that should
rule out quite a lot of cases we might be worried about, right?
Yes, it rules out the most obvious cases. The problem is really just
that we don't know how common weirder cases are. But that doesn't mean
we shouldn't try and risk it. This is a nice cleanup and playing
/proc/self/exe games isn't super common.
quoted
Which honestly, no idea how many such workloads exist. (I know at least
of runC and LXC need to sometimes reopen to rexec themselves (weird bug
to protect against attacking the exe file) and thus re-open
/proc/self/exe but read-only.)
@@ -476,6 +476,7 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,{structvm_area_struct*mpnt,*tmp,*prev,**pprev;structrb_node**rb_link,*rb_parent;+structfile*exe_file;intretval;unsignedlongcharge;LIST_HEAD(uf);
@@ -493,7 +494,10 @@ static __latent_entropy int dup_mmap(struct mm_struct *mm,mmap_write_lock_nested(mm,SINGLE_DEPTH_NESTING);/* No ordering required: file already has been exposed. */-RCU_INIT_POINTER(mm->exe_file,get_mm_exe_file(oldmm));+exe_file=get_mm_exe_file(oldmm);+RCU_INIT_POINTER(mm->exe_file,exe_file);+if(exe_file)+deny_write_access(exe_file);mm->total_vm=oldmm->total_vm;mm->data_vm=oldmm->data_vm;
@@ -1194,10 +1211,22 @@ int atomic_set_mm_exe_file(struct mm_struct *mm, struct file *new_exe_file)}/* set the new file, lockless */+ret=deny_write_access(new_exe_file);+if(ret)+return-EACCES;get_file(new_exe_file);+old_exe_file=xchg(&mm->exe_file,new_exe_file);-if(old_exe_file)+if(old_exe_file){+/*+*Don'tracewithdup_mmap()gettingthefileanddisallowing+*writeaccesswhilesomeonemightopenthefilewritable.+*/+mmap_read_lock(mm);+allow_write_access(old_exe_file);fput(old_exe_file);+mmap_read_unlock(mm);+}return0;}
From: David Hildenbrand <hidden> Date: 2021-08-12 12:39:10
On 12.08.21 14:32, Christian Brauner wrote:
On Thu, Aug 12, 2021 at 12:13:44PM +0200, David Hildenbrand wrote:
quoted
On 12.08.21 12:05, Christian Brauner wrote:
quoted
[+Cc Andrei]
On Thu, Aug 12, 2021 at 10:43:44AM +0200, David Hildenbrand wrote:
quoted
We want to remove VM_DENYWRITE only currently only used when mapping the
executable during exec. During exec, we already deny_write_access() the
executable, however, after exec completes the VMAs mapped
with VM_DENYWRITE effectively keeps write access denied via
deny_write_access().
Let's deny write access when setting the MM exe_file. With this change, we
can remove VM_DENYWRITE for mapping executables.
This represents a minor user space visible change:
sys_prctl(PR_SET_MM_EXE_FILE) can now fail if the file is already
opened writable. Also, after sys_prctl(PR_SET_MM_EXE_FILE), the file
Just for completeness, this also affects PR_SET_MM_MAP when exe_fd is
set.
Correct.
quoted
quoted
cannot be opened writable. Note that we can already fail with -EACCES if
the file doesn't have execute permissions.
Signed-off-by: David Hildenbrand <redacted>
---
The biggest user I know and that I'm involved in is CRIU which heavily
uses PR_SET_MM_MAP (with a fallback to PR_SET_MM_EXE_FILE on older
kernels) during restore. Afair, criu opens the exe fd as an O_PATH
during dump and thus will use the same flag during restore when
opening it. So that should be fine.
Yes.
quoted
However, if I understand the consequences of this change correctly, a
problem could be restoring workloads that hold a writable fd open to
their exe file at dump time which would mean that during restore that fd
would be reopened writable causing CRIU to fail when setting the exe
file for the task to be restored.
If it's their exe file, then the existing VM_DENYWRITE handling would have
forbidden these workloads to open the fd of their exe file writable, right?
Yes.
quoted
At least before doing any PR_SET_MM_MAP/PR_SET_MM_EXE_FILE. But that should
rule out quite a lot of cases we might be worried about, right?
Yes, it rules out the most obvious cases. The problem is really just
that we don't know how common weirder cases are. But that doesn't mean
we shouldn't try and risk it. This is a nice cleanup and playing
/proc/self/exe games isn't super common.
Right, and having the file your executing opened writable isn't
something very common as well.
If we really run into problems, we could not protect the new file when
issuing PR_SET_MM_MAP/PR_SET_MM_EXE_FILE. But I'd like to avoid that, if
possible, because it feels like working around something that never
should have worked that way and is quite inconsistent.
--
Thanks,
David / dhildenb
On Wed, Aug 11, 2021 at 10:45 PM David Hildenbrand [off-list ref] wrote:
/* No ordering required: file already has been exposed. */
- RCU_INIT_POINTER(mm->exe_file, get_mm_exe_file(oldmm));
+ exe_file = get_mm_exe_file(oldmm);
+ RCU_INIT_POINTER(mm->exe_file, exe_file);
+ if (exe_file)
+ deny_write_access(exe_file);
Can we make a helper function for this, since it's done in two different places?
- if (new_exe_file)
+ if (new_exe_file) {
get_file(new_exe_file);
+ /*
+ * exec code is required to deny_write_access() successfully,
+ * so this cannot fail
+ */
+ deny_write_access(new_exe_file);
+ }
rcu_assign_pointer(mm->exe_file, new_exe_file);
And the above looks positively wrong. The comment is also nonsensical,
in that it basically says "we thought this cannot fail, so we'll just
rely on it".
If it truly cannot fail, then the comment should give the reason, not
the "we depend on this not failing".
And honestly, I don't see why it couldn't fail. And if it *does* fail,
we cannot then RCU-assign the exe_file pointer with this, because
you'll get a counter imbalance when you do the allow_write_access()
later.
Anyway, do_open_execat() does do deny_write_access() with proper error
checking. I think that is the existing reference that you depend on -
so that it doesn't fail. So the comment could possibly say that the
only caller has done this, but can we not just use the reference
deny_write_access() directly, and not do a new one here?
IOW, maybe there's an extraneous 'allow_write_access()' somewhere that
should be dropped when we do the whole binprm dance in execve()?
Linus
From: David Hildenbrand <hidden> Date: 2021-08-12 19:39:39
On 12.08.21 18:51, Linus Torvalds wrote:
On Wed, Aug 11, 2021 at 10:45 PM David Hildenbrand [off-list ref] wrote:
quoted
/* No ordering required: file already has been exposed. */
- RCU_INIT_POINTER(mm->exe_file, get_mm_exe_file(oldmm));
+ exe_file = get_mm_exe_file(oldmm);
+ RCU_INIT_POINTER(mm->exe_file, exe_file);
+ if (exe_file)
+ deny_write_access(exe_file);
Can we make a helper function for this, since it's done in two different places?
Sure, no compelling reason not to (except finding a suitable name, but
I'll think about that tomorrow).
quoted
- if (new_exe_file)
+ if (new_exe_file) {
get_file(new_exe_file);
+ /*
+ * exec code is required to deny_write_access() successfully,
+ * so this cannot fail
+ */
+ deny_write_access(new_exe_file);
+ }
rcu_assign_pointer(mm->exe_file, new_exe_file);
And the above looks positively wrong. The comment is also nonsensical,
in that it basically says "we thought this cannot fail, so we'll just
rely on it".
Well, it documents the expectation towards the caller, but in a
suboptimal way, I agree.
If it truly cannot fail, then the comment should give the reason, not
the "we depend on this not failing".
Right, "We depend on the caller already have done a deny_write_access()
successfully first such that this call cannot fail." combined with
if (deny_write_access(new_exe_file))
pr_warn("Unexpected failure of deny_write_access() in %s",
__func__);
suggestions welcome.
And honestly, I don't see why it couldn't fail. And if it *does* fail,
we cannot then RCU-assign the exe_file pointer with this, because
you'll get a counter imbalance when you do the allow_write_access()
later.
Anyone calling set_mm_exe_file() (-> begin_new_exec()) is expected to
successfully triggered a deny_write_access() upfront such that we won't
fail at that point.
Further, on the dup_mmap() path we are sure the previous oldmm exe_file
properly saw a successful deny_write_access() already, because that's
now guaranteed for any exe_file.
Anyway, do_open_execat() does do deny_write_access() with proper error
checking. I think that is the existing reference that you depend on -
so that it doesn't fail. So the comment could possibly say that the
only caller has done this, but can we not just use the reference
deny_write_access() directly, and not do a new one here?
I think that might over-complicate the exec code where we would see a
allow_write_access() on error paths, but not on success paths. This here
looks cleaner to me, agreeing that the comment and the error check has
to be improved.
We handle all allow_write_access()/deny_write_access() regarding
exe_file completely in kernel/fork.c, which is IMHO quite nice.
IOW, maybe there's an extraneous 'allow_write_access()' somewhere that
should be dropped when we do the whole binprm dance in execve()?
fs/exec.c: free_bprm() and exec_binprm() to be precise.
Thanks!
--
Thanks,
David / dhildenb