8a8cfb
commit f8ed116aa574435c6e28260f21963233682d3b57
8a8cfb
Author: Florian Weimer <fweimer@redhat.com>
8a8cfb
Date:   Fri Dec 13 10:18:46 2019 +0100
8a8cfb
8a8cfb
    dlopen: Rework handling of pending NODELETE status
8a8cfb
    
8a8cfb
    Commit a2e8aa0d9ea648068d8be52dd7b15f1b6a008e23 ("Block signals during
8a8cfb
    the initial part of dlopen") was deemed necessary because of
8a8cfb
    read-modify-write operations like the one in  add_dependency in
8a8cfb
    elf/dl-lookup.c.  In the old code, we check for any kind of NODELETE
8a8cfb
    status and bail out:
8a8cfb
    
8a8cfb
          /* Redo the NODELETE check, as when dl_load_lock wasn't held
8a8cfb
             yet this could have changed.  */
8a8cfb
          if (map->l_nodelete != link_map_nodelete_inactive)
8a8cfb
            goto out;
8a8cfb
    
8a8cfb
    And then set pending status (during relocation):
8a8cfb
    
8a8cfb
              if (flags & DL_LOOKUP_FOR_RELOCATE)
8a8cfb
                map->l_nodelete = link_map_nodelete_pending;
8a8cfb
              else
8a8cfb
                map->l_nodelete = link_map_nodelete_active;
8a8cfb
    
8a8cfb
    If a signal arrives during relocation and the signal handler, through
8a8cfb
    lazy binding, adds a global scope dependency on the same map, it will
8a8cfb
    set map->l_nodelete to link_map_nodelete_active.  This will be
8a8cfb
    overwritten with link_map_nodelete_pending by the dlopen relocation
8a8cfb
    code.
8a8cfb
    
8a8cfb
    To avoid such problems in relation to the l_nodelete member, this
8a8cfb
    commit introduces two flags for active NODELETE status (irrevocable)
8a8cfb
    and pending NODELETE status (revocable until activate_nodelete is
8a8cfb
    invoked).  As a result, NODELETE processing in dlopen does not
8a8cfb
    introduce further reasons why lazy binding from signal handlers
8a8cfb
    is unsafe during dlopen, and a subsequent commit can remove signal
8a8cfb
    blocking from dlopen.
8a8cfb
    
8a8cfb
    This does not address pre-existing issues (unrelated to the NODELETE
8a8cfb
    changes) which make lazy binding in a signal handler during dlopen
8a8cfb
    unsafe, such as the use of malloc in both cases.
8a8cfb
    
8a8cfb
    Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
8a8cfb
    Reviewed-by: Carlos O'Donell <carlos@redhat.com>
8a8cfb
8a8cfb
diff --git a/elf/dl-close.c b/elf/dl-close.c
8a8cfb
index 243a028c443173c1..fa7f3e8174576e46 100644
8a8cfb
--- a/elf/dl-close.c
8a8cfb
+++ b/elf/dl-close.c
8a8cfb
@@ -197,7 +197,7 @@ _dl_close_worker (struct link_map *map, bool force)
8a8cfb
       /* Check whether this object is still used.  */
8a8cfb
       if (l->l_type == lt_loaded
8a8cfb
 	  && l->l_direct_opencount == 0
8a8cfb
-	  && l->l_nodelete != link_map_nodelete_active
8a8cfb
+	  && !l->l_nodelete_active
8a8cfb
 	  /* See CONCURRENCY NOTES in cxa_thread_atexit_impl.c to know why
8a8cfb
 	     acquire is sufficient and correct.  */
8a8cfb
 	  && atomic_load_acquire (&l->l_tls_dtor_count) == 0
8a8cfb
@@ -279,8 +279,7 @@ _dl_close_worker (struct link_map *map, bool force)
8a8cfb
 
8a8cfb
       if (!used[i])
8a8cfb
 	{
8a8cfb
-	  assert (imap->l_type == lt_loaded
8a8cfb
-		  && imap->l_nodelete != link_map_nodelete_active);
8a8cfb
+	  assert (imap->l_type == lt_loaded && !imap->l_nodelete_active);
8a8cfb
 
8a8cfb
 	  /* Call its termination function.  Do not do it for
8a8cfb
 	     half-cooked objects.  Temporarily disable exception
8a8cfb
@@ -820,7 +819,7 @@ _dl_close (void *_map)
8a8cfb
      before we took the lock. There is no way to detect this (see below)
8a8cfb
      so we proceed assuming this isn't the case.  First see whether we
8a8cfb
      can remove the object at all.  */
8a8cfb
-  if (__glibc_unlikely (map->l_nodelete == link_map_nodelete_active))
8a8cfb
+  if (__glibc_unlikely (map->l_nodelete_active))
8a8cfb
     {
8a8cfb
       /* Nope.  Do nothing.  */
8a8cfb
       __rtld_lock_unlock_recursive (GL(dl_load_lock));
8a8cfb
diff --git a/elf/dl-lookup.c b/elf/dl-lookup.c
8a8cfb
index 35a3f96a6296294a..01724a54f8840f9f 100644
8a8cfb
--- a/elf/dl-lookup.c
8a8cfb
+++ b/elf/dl-lookup.c
8a8cfb
@@ -187,6 +187,28 @@ enter_unique_sym (struct unique_sym *table, size_t size,
8a8cfb
   table[idx].map = map;
8a8cfb
 }
8a8cfb
 
8a8cfb
+/* Mark MAP as NODELETE according to the lookup mode in FLAGS.  During
8a8cfb
+   initial relocation, NODELETE state is pending only.  */
8a8cfb
+static void
8a8cfb
+mark_nodelete (struct link_map *map, int flags)
8a8cfb
+{
8a8cfb
+  if (flags & DL_LOOKUP_FOR_RELOCATE)
8a8cfb
+    map->l_nodelete_pending = true;
8a8cfb
+  else
8a8cfb
+    map->l_nodelete_active = true;
8a8cfb
+}
8a8cfb
+
8a8cfb
+/* Return true if MAP is marked as NODELETE according to the lookup
8a8cfb
+   mode in FLAGS> */
8a8cfb
+static bool
8a8cfb
+is_nodelete (struct link_map *map, int flags)
8a8cfb
+{
8a8cfb
+  /* Non-pending NODELETE always counts.  Pending NODELETE only counts
8a8cfb
+     during initial relocation processing.  */
8a8cfb
+  return map->l_nodelete_active
8a8cfb
+    || ((flags & DL_LOOKUP_FOR_RELOCATE) && map->l_nodelete_pending);
8a8cfb
+}
8a8cfb
+
8a8cfb
 /* Utility function for do_lookup_x. Lookup an STB_GNU_UNIQUE symbol
8a8cfb
    in the unique symbol table, creating a new entry if necessary.
8a8cfb
    Return the matching symbol in RESULT.  */
8a8cfb
@@ -311,8 +333,7 @@ do_lookup_unique (const char *undef_name, uint_fast32_t new_hash,
8a8cfb
       enter_unique_sym (entries, size,
8a8cfb
                         new_hash, strtab + sym->st_name, sym, map);
8a8cfb
 
8a8cfb
-      if (map->l_type == lt_loaded
8a8cfb
-	  && map->l_nodelete == link_map_nodelete_inactive)
8a8cfb
+      if (map->l_type == lt_loaded && !is_nodelete (map, flags))
8a8cfb
 	{
8a8cfb
 	  /* Make sure we don't unload this object by
8a8cfb
 	     setting the appropriate flag.  */
8a8cfb
@@ -320,10 +341,7 @@ do_lookup_unique (const char *undef_name, uint_fast32_t new_hash,
8a8cfb
 	    _dl_debug_printf ("\
8a8cfb
 marking %s [%lu] as NODELETE due to unique symbol\n",
8a8cfb
 			      map->l_name, map->l_ns);
8a8cfb
-	  if (flags & DL_LOOKUP_FOR_RELOCATE)
8a8cfb
-	    map->l_nodelete = link_map_nodelete_pending;
8a8cfb
-	  else
8a8cfb
-	    map->l_nodelete = link_map_nodelete_active;
8a8cfb
+	  mark_nodelete (map, flags);
8a8cfb
 	}
8a8cfb
     }
8a8cfb
   ++tab->n_elements;
8a8cfb
@@ -586,7 +604,7 @@ add_dependency (struct link_map *undef_map, struct link_map *map, int flags)
8a8cfb
      dependencies may pick an dependency which can be dlclose'd, but
8a8cfb
      such IFUNC resolvers are undefined anyway.  */
8a8cfb
   assert (map->l_type == lt_loaded);
8a8cfb
-  if (map->l_nodelete != link_map_nodelete_inactive)
8a8cfb
+  if (is_nodelete (map, flags))
8a8cfb
     return 0;
8a8cfb
 
8a8cfb
   struct link_map_reldeps *l_reldeps
8a8cfb
@@ -694,17 +712,16 @@ add_dependency (struct link_map *undef_map, struct link_map *map, int flags)
8a8cfb
 
8a8cfb
       /* Redo the NODELETE check, as when dl_load_lock wasn't held
8a8cfb
 	 yet this could have changed.  */
8a8cfb
-      if (map->l_nodelete != link_map_nodelete_inactive)
8a8cfb
+      if (is_nodelete (map, flags))
8a8cfb
 	goto out;
8a8cfb
 
8a8cfb
       /* If the object with the undefined reference cannot be removed ever
8a8cfb
 	 just make sure the same is true for the object which contains the
8a8cfb
 	 definition.  */
8a8cfb
-      if (undef_map->l_type != lt_loaded
8a8cfb
-	  || (undef_map->l_nodelete != link_map_nodelete_inactive))
8a8cfb
+      if (undef_map->l_type != lt_loaded || is_nodelete (map, flags))
8a8cfb
 	{
8a8cfb
 	  if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_BINDINGS)
8a8cfb
-	      && map->l_nodelete == link_map_nodelete_inactive)
8a8cfb
+	      && !is_nodelete (map, flags))
8a8cfb
 	    {
8a8cfb
 	      if (undef_map->l_name[0] == '\0')
8a8cfb
 		_dl_debug_printf ("\
8a8cfb
@@ -716,11 +733,7 @@ marking %s [%lu] as NODELETE due to reference to %s [%lu]\n",
8a8cfb
 				  map->l_name, map->l_ns,
8a8cfb
 				  undef_map->l_name, undef_map->l_ns);
8a8cfb
 	    }
8a8cfb
-
8a8cfb
-	  if (flags & DL_LOOKUP_FOR_RELOCATE)
8a8cfb
-	    map->l_nodelete = link_map_nodelete_pending;
8a8cfb
-	  else
8a8cfb
-	    map->l_nodelete = link_map_nodelete_active;
8a8cfb
+	  mark_nodelete (map, flags);
8a8cfb
 	  goto out;
8a8cfb
 	}
8a8cfb
 
8a8cfb
@@ -746,17 +759,14 @@ marking %s [%lu] as NODELETE due to reference to %s [%lu]\n",
8a8cfb
 		 cannot be unloaded.  This is semantically the correct
8a8cfb
 		 behavior.  */
8a8cfb
 	      if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_BINDINGS)
8a8cfb
-		  && map->l_nodelete == link_map_nodelete_inactive)
8a8cfb
+		  && !is_nodelete (map, flags))
8a8cfb
 		_dl_debug_printf ("\
8a8cfb
 marking %s [%lu] as NODELETE due to memory allocation failure\n",
8a8cfb
 				  map->l_name, map->l_ns);
