Crashes of libffi when using W^X memory and forks

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view

Crashes of libffi when using W^X memory and forks

Armin Rigo
Hi all,

Libffi supports "W^X" memory, which is the name of a security-
related operating system feature where a process cannot have
both writeable and executable memory.  (I don't want to discuss the
pros and cons of it, let's just accept that operating system tend to
move in that direction nowadays.)

However, that support doesn't really work in the presence of fork(),
and it can't be easily fixed to.  The problem is the following.  When
we create a callback, ffi_closure_alloc() alloctes memory in
MAP_SHARED mmap pages, in such a way that the same memory is visible
twice: once at some address as writeable memory, and once at some
different address as executable memory.  (There is a custom 5000-lines
implementation of dlmalloc() in the source of libffi just to allocate
small fixed-size pieces out of these pages.)  The problem is that this
approach fails if the main program calls fork() and both parent and
child continue to use the same memory---still shared between them, so
now visible in four places instead of two.

Small examples as easy to come by: it is enough that both parent and
child call any of the ffi_closure_alloc() or ffi_closure_free()
functions.  The crash occurs typically because dlmalloc gets confused
by the other process' dlmalloc (unsynchronized writes to shared
memory).  It can also occur because one process calls
ffi_closure_free() then allocates a fresh new closure at the same
place; when the other process tries to use the old callback, it sees
suddenly different bytes in it.

This is not only a theoretical problem.  Some time ago I opened summarizing the situation for
CPython's libffi-based ctypes module.  The issue contains references
to other places.

If fixing this situation the easy way appears impossible, we would
like to fix it the hard way.  That means changing the internals of
libffi, so that it stops relying on MAP_SHARED memory.  Something
similar is already done for the ARM platform; see
FFI_EXEC_TRAMPOLINE_TABLE.  The more general idea is to allocate a few
pages of memory, fill the first page with precomputed executable
trampolines, and turn that first page into read-only executable
memory.  Each trampoline references data that will be stored in the
other pages, but isn't so far.  Every time ffi_closure_alloc() is
called, we grab one of these trampolines.  Every time
ffi_prep_closure{_loc}() is called, we fill the corresponding data;
but the code is already there and read-only.  This makes the extra
argument to ffi_prep_closure_loc() unneeded again, but we can
certainly support it for backward compatibility.  Moreover,
ffi_prep_closure() can check that the pointer was really obtained with
ffi_closure_alloc(), and if not it must still fill in the code (and
hope that then the OS is not running W^X).

This concerns most platforms, not just one.  It can probably be done
incrementally, though.  We could deprecate "closures.c" but keep it
here for transition, and write a new "closures2.c" which would be used
instead of "closures.c".  This would remove the need for
selinux_enabled_check() and dlmalloc.c.  (The different
is_emutramp_enabled() probably needs to stay.)

Does this sound like a plan?

A bientôt,