[v10,0/12] implement dlmem() function

Message ID 20230403090421.560208-1-stsp2@yandex.ru
Headers
Series implement dlmem() function |

Message

stsp April 3, 2023, 9:04 a.m. UTC
  This patch-set implements the dlmem() function that allows to load
the solib from the file-mapped or anonymously-shared memory buffer.
Generic memory buffer is also supported with DLMEM_GENBUF_SRC flag,
but it should not be preferred. Private anonymous mapping used as
a source buffer, also needs this flag to be set.

dlmem() suits as a building block for implementing the functions like
fdlopen() and dlopen_with_offset(), which are demo-implemented in a
test-case called tst-dlmem-extfns.
The reasons why it suits well for such file-based loaders, are below:
1. It correctly handles the file association of the original solib
   buffer if it was mmap'ed from a file (unless DLMEM_GENBUF_SRC
   of DLMEM_DONTREPLACE flag is set).
2. It allows to provide a solib name, which can be the file name.

With the above properties, the "direct" implementation of these functions
gives no advantages over implementing them with dlmem().

In addition, dlmem() has lots of optional functionality for the fine-grained
control over the loading process. It allows you to set nsid (like dlmopen()),
specify the solib relocation address and even relocate the solib into
the user's buffer. That "advanced" functionality is only needed for the
very specific use-cases, like virtualized environments where the relocation
address may have a special constraints, eg MAP_32BIT. In all other cases
it is advised to set the "dlm_args" pointer of dlmem() call to NULL, but
see "Limitations" below to find out when its not the case.

The API looks as below:

/* Callback for dlmem. */
typedef void *
(dlmem_premap_t) (void *mappref, size_t maplength, size_t mapalign,
	          void *cookie);

/* Do not replace mapping created by premap callback.
   dlmem() will then use memcpy(). */
#define DLMEM_DONTREPLACE 1
/* Treat source memory buffer as a generic unaligned buffer, rather
   than a file-backed or anonymously-shared mapping. Anonymous private
   mapping also needs this flag to be set. */
#define DLMEM_GENBUF_SRC 2

struct dlmem_args {
  /* Optional name to associate with the loaded object. */
  const char *soname;
  /* Namespace where to load the object. */
  Lmid_t nsid;
  /* dlmem-specific flags. */
  unsigned int flags;
  /* Optional premap callback. */
  dlmem_premap_t *premap;
  /* Optional argument for premap callback. */
  void *cookie;
};

/* Like `dlmopen', but loads shared object from memory buffer.  */
extern void *dlmem (const unsigned char *buffer, size_t size, int mode,
		    struct dlmem_args *dlm_args);

Advanced functionality:

In most cases dlm_args should just be set to NULL. It provides the
advanced functionality, most of which is obvious (soname, nsid).
The optional premap callback allows to set the relocation address for
the solib by mapping the destination space and returning its address.
More so, if DLMEM_DONTREPLACE flag is used, then the mapping
established by the premap callback, will not be replaced with the
file-backed mapping. In that case dlmem() have to use memcpy(), which
is likely even faster than mmaps() but doesn't end up with the proper
/proc/self/map_files or /proc/self/maps entries. So for example if the
premap callback uses MAP_SHARED, then with the use of the DLMEM_DONTREPLACE
flag you can get your solib relocated into a shared memory buffer.
Note that the premap callback may be called under glibc locks, so it
should restrict itself to the syscall functionality. Certainly no libdl
functions can be used. mmap(), shm_open(), open(), ftruncate(),
memfd_create() should be the sufficient set of functions that may
ever be in use in a premap callback, but in most cases the callback
should just be disabled.


Limitations:

- If you need to load the solib from anonymously-mapped buffer, you need
  to use MAP_SHARED|MAP_ANONYMOUS mmap flags when creating that buffer.
  If it is not possible in your use-case and the buffer was created
  with MAP_PRIVATE|MAP_ANONYMOUS flags, then DLMEM_GENBUF_SRC flag
  needs to be set when calling dlmem().
  Failure to follow that guide-line results in an UB (loader will not
  be able to properly lay out an elf segments).