8a8cfb
-	      if (flags & DL_LOOKUP_FOR_RELOCATE)
8a8cfb
-		/* In case of non-lazy binding, we could actually
8a8cfb
-		   report the memory allocation error, but for now, we
8a8cfb
-		   use the conservative approximation as well. */
8a8cfb
-		map->l_nodelete = link_map_nodelete_pending;
8a8cfb
-	      else
8a8cfb
-		map->l_nodelete = link_map_nodelete_active;
8a8cfb
+	      /* In case of non-lazy binding, we could actually report
8a8cfb
+		 the memory allocation error, but for now, we use the
8a8cfb
+		 conservative approximation as well.  */
8a8cfb
+	      mark_nodelete (map, flags);
8a8cfb
 	      goto out;
8a8cfb
 	    }
8a8cfb
 	  else
8a8cfb
diff --git a/elf/dl-open.c b/elf/dl-open.c
8a8cfb
index c7ed85b7ee99a296..a382bfae8aa3a2f8 100644
8a8cfb
--- a/elf/dl-open.c
8a8cfb
+++ b/elf/dl-open.c
8a8cfb
@@ -440,13 +440,17 @@ activate_nodelete (struct link_map *new)
8a8cfb
      NODELETE status for objects outside the local scope.  */
8a8cfb
   for (struct link_map *l = GL (dl_ns)[new->l_ns]._ns_loaded; l != NULL;
8a8cfb
        l = l->l_next)
