| commit 440b7f8653e4ed8f6e1425145208050b795e9a6c |
| Author: Florian Weimer <fweimer@redhat.com> |
| Date: Thu Oct 31 18:25:39 2019 +0100 |
| |
| Avoid late failure in dlopen in global scope update [BZ #25112] |
| |
| The call to add_to_global in dl_open_worker happens after running ELF |
| constructors for new objects. At this point, proper recovery from |
| malloc failure would be quite complicated: We would have to run the |
| ELF destructors and close all opened objects, something that we |
| currently do not do. |
| |
| Instead, this change splits add_to_global into two phases, |
| add_to_global_resize (which can raise an exception, called before ELF |
| constructors run), and add_to_global_update (which cannot, called |
| after ELF constructors). A complication arises due to recursive |
| dlopen: After the inner dlopen consumes some space, the pre-allocation |
| in the outer dlopen may no longer be sufficient. A new member in the |
| namespace structure, _ns_global_scope_pending_adds keeps track of the |
| maximum number of objects that need to be added to the global scope. |
| This enables the inner add_to_global_resize call to take into account |
| the needs of an outer dlopen. |
| |
| Most code in the dynamic linker assumes that the number of global |
| scope entries fits into an unsigned int (matching the r_nlist member |
| of struct r_scop_elem). Therefore, change the type of |
| _ns_global_scope_alloc to unsigned int (from size_t), and add overflow |
| checks. |
| |
| Change-Id: Ie08e2f318510d5a6a4bcb1c315f46791b5b77524 |
| |
| diff --git a/elf/dl-open.c b/elf/dl-open.c |
| index c9c0254ee74c4f4b..85db4f0ecb5f29ce 100644 |
| |
| |
| @@ -50,22 +50,38 @@ struct dl_open_args |
| struct link_map *map; |
| /* Namespace ID. */ |
| Lmid_t nsid; |
| + |
| + /* Original value of _ns_global_scope_pending_adds. Set by |
| + dl_open_worker. Only valid if nsid is a real namespace |
| + (non-negative). */ |
| + unsigned int original_global_scope_pending_adds; |
| + |
| /* Original parameters to the program and the current environment. */ |
| int argc; |
| char **argv; |
| char **env; |
| }; |
| |
| +/* Called in case the global scope cannot be extended. */ |
| +static void __attribute__ ((noreturn)) |
| +add_to_global_resize_failure (struct link_map *new) |
| +{ |
| + _dl_signal_error (ENOMEM, new->l_libname->name, NULL, |
| + N_ ("cannot extend global scope")); |
| +} |
| |
| -static int |
| -add_to_global (struct link_map *new) |
| +/* Grow the global scope array for the namespace, so that all the new |
| + global objects can be added later in add_to_global_update, without |
| + risk of memory allocation failure. add_to_global_resize raises |
| + exceptions for memory allocation errors. */ |
| +static void |
| +add_to_global_resize (struct link_map *new) |
| { |
| - struct link_map **new_global; |
| - unsigned int to_add = 0; |
| - unsigned int cnt; |
| + struct link_namespaces *ns = &GL (dl_ns)[new->l_ns]; |
| |
| /* Count the objects we have to put in the global scope. */ |
| - for (cnt = 0; cnt < new->l_searchlist.r_nlist; ++cnt) |
| + unsigned int to_add = 0; |
| + for (unsigned int cnt = 0; cnt < new->l_searchlist.r_nlist; ++cnt) |
| if (new->l_searchlist.r_list[cnt]->l_global == 0) |
| ++to_add; |
| |
| @@ -83,47 +99,51 @@ add_to_global (struct link_map *new) |
| in an realloc() call. Therefore we allocate a completely new |
| array the first time we have to add something to the locale scope. */ |
| |
| - struct link_namespaces *ns = &GL(dl_ns)[new->l_ns]; |
| + if (__builtin_add_overflow (ns->_ns_global_scope_pending_adds, to_add, |
| + &ns->_ns_global_scope_pending_adds)) |
| + add_to_global_resize_failure (new); |
| + |
| + unsigned int new_size = 0; /* 0 means no new allocation. */ |
| + void *old_global = NULL; /* Old allocation if free-able. */ |
| + |
| + /* Minimum required element count for resizing. Adjusted below for |
| + an exponential resizing policy. */ |
| + size_t required_new_size; |
| + if (__builtin_add_overflow (ns->_ns_main_searchlist->r_nlist, |
| + ns->_ns_global_scope_pending_adds, |
| + &required_new_size)) |
| + add_to_global_resize_failure (new); |
| + |
| if (ns->_ns_global_scope_alloc == 0) |
| { |
| - /* This is the first dynamic object given global scope. */ |
| - ns->_ns_global_scope_alloc |
| - = ns->_ns_main_searchlist->r_nlist + to_add + 8; |
| - new_global = (struct link_map **) |
| - malloc (ns->_ns_global_scope_alloc * sizeof (struct link_map *)); |
| - if (new_global == NULL) |
| - { |
| - ns->_ns_global_scope_alloc = 0; |
| - nomem: |
| - _dl_signal_error (ENOMEM, new->l_libname->name, NULL, |
| - N_("cannot extend global scope")); |
| - return 1; |
| - } |
| + if (__builtin_add_overflow (required_new_size, 8, &new_size)) |
| + add_to_global_resize_failure (new); |
| + } |
| + else if (required_new_size > ns->_ns_global_scope_alloc) |
| + { |
| + if (__builtin_mul_overflow (required_new_size, 2, &new_size)) |
| + add_to_global_resize_failure (new); |
| |
| - /* Copy over the old entries. */ |
| - ns->_ns_main_searchlist->r_list |
| - = memcpy (new_global, ns->_ns_main_searchlist->r_list, |
| - (ns->_ns_main_searchlist->r_nlist |
| - * sizeof (struct link_map *))); |
| + /* The old array was allocated with our malloc, not the minimal |
| + malloc. */ |
| + old_global = ns->_ns_main_searchlist->r_list; |
| } |
| - else if (ns->_ns_main_searchlist->r_nlist + to_add |
| - > ns->_ns_global_scope_alloc) |
| + |
| + if (new_size > 0) |
| { |
| - /* We have to extend the existing array of link maps in the |
| - main map. */ |
| - struct link_map **old_global |
| - = GL(dl_ns)[new->l_ns]._ns_main_searchlist->r_list; |
| - size_t new_nalloc = ((ns->_ns_global_scope_alloc + to_add) * 2); |
| - |
| - new_global = (struct link_map **) |
| - malloc (new_nalloc * sizeof (struct link_map *)); |
| + size_t allocation_size; |
| + if (__builtin_mul_overflow (new_size, sizeof (struct link_map *), |
| + &allocation_size)) |
| + add_to_global_resize_failure (new); |
| + struct link_map **new_global = malloc (allocation_size); |
| if (new_global == NULL) |
| - goto nomem; |
| + add_to_global_resize_failure (new); |
| |
| - memcpy (new_global, old_global, |
| - ns->_ns_global_scope_alloc * sizeof (struct link_map *)); |
| + /* Copy over the old entries. */ |
| + memcpy (new_global, ns->_ns_main_searchlist->r_list, |
| + ns->_ns_main_searchlist->r_nlist * sizeof (struct link_map *)); |
| |
| - ns->_ns_global_scope_alloc = new_nalloc; |
| + ns->_ns_global_scope_alloc = new_size; |
| ns->_ns_main_searchlist->r_list = new_global; |
| |
| if (!RTLD_SINGLE_THREAD_P) |
| @@ -131,16 +151,28 @@ add_to_global (struct link_map *new) |
| |
| free (old_global); |
| } |
| +} |
| + |
| +/* Actually add the new global objects to the global scope. Must be |
| + called after add_to_global_resize. This function cannot fail. */ |
| +static void |
| +add_to_global_update (struct link_map *new) |
| +{ |
| + struct link_namespaces *ns = &GL (dl_ns)[new->l_ns]; |
| |
| /* Now add the new entries. */ |
| unsigned int new_nlist = ns->_ns_main_searchlist->r_nlist; |
| - for (cnt = 0; cnt < new->l_searchlist.r_nlist; ++cnt) |
| + for (unsigned int cnt = 0; cnt < new->l_searchlist.r_nlist; ++cnt) |
| { |
| struct link_map *map = new->l_searchlist.r_list[cnt]; |
| |
| if (map->l_global == 0) |
| { |
| map->l_global = 1; |
| + |
| + /* The array has been resized by add_to_global_resize. */ |
| + assert (new_nlist < ns->_ns_global_scope_alloc); |
| + |
| ns->_ns_main_searchlist->r_list[new_nlist++] = map; |
| |
| /* We modify the global scope. Report this. */ |
| @@ -149,10 +181,15 @@ add_to_global (struct link_map *new) |
| map->l_name, map->l_ns); |
| } |
| } |
| + |
| + /* Some of the pending adds have been performed by the loop above. |
| + Adjust the counter accordingly. */ |
| + unsigned int added = new_nlist - ns->_ns_main_searchlist->r_nlist; |
| + assert (added <= ns->_ns_global_scope_pending_adds); |
| + ns->_ns_global_scope_pending_adds -= added; |
| + |
| atomic_write_barrier (); |
| ns->_ns_main_searchlist->r_nlist = new_nlist; |
| - |
| - return 0; |
| } |
| |
| /* Search link maps in all namespaces for the DSO that contains the object at |
| @@ -225,6 +262,10 @@ dl_open_worker (void *a) |
| args->nsid = call_map->l_ns; |
| } |
| |
| + /* Retain the old value, so that it can be restored. */ |
| + args->original_global_scope_pending_adds |
| + = GL (dl_ns)[args->nsid]._ns_global_scope_pending_adds; |
| + |
| /* One might be tempted to assert that we are RT_CONSISTENT at this point, but that |
| may not be true if this is a recursive call to dlopen. */ |
| _dl_debug_initialize (0, args->nsid); |
| @@ -266,7 +307,10 @@ dl_open_worker (void *a) |
| /* If the user requested the object to be in the global namespace |
| but it is not so far, add it now. */ |
| if ((mode & RTLD_GLOBAL) && new->l_global == 0) |
| - (void) add_to_global (new); |
| + { |
| + add_to_global_resize (new); |
| + add_to_global_update (new); |
| + } |
| |
| assert (_dl_debug_initialize (0, args->nsid)->r_state == RT_CONSISTENT); |
| |
| @@ -523,6 +567,11 @@ TLS generation counter wrapped! Please report this.")); |
| DL_STATIC_INIT (new); |
| #endif |
| |
| + /* Perform the necessary allocations for adding new global objects |
| + to the global scope below, via add_to_global_update. */ |
| + if (mode & RTLD_GLOBAL) |
| + add_to_global_resize (new); |
| + |
| /* Run the initializer functions of new objects. Temporarily |
| disable the exception handler, so that lazy binding failures are |
| fatal. */ |
| @@ -539,10 +588,7 @@ TLS generation counter wrapped! Please report this.")); |
| |
| /* Now we can make the new map available in the global scope. */ |
| if (mode & RTLD_GLOBAL) |
| - /* Move the object in the global namespace. */ |
| - if (add_to_global (new) != 0) |
| - /* It failed. */ |
| - return; |
| + add_to_global_update (new); |
| |
| #ifndef SHARED |
| /* We must be the static _dl_open in libc.a. A static program that |
| @@ -556,7 +602,6 @@ TLS generation counter wrapped! Please report this.")); |
| new->l_name, new->l_ns, new->l_direct_opencount); |
| } |
| |
| - |
| void * |
| _dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid, |
| int argc, char *argv[], char *env[]) |
| @@ -624,6 +669,19 @@ no more namespaces available for dlmopen()")); |
| _dl_unload_cache (); |
| #endif |
| |
| + /* Do this for both the error and success cases. The old value has |
| + only been determined if the namespace ID was assigned (i.e., it |
| + is not __LM_ID_CALLER). In the success case, we actually may |
| + have consumed more pending adds than planned (because the local |
| + scopes overlap in case of a recursive dlopen, the inner dlopen |
| + doing some of the globalization work of the outer dlopen), so the |
| + old pending adds value is larger than absolutely necessary. |
| + Since it is just a conservative upper bound, this is harmless. |
| + The top-level dlopen call will restore the field to zero. */ |
| + if (args.nsid >= 0) |
| + GL (dl_ns)[args.nsid]._ns_global_scope_pending_adds |
| + = args.original_global_scope_pending_adds; |
| + |
| /* See if an error occurred during loading. */ |
| if (__glibc_unlikely (exception.errstring != NULL)) |
| { |
| diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h |
| index 6c5298a80bff8e96..57fbefea3cb841e9 100644 |
| |
| |
| @@ -311,7 +311,14 @@ struct rtld_global |
| /* This is zero at program start to signal that the global scope map is |
| allocated by rtld. Later it keeps the size of the map. It might be |
| reset if in _dl_close if the last global object is removed. */ |
| - size_t _ns_global_scope_alloc; |
| + unsigned int _ns_global_scope_alloc; |
| + |
| + /* During dlopen, this is the number of objects that still need to |
| + be added to the global scope map. It has to be taken into |
| + account when resizing the map, for future map additions after |
| + recursive dlopen calls from ELF constructors. */ |
| + unsigned int _ns_global_scope_pending_adds; |
| + |
| /* Search table for unique objects. */ |
| struct unique_sym_table |
| { |