From f63b73814f74032c0e5d0a83300e3d864ef905e5 Mon Sep 17 00:00:00 2001 From: Florian Weimer Date: Wed, 13 Nov 2019 15:44:56 +0100 Subject: Remove all loaded objects if dlopen fails, ignoring NODELETE [BZ #20839] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces a “pending NODELETE” state in the link map, which is flipped to the persistent NODELETE state late in dlopen, via activate_nodelete. During initial relocation, symbol binding records pending NODELETE state only. dlclose ignores pending NODELETE state. Taken together, this results that a partially completed dlopen is rolled back completely because new NODELETE mappings are unloaded. Tested on x86_64-linux-gnu and i386-linux-gnu. Change-Id: Ib2a3d86af6f92d75baca65431d74783ee0dbc292 --- elf/Makefile | 15 +++++++-- elf/dl-close.c | 14 ++------ elf/dl-lookup.c | 72 +++++++++++++++++++++++++++++++-------- elf/dl-open.c | 82 +++++++++++++++++++++++++++++++++++++++------ elf/get-dynamic-info.h | 2 ++ elf/tst-dlopenfail.c | 79 +++++++++++++++++++++++++++++++++++++++++++ elf/tst-dlopenfaillinkmod.c | 17 ++++++++++ elf/tst-dlopenfailmod1.c | 36 ++++++++++++++++++++ elf/tst-dlopenfailmod2.c | 29 ++++++++++++++++ 9 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 elf/tst-dlopenfail.c create mode 100644 elf/tst-dlopenfaillinkmod.c create mode 100644 elf/tst-dlopenfailmod1.c create mode 100644 elf/tst-dlopenfailmod2.c (limited to 'elf') diff --git a/elf/Makefile b/elf/Makefile index 30c389657b..305bed2d38 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -199,7 +199,7 @@ tests += restest1 preloadtest loadfail multiload origtest resolvfail \ tst-debug1 tst-main1 tst-absolute-sym tst-absolute-zero tst-big-note \ tst-unwind-ctor tst-unwind-main tst-audit13 \ tst-sonamemove-link tst-sonamemove-dlopen tst-dlopen-tlsmodid \ - tst-dlopen-self tst-auditmany tst-initfinilazyfail + tst-dlopen-self tst-auditmany tst-initfinilazyfail tst-dlopenfail # reldep9 tests-internal += loadtest unload unload2 circleload1 \ neededtest neededtest2 neededtest3 neededtest4 \ @@ -291,7 +291,8 @@ modules-names = testobj1 testobj2 testobj3 testobj4 testobj5 testobj6 \ tst-auditmanymod1 tst-auditmanymod2 tst-auditmanymod3 \ tst-auditmanymod4 tst-auditmanymod5 tst-auditmanymod6 \ tst-auditmanymod7 tst-auditmanymod8 tst-auditmanymod9 \ - tst-initlazyfailmod tst-finilazyfailmod + tst-initlazyfailmod tst-finilazyfailmod \ + tst-dlopenfailmod1 tst-dlopenfaillinkmod tst-dlopenfailmod2 # Most modules build with _ISOMAC defined, but those filtered out # depend on internal headers. modules-names-tests = $(filter-out ifuncmod% tst-libc_dlvsym-dso tst-tlsmod%,\ @@ -1597,3 +1598,13 @@ LDFLAGS-tst-initlazyfailmod.so = \ -Wl,-z,lazy -Wl,--unresolved-symbols=ignore-all LDFLAGS-tst-finilazyfailmod.so = \ -Wl,-z,lazy -Wl,--unresolved-symbols=ignore-all + +$(objpfx)tst-dlopenfail: $(libdl) +$(objpfx)tst-dlopenfail.out: \ + $(objpfx)tst-dlopenfailmod1.so $(objpfx)tst-dlopenfailmod2.so +# Order matters here. tst-dlopenfaillinkmod.so's soname ensures +# a run-time loader failure. +$(objpfx)tst-dlopenfailmod1.so: \ + $(shared-thread-library) $(objpfx)tst-dlopenfaillinkmod.so +LDFLAGS-tst-dlopenfaillinkmod.so = -Wl,-soname,tst-dlopenfail-missingmod.so +$(objpfx)tst-dlopenfailmod2.so: $(shared-thread-library) diff --git a/elf/dl-close.c b/elf/dl-close.c index 33486b9cbc..e35a62daf6 100644 --- a/elf/dl-close.c +++ b/elf/dl-close.c @@ -168,14 +168,6 @@ _dl_close_worker (struct link_map *map, bool force) char done[nloaded]; struct link_map *maps[nloaded]; - /* Clear DF_1_NODELETE to force object deletion. We don't need to touch - l_tls_dtor_count because forced object deletion only happens when an - error occurs during object load. Destructor registration for TLS - non-POD objects should not have happened till then for this - object. */ - if (force) - map->l_flags_1 &= ~DF_1_NODELETE; - /* Run over the list and assign indexes to the link maps and enter them into the MAPS array. */ int idx = 0; @@ -205,7 +197,7 @@ _dl_close_worker (struct link_map *map, bool force) /* Check whether this object is still used. */ if (l->l_type == lt_loaded && l->l_direct_opencount == 0 - && (l->l_flags_1 & DF_1_NODELETE) == 0 + && l->l_nodelete != link_map_nodelete_active /* See CONCURRENCY NOTES in cxa_thread_atexit_impl.c to know why acquire is sufficient and correct. */ && atomic_load_acquire (&l->l_tls_dtor_count) == 0 @@ -288,7 +280,7 @@ _dl_close_worker (struct link_map *map, bool force) if (!used[i]) { assert (imap->l_type == lt_loaded - && (imap->l_flags_1 & DF_1_NODELETE) == 0); + && imap->l_nodelete != link_map_nodelete_active); /* Call its termination function. Do not do it for half-cooked objects. Temporarily disable exception @@ -838,7 +830,7 @@ _dl_close (void *_map) before we took the lock. There is no way to detect this (see below) so we proceed assuming this isn't the case. First see whether we can remove the object at all. */ - if (__glibc_unlikely (map->l_flags_1 & DF_1_NODELETE)) + if (__glibc_unlikely (map->l_nodelete == link_map_nodelete_active)) { /* Nope. Do nothing. */ __rtld_lock_unlock_recursive (GL(dl_load_lock)); diff --git a/elf/dl-lookup.c b/elf/dl-lookup.c index aaaf43799e..a2e85a5568 100644 --- a/elf/dl-lookup.c +++ b/elf/dl-lookup.c @@ -192,9 +192,10 @@ enter_unique_sym (struct unique_sym *table, size_t size, Return the matching symbol in RESULT. */ static void do_lookup_unique (const char *undef_name, uint_fast32_t new_hash, - const struct link_map *map, struct sym_val *result, + struct link_map *map, struct sym_val *result, int type_class, const ElfW(Sym) *sym, const char *strtab, - const ElfW(Sym) *ref, const struct link_map *undef_map) + const ElfW(Sym) *ref, const struct link_map *undef_map, + int flags) { /* We have to determine whether we already found a symbol with this name before. If not then we have to add it to the search table. @@ -222,7 +223,7 @@ do_lookup_unique (const char *undef_name, uint_fast32_t new_hash, copy from the copy addressed through the relocation. */ result->s = sym; - result->m = (struct link_map *) map; + result->m = map; } else { @@ -311,9 +312,19 @@ do_lookup_unique (const char *undef_name, uint_fast32_t new_hash, new_hash, strtab + sym->st_name, sym, map); if (map->l_type == lt_loaded) - /* Make sure we don't unload this object by - setting the appropriate flag. */ - ((struct link_map *) map)->l_flags_1 |= DF_1_NODELETE; + { + /* Make sure we don't unload this object by + setting the appropriate flag. */ + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_BINDINGS) + && map->l_nodelete == link_map_nodelete_inactive) + _dl_debug_printf ("\ +marking %s [%lu] as NODELETE due to unique symbol\n", + map->l_name, map->l_ns); + if (flags & DL_LOOKUP_FOR_RELOCATE) + map->l_nodelete = link_map_nodelete_pending; + else + map->l_nodelete = link_map_nodelete_active; + } } ++tab->n_elements; @@ -525,8 +536,9 @@ do_lookup_x (const char *undef_name, uint_fast32_t new_hash, return 1; case STB_GNU_UNIQUE:; - do_lookup_unique (undef_name, new_hash, map, result, type_class, - sym, strtab, ref, undef_map); + do_lookup_unique (undef_name, new_hash, (struct link_map *) map, + result, type_class, sym, strtab, ref, + undef_map, flags); return 1; default: @@ -568,9 +580,13 @@ add_dependency (struct link_map *undef_map, struct link_map *map, int flags) if (undef_map == map) return 0; - /* Avoid references to objects which cannot be unloaded anyway. */ + /* Avoid references to objects which cannot be unloaded anyway. We + do not need to record dependencies if this object goes away + during dlopen failure, either. IFUNC resolvers with relocation + dependencies may pick an dependency which can be dlclose'd, but + such IFUNC resolvers are undefined anyway. */ assert (map->l_type == lt_loaded); - if ((map->l_flags_1 & DF_1_NODELETE) != 0) + if (map->l_nodelete != link_map_nodelete_inactive) return 0; struct link_map_reldeps *l_reldeps @@ -678,16 +694,33 @@ add_dependency (struct link_map *undef_map, struct link_map *map, int flags) /* Redo the NODELETE check, as when dl_load_lock wasn't held yet this could have changed. */ - if ((map->l_flags_1 & DF_1_NODELETE) != 0) + if (map->l_nodelete != link_map_nodelete_inactive) goto out; /* If the object with the undefined reference cannot be removed ever just make sure the same is true for the object which contains the definition. */ if (undef_map->l_type != lt_loaded - || (undef_map->l_flags_1 & DF_1_NODELETE) != 0) + || (undef_map->l_nodelete != link_map_nodelete_inactive)) { - map->l_flags_1 |= DF_1_NODELETE; + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_BINDINGS) + && map->l_nodelete == link_map_nodelete_inactive) + { + if (undef_map->l_name[0] == '\0') + _dl_debug_printf ("\ +marking %s [%lu] as NODELETE due to reference to main program\n", + map->l_name, map->l_ns); + else + _dl_debug_printf ("\ +marking %s [%lu] as NODELETE due to reference to %s [%lu]\n", + map->l_name, map->l_ns, + undef_map->l_name, undef_map->l_ns); + } + + if (flags & DL_LOOKUP_FOR_RELOCATE) + map->l_nodelete = link_map_nodelete_pending; + else + map->l_nodelete = link_map_nodelete_active; goto out; } @@ -712,7 +745,18 @@ add_dependency (struct link_map *undef_map, struct link_map *map, int flags) no fatal problem. We simply make sure the referenced object cannot be unloaded. This is semantically the correct behavior. */ - map->l_flags_1 |= DF_1_NODELETE; + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_BINDINGS) + && map->l_nodelete == link_map_nodelete_inactive) + _dl_debug_printf ("\ +marking %s [%lu] as NODELETE due to memory allocation failure\n", + map->l_name, map->l_ns); + if (flags & DL_LOOKUP_FOR_RELOCATE) + /* In case of non-lazy binding, we could actually + report the memory allocation error, but for now, we + use the conservative approximation as well. */ + map->l_nodelete = link_map_nodelete_pending; + else + map->l_nodelete = link_map_nodelete_active; goto out; } else diff --git a/elf/dl-open.c b/elf/dl-open.c index 03aaff7c66..7415c095ac 100644 --- a/elf/dl-open.c +++ b/elf/dl-open.c @@ -424,6 +424,40 @@ TLS generation counter wrapped! Please report this.")); } } +/* Mark the objects as NODELETE if required. This is delayed until + after dlopen failure is not possible, so that _dl_close can clean + up objects if necessary. */ +static void +activate_nodelete (struct link_map *new, int mode) +{ + if (mode & RTLD_NODELETE || new->l_nodelete == link_map_nodelete_pending) + { + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_FILES)) + _dl_debug_printf ("activating NODELETE for %s [%lu]\n", + new->l_name, new->l_ns); + new->l_nodelete = link_map_nodelete_active; + } + + for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i) + { + struct link_map *imap = new->l_searchlist.r_list[i]; + if (imap->l_nodelete == link_map_nodelete_pending) + { + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_FILES)) + _dl_debug_printf ("activating NODELETE for %s [%lu]\n", + imap->l_name, imap->l_ns); + + /* Only new objects should have set + link_map_nodelete_pending. Existing objects should not + have gained any new dependencies and therefore cannot + reach NODELETE status. */ + assert (!imap->l_init_called || imap->l_type != lt_loaded); + + imap->l_nodelete = link_map_nodelete_active; + } + } +} + /* struct dl_init_args and call_dl_init are used to call _dl_init with exception handling disabled. */ struct dl_init_args @@ -493,12 +527,6 @@ dl_open_worker (void *a) return; } - /* Mark the object as not deletable if the RTLD_NODELETE flags was passed. - Do this early so that we don't skip marking the object if it was - already loaded. */ - if (__glibc_unlikely (mode & RTLD_NODELETE)) - new->l_flags_1 |= DF_1_NODELETE; - if (__glibc_unlikely (mode & __RTLD_SPROF)) /* This happens only if we load a DSO for 'sprof'. */ return; @@ -514,19 +542,37 @@ dl_open_worker (void *a) _dl_debug_printf ("opening file=%s [%lu]; direct_opencount=%u\n\n", new->l_name, new->l_ns, new->l_direct_opencount); - /* If the user requested the object to be in the global namespace - but it is not so far, add it now. */ + /* If the user requested the object to be in the global + namespace but it is not so far, prepare to add it now. This + can raise an exception to do a malloc failure. */ if ((mode & RTLD_GLOBAL) && new->l_global == 0) + add_to_global_resize (new); + + /* Mark the object as not deletable if the RTLD_NODELETE flags + was passed. */ + if (__glibc_unlikely (mode & RTLD_NODELETE)) { - add_to_global_resize (new); - add_to_global_update (new); + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_FILES) + && new->l_nodelete == link_map_nodelete_inactive) + _dl_debug_printf ("marking %s [%lu] as NODELETE\n", + new->l_name, new->l_ns); + new->l_nodelete = link_map_nodelete_active; } + /* Finalize the addition to the global scope. */ + if ((mode & RTLD_GLOBAL) && new->l_global == 0) + add_to_global_update (new); + assert (_dl_debug_initialize (0, args->nsid)->r_state == RT_CONSISTENT); return; } + /* Schedule NODELETE marking for the directly loaded object if + requested. */ + if (__glibc_unlikely (mode & RTLD_NODELETE)) + new->l_nodelete = link_map_nodelete_pending; + /* Load that object's dependencies. */ _dl_map_object_deps (new, NULL, 0, 0, mode & (__RTLD_DLOPEN | RTLD_DEEPBIND | __RTLD_AUDIT)); @@ -601,6 +647,14 @@ dl_open_worker (void *a) int relocation_in_progress = 0; + /* Perform relocation. This can trigger lazy binding in IFUNC + resolvers. For NODELETE mappings, these dependencies are not + recorded because the flag has not been applied to the newly + loaded objects. This means that upon dlopen failure, these + NODELETE objects can be unloaded despite existing references to + them. However, such relocation dependencies in IFUNC resolvers + are undefined anyway, so this is not a problem. */ + for (unsigned int i = nmaps; i-- > 0; ) { l = maps[i]; @@ -630,7 +684,7 @@ dl_open_worker (void *a) _dl_start_profile (); /* Prevent unloading the object. */ - GL(dl_profile_map)->l_flags_1 |= DF_1_NODELETE; + GL(dl_profile_map)->l_nodelete = link_map_nodelete_active; } } else @@ -661,6 +715,8 @@ dl_open_worker (void *a) All memory allocations for new objects must have happened before. */ + activate_nodelete (new, mode); + /* Second stage after resize_scopes: Actually perform the scope update. After this, dlsym and lazy binding can bind to new objects. */ @@ -820,6 +876,10 @@ no more namespaces available for dlmopen()")); GL(dl_tls_dtv_gaps) = true; _dl_close_worker (args.map, true); + + /* All link_map_nodelete_pending objects should have been + deleted at this point, which is why it is not necessary + to reset the flag here. */ } assert (_dl_debug_initialize (0, args.nsid)->r_state == RT_CONSISTENT); diff --git a/elf/get-dynamic-info.h b/elf/get-dynamic-info.h index af9dd57888..075683d729 100644 --- a/elf/get-dynamic-info.h +++ b/elf/get-dynamic-info.h @@ -163,6 +163,8 @@ elf_get_dynamic_info (struct link_map *l, ElfW(Dyn) *temp) if (info[VERSYMIDX (DT_FLAGS_1)] != NULL) { l->l_flags_1 = info[VERSYMIDX (DT_FLAGS_1)]->d_un.d_val; + if (l->l_flags_1 & DF_1_NODELETE) + l->l_nodelete = link_map_nodelete_pending; /* Only DT_1_SUPPORTED_MASK bits are supported, and we would like to assert this, but we can't. Users have been setting diff --git a/elf/tst-dlopenfail.c b/elf/tst-dlopenfail.c new file mode 100644 index 0000000000..ce3140c899 --- /dev/null +++ b/elf/tst-dlopenfail.c @@ -0,0 +1,79 @@ +/* Test dlopen rollback after failures involving NODELETE objects (bug 20839). + Copyright (C) 2019 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static int +do_test (void) +{ + /* This test uses libpthread as the canonical NODELETE module. If + libpthread is no longer NODELETE because it has been merged into + libc, the test needs to be updated. */ + TEST_VERIFY (dlsym (NULL, "pthread_create") == NULL); + + /* This is expected to fail because of the missing dependency. */ + puts ("info: attempting to load tst-dlopenfailmod1.so"); + TEST_VERIFY (dlopen ("tst-dlopenfailmod1.so", RTLD_LAZY) == NULL); + const char *message = dlerror (); + TEST_COMPARE_STRING (message, + "tst-dlopenfail-missingmod.so:" + " cannot open shared object file:" + " No such file or directory"); + + /* Do not probe for the presence of libpthread at this point because + that might trigger relocation if bug 20839 is present, obscuring + a subsequent crash. */ + + /* This is expected to succeed. */ + puts ("info: loading tst-dlopenfailmod2.so"); + void *handle = xdlopen ("tst-dlopenfailmod2.so", RTLD_NOW); + xdlclose (handle); + + /* libpthread should remain loaded. */ + TEST_VERIFY (dlopen (LIBPTHREAD_SO, RTLD_LAZY | RTLD_NOLOAD) != NULL); + TEST_VERIFY (dlsym (NULL, "pthread_create") == NULL); + + /* We can make libpthread global, and then the symbol should become + available. */ + TEST_VERIFY (dlopen (LIBPTHREAD_SO, RTLD_LAZY | RTLD_GLOBAL) != NULL); + TEST_VERIFY (dlsym (NULL, "pthread_create") != NULL); + + /* sem_open is sufficiently complex to depend on relocations. */ + void *(*sem_open_ptr) (const char *, int flag, ...) + = dlsym (NULL, "sem_open"); + if (sem_open_ptr == NULL) + /* Hurd does not implement sem_open. */ + puts ("warning: sem_open not found, further testing not possible"); + else + { + errno = 0; + TEST_VERIFY (sem_open_ptr ("/", 0) == NULL); + TEST_COMPARE (errno, EINVAL); + } + + return 0; +} + +#include diff --git a/elf/tst-dlopenfaillinkmod.c b/elf/tst-dlopenfaillinkmod.c new file mode 100644 index 0000000000..3b14b02bc9 --- /dev/null +++ b/elf/tst-dlopenfaillinkmod.c @@ -0,0 +1,17 @@ +/* Empty module with a soname which is not available at run time. + Copyright (C) 2019 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ diff --git a/elf/tst-dlopenfailmod1.c b/elf/tst-dlopenfailmod1.c new file mode 100644 index 0000000000..6ef4882989 --- /dev/null +++ b/elf/tst-dlopenfailmod1.c @@ -0,0 +1,36 @@ +/* Module which depends on two modules: one NODELETE, one missing. + Copyright (C) 2019 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +/* Note: Due to the missing second module, this object cannot be + loaded at run time. */ + +#include +#include +#include + +/* Force linking against libpthread. */ +void *pthread_create_reference = pthread_create; + +/* The constructor will never be executed because the module cannot be + loaded. */ +static void __attribute__ ((constructor)) +init (void) +{ + puts ("tst-dlopenfailmod1 constructor executed"); + _exit (1); +} diff --git a/elf/tst-dlopenfailmod2.c b/elf/tst-dlopenfailmod2.c new file mode 100644 index 0000000000..7d600386c1 --- /dev/null +++ b/elf/tst-dlopenfailmod2.c @@ -0,0 +1,29 @@ +/* Module which depends on on a NODELETE module, and can be loaded. + Copyright (C) 2019 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#include +#include + +/* Force linking against libpthread. */ +void *pthread_create_reference = pthread_create; + +static void __attribute__ ((constructor)) +init (void) +{ + puts ("info: tst-dlopenfailmod2.so constructor invoked"); +} -- cgit 1.4.1