8a8cfb
-    if (l->l_nodelete == link_map_nodelete_pending)
8a8cfb
+    if (l->l_nodelete_pending)
8a8cfb
       {
8a8cfb
 	if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_FILES))
8a8cfb
 	  _dl_debug_printf ("activating NODELETE for %s [%lu]\n",
8a8cfb
 			    l->l_name, l->l_ns);
8a8cfb
 
8a8cfb
-	l->l_nodelete = link_map_nodelete_active;
8a8cfb
+	l->l_nodelete_active = true;
8a8cfb
+
8a8cfb
+	/* This is just a debugging aid, to indicate that
8a8cfb
+	   activate_nodelete has run for this map.  */
8a8cfb
+	l->l_nodelete_pending = false;
8a8cfb
       }
8a8cfb
 }
8a8cfb
 
8a8cfb
@@ -549,10 +553,10 @@ dl_open_worker (void *a)
8a8cfb
       if (__glibc_unlikely (mode & RTLD_NODELETE))
8a8cfb
 	{
8a8cfb
 	  if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_FILES)
8a8cfb
-	      && new->l_nodelete == link_map_nodelete_inactive)
8a8cfb
+	      && !new->l_nodelete_active)
8a8cfb
 	    _dl_debug_printf ("marking %s [%lu] as NODELETE\n",
8a8cfb
 			      new->l_name, new->l_ns);