- If you use a private file-backed mapping, then it shouldn't be
  modified by hands before passing to dlmem(). I.e. you can't apply
  mprotect() to it to change protection bits, and you can't apply
  memmove() to it to move the solib to the beginning of the buffer,
  and so on. dlmem() can only work with "virgin" private file-backed
  mappings. You can set DLMEM_GENBUF_SRC flag as a work-around if
  the mapping is already corrupted.
  Failure to follow that guide-line results in an UB (loader will not
  be able to properly lay out an elf segments).

- The need of mapping the entire solib (with debug info etc) may
  represent a problem on a 32bit architectures if the solib has an
  absurdly large size, like 3Gb or more.

- For the very same reason the efficient implementation of Android's
  dlopen_with_offset() is difficult, as in that case you'd need to
  map the entire file container, starting from the needed offset.
  The demo implementation in this patch implements dlopen_with_offset4()
  that has an additional "length" argument where the solib length
  should be passed.

- As linux doesn't implement MAP_UNALIGNED as some unixes did, the
  efficient implementation of dlopen_with_offset4() is difficult
  if the offset is not page-aligned. Demo in this example fixes the
  alignment by hands, using the MAP_SHARED|MAP_ANONYMOUS intermediate
  buffer. The alignment cannot be fixed in an existing buffer with
  memmove(), as that will make the file-backed mapping unacceptable
  for the use with dlmem(). I suspect that google's dlopen_with_offset()
  has similar limitation because mmap() with unaligned offset is
  not possible in any implementation, be it a "direct" implementation
  or "over-dlmem" implementation.


Changes in v10:
- addressed review comments of Adhemerval Zanella
- moved refactor patches to the beginning of the serie to simplify review
- fixed a few bugs in an elf relocation machinery after various hot discussions
- added a new test tst-dlmem-extfns that demo-implements dlopen_with_offset4()
  and fdlopen()
- studied and documented all limitations, most importantly those leading to UB
- better documented premap callback as suggested by Szabolcs Nagy
- added DLMEM_GENBUF_SRC flag for unaligned generic memory buffers

Changes in v9:
- use "zero-copy" machinery instead of memcpy(). It works on linux 5.13
  and newer, falling back to memcpy() otherwise. Suggested by Florian Weimer.
- implement fdlopen() using the above functionality. It is in a new test
  tst-dlmem-fdlopen. Suggested by Carlos O'Donell.
- add DLMEM_DONTREPLACE flag that doesn't replace the backing-store mapping.
  It switches back to memcpy(). Test-case is called tst-dlmem-shm.

Changes in v8:
- drop audit machinery and instead add an extra arg (optional pointer
  to a struct) to dlmem() itself that allows to install a custom premap
  callback or to specify nsid. Audit machinery was meant to allow
  controling over the pre-existing APIs like dlopen(), but if someone
  ever needs such extensions to dlopen(), he can trivially implement
  dlopen() on top of dlmem().

Changes in v7:
- add _dl_audit_premap audit extension and its usage example

Changes in v6:
- use __strdup("") for l_name as suggested by Andreas Schwab

Changes in v5:
- added _dl_audit_premap_dlmem audit extension for dlmem
- added tst-auditmod-dlmem.c test-case that feeds shm fd to dlmem()

Changes in v4:
- re-target to GLIBC_2.38
- add tst-auditdlmem.c test-case to test auditing
- drop length page-aligning in tst-dlmem: mmap() aligns length on its own
- bugfix: in do_mmapcpy() allow mmaps past end of buffer

Changes in v3:
- Changed prototype of dlmem() (and all the internal machinery) to
  use "const unsigned char *buffer" instead of "const char *buffer".

Changes in v2:
- use <support/test-driver.c> instead of "../test-skeleton.c"
- re-target to GLIBC_2.37
- update all libc.abilist files