8a8cfb
-	  new->l_nodelete = link_map_nodelete_active;
8a8cfb
+	  new->l_nodelete_active = true;
8a8cfb
 	}
8a8cfb
 
8a8cfb
       /* Finalize the addition to the global scope.  */
8a8cfb
@@ -568,7 +572,7 @@ dl_open_worker (void *a)
8a8cfb
   /* Schedule NODELETE marking for the directly loaded object if
8a8cfb
      requested.  */
8a8cfb
   if (__glibc_unlikely (mode & RTLD_NODELETE))
8a8cfb
-    new->l_nodelete = link_map_nodelete_pending;
8a8cfb
+    new->l_nodelete_pending = true;
8a8cfb
 
8a8cfb
   /* Load that object's dependencies.  */
8a8cfb
   _dl_map_object_deps (new, NULL, 0, 0,
8a8cfb
@@ -680,7 +684,7 @@ dl_open_worker (void *a)
8a8cfb
 	      _dl_start_profile ();
8a8cfb
 
8a8cfb
 	      /* Prevent unloading the object.  */
8a8cfb
-	      GL(dl_profile_map)->l_nodelete = link_map_nodelete_active;
8a8cfb
+	      GL(dl_profile_map)->l_nodelete_active = true;
8a8cfb
 	    }
8a8cfb
 	}
8a8cfb
       else
8a8cfb
@@ -879,9 +883,9 @@ no more namespaces available for dlmopen()"));
8a8cfb
 	     happens inside dl_open_worker.  */
8a8cfb
 	  __libc_signal_restore_set (&args.original_signal_mask);
8a8cfb
 
8a8cfb
-	  /* All link_map_nodelete_pending objects should have been
8a8cfb
-	     deleted at this point, which is why it is not necessary
8a8cfb
-	     to reset the flag here.  */
8a8cfb
+	  /* All l_nodelete_pending objects should have been deleted
8a8cfb
+	     at this point, which is why it is not necessary to reset
8a8cfb
+	     the flag here.  */
8a8cfb
 	}
8a8cfb
       else
8a8cfb
 	__libc_signal_restore_set (&args.original_signal_mask);
8a8cfb
diff --git a/elf/get-dynamic-info.h b/elf/get-dynamic-info.h
8a8cfb
index ea286abaea0128d1..78ba7e76db9706cc 100644
8a8cfb
--- a/elf/get-dynamic-info.h
8a8cfb
+++ b/elf/get-dynamic-info.h
8a8cfb
@@ -164,7 +164,7 @@ elf_get_dynamic_info (struct link_map *l, ElfW(Dyn) *temp)
8a8cfb
     {
8a8cfb
       l->l_flags_1 = info[VERSYMIDX (DT_FLAGS_1)]->d_un.d_val;
8a8cfb
       if (l->l_flags_1 & DF_1_NODELETE)
8a8cfb
-	l->l_nodelete = link_map_nodelete_pending;
8a8cfb
+	l->l_nodelete_pending = true;
8a8cfb
 
8a8cfb
       /* Only DT_1_SUPPORTED_MASK bits are supported, and we would like
8a8cfb
 	 to assert this, but we can't. Users have been setting
8a8cfb
diff --git a/include/link.h b/include/link.h
8a8cfb
index a277b77cad6b52b1..e90fa79a0b332087 100644
8a8cfb
--- a/include/link.h
8a8cfb
+++ b/include/link.h
8a8cfb
@@ -79,22 +79,6 @@ struct r_search_path_struct
8a8cfb
     int malloced;
8a8cfb
   };
8a8cfb
 
8a8cfb
-/* Type used by the l_nodelete member.  */
8a8cfb
-enum link_map_nodelete
8a8cfb
-{
8a8cfb
- /* This link map can be deallocated.  */
8a8cfb
- link_map_nodelete_inactive = 0, /* Zero-initialized in _dl_new_object.  */
8a8cfb
-
8a8cfb
- /* This link map cannot be deallocated.  */
8a8cfb
- link_map_nodelete_active,
8a8cfb
-
8a8cfb
- /* This link map cannot be deallocated after dlopen has succeded.
8a8cfb
-    dlopen turns this into link_map_nodelete_active.  dlclose treats
8a8cfb
-    this intermediate state as link_map_nodelete_active.  */
8a8cfb
- link_map_nodelete_pending,
8a8cfb
-};
8a8cfb
-
8a8cfb
-
8a8cfb
 /* Structure describing a loaded shared object.  The `l_next' and `l_prev'
8a8cfb
    members form a chain of all the shared objects loaded at startup.
8a8cfb
 
8a8cfb
@@ -218,10 +202,17 @@ struct link_map
8a8cfb
 				       freed, ie. not allocated with
8a8cfb
 				       the dummy malloc in ld.so.  */
8a8cfb
 
8a8cfb
-    /* Actually of type enum link_map_nodelete.  Separate byte due to
8a8cfb
-       a read in add_dependency in elf/dl-lookup.c outside the loader
8a8cfb
-       lock.  Only valid for l_type == lt_loaded.  */
8a8cfb
-    unsigned char l_nodelete;
8a8cfb
+    /* NODELETE status of the map.  Only valid for maps of type
8a8cfb
+       lt_loaded.  Lazy binding sets l_nodelete_active directly,
8a8cfb
+       potentially from signal handlers.  Initial loading of an
8a8cfb
+       DF_1_NODELETE object set l_nodelete_pending.  Relocation may
8a8cfb
+       set l_nodelete_pending as well.  l_nodelete_pending maps are
8a8cfb
+       promoted to l_nodelete_active status in the final stages of
8a8cfb
+       dlopen, prior to calling ELF constructors.  dlclose only
8a8cfb
+       refuses to unload l_nodelete_active maps, the pending status is
8a8cfb
+       ignored.  */
8a8cfb
+    bool l_nodelete_active;
8a8cfb
+    bool l_nodelete_pending;
8a8cfb
 
8a8cfb
 #include <link_map.h>
8a8cfb