Loading glibc in a new namespace fails vtable check

Moshe Rubin moshe.rubin@gmail.com
Thu Sep 26 15:14:25 GMT 2024


Hi Florian,

Thank you for reaching out to me on this issue.  I tried to come up with a
minimal reproducible example:

<code>
// To build: g++ minimal_example.cpp -o minimal_example -g -ldl
// To run: ./minimal_example <path-to-libc>
// Example: ./minimal_example /usr/lib/x86_64-linux-gnu/libc.so.6

#include <dlfcn.h>
#include <cstdio>

int main(int argc, char*argv[])
{
  printf("glibc path: %s\n", argv[1]);

  // Load a second copy of glibc to a new namespace
  void* handle = dlmopen(LM_ID_NEWLM, argv[1], RTLD_LAZY);
  if (!handle) {
    printf("Failed to load glibc \"%s\" using dlmopen\n", argv[1]);
    return -1;
  }

  // Get the address of fileno() from the new glibc
  int (*libc_fileno)(FILE *fp);
  libc_fileno = reinterpret_cast<decltype(libc_fileno)>(dlsym(handle,
"fileno"));

  int fd = libc_fileno(stderr);

  return 0;
}
</code>

Here's is a typical run:

<output>
$ ./minimal_example /usr/lib/x86_64-linux-gnu/libc.so.6
glibc path: /usr/lib/x86_64-linux-gnu/libc.so.6
fd = 2
</output

Unfortunately, the app runs successfully, no fatal error.  I do, however,
have a question.  From inside gdb, I executed the
following two commands before and following the call to dlmopen():

shell cat /proc/self/maps > /tmp/foo.before
shell cat /proc/self/maps > /tmp/foo.after

Two observations:

(1) I don't see the second glibc instance in foo.after.
(2) The addresses have totally changed  before and after the dlmopen() call.

foo.before:
56076df9c000-56076df9e000 r--p 00000000 103:0e 3313694
/usr/bin/cat
56076df9e000-56076dfa2000 r-xp 00002000 103:0e 3313694
/usr/bin/cat
56076dfa2000-56076dfa4000 r--p 00006000 103:0e 3313694
/usr/bin/cat
56076dfa4000-56076dfa5000 r--p 00007000 103:0e 3313694
/usr/bin/cat
56076dfa5000-56076dfa6000 rw-p 00008000 103:0e 3313694
/usr/bin/cat
56076dfa6000-56076dfc7000 rw-p 00000000 00:00 0
 [heap]
7f7fe6673000-7f7fe6695000 rw-p 00000000 00:00 0
7f7fe6695000-7f7fe697f000 r--p 00000000 103:0e 3277352
/usr/lib/locale/locale-archive
7f7fe697f000-7f7fe6982000 rw-p 00000000 00:00 0
7f7fe6982000-7f7fe69aa000 r--p 00000000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f7fe69aa000-7f7fe6b3f000 r-xp 00028000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f7fe6b3f000-7f7fe6b97000 r--p 001bd000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f7fe6b97000-7f7fe6b98000 ---p 00215000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f7fe6b98000-7f7fe6b9c000 r--p 00215000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f7fe6b9c000-7f7fe6b9e000 rw-p 00219000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f7fe6b9e000-7f7fe6bab000 rw-p 00000000 00:00 0
7f7fe6bb7000-7f7fe6bbe000 r--s 00000000 103:0e 3314018
/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f7fe6bbe000-7f7fe6bc0000 rw-p 00000000 00:00 0
7f7fe6bc0000-7f7fe6bc2000 r--p 00000000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7fe6bc2000-7f7fe6bec000 r-xp 00002000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7fe6bec000-7f7fe6bf7000 r--p 0002c000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7fe6bf7000-7f7fe6bf8000 r--p 00000000 103:0e 3304170
/usr/lib/locale/C.utf8/LC_COLLATE
7f7fe6bf8000-7f7fe6bfa000 r--p 00037000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7fe6bfa000-7f7fe6bfc000 rw-p 00039000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffcfb994000-7ffcfb9b5000 rw-p 00000000 00:00 0
 [stack]
7ffcfb9b8000-7ffcfb9bc000 r--p 00000000 00:00 0
 [vvar]
7ffcfb9bc000-7ffcfb9be000 r-xp 00000000 00:00 0
 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0
 [vsyscall]

foo.after:
55b0e5b41000-55b0e5b43000 r--p 00000000 103:0e 3313694
/usr/bin/cat
55b0e5b43000-55b0e5b47000 r-xp 00002000 103:0e 3313694
/usr/bin/cat
55b0e5b47000-55b0e5b49000 r--p 00006000 103:0e 3313694
/usr/bin/cat
55b0e5b49000-55b0e5b4a000 r--p 00007000 103:0e 3313694
/usr/bin/cat
55b0e5b4a000-55b0e5b4b000 rw-p 00008000 103:0e 3313694
/usr/bin/cat
55b0e5b4b000-55b0e5b6c000 rw-p 00000000 00:00 0
 [heap]
7f4f288c8000-7f4f288ea000 rw-p 00000000 00:00 0
7f4f288ea000-7f4f28bd4000 r--p 00000000 103:0e 3277352
/usr/lib/locale/locale-archive
7f4f28bd4000-7f4f28bd7000 rw-p 00000000 00:00 0
7f4f28bd7000-7f4f28bff000 r--p 00000000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f4f28bff000-7f4f28d94000 r-xp 00028000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f4f28d94000-7f4f28dec000 r--p 001bd000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f4f28dec000-7f4f28ded000 ---p 00215000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f4f28ded000-7f4f28df1000 r--p 00215000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f4f28df1000-7f4f28df3000 rw-p 00219000 103:0e 3313752
/usr/lib/x86_64-linux-gnu/libc.so.6
7f4f28df3000-7f4f28e00000 rw-p 00000000 00:00 0
7f4f28e0c000-7f4f28e13000 r--s 00000000 103:0e 3314018
/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f4f28e13000-7f4f28e15000 rw-p 00000000 00:00 0
7f4f28e15000-7f4f28e17000 r--p 00000000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f4f28e17000-7f4f28e41000 r-xp 00002000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f4f28e41000-7f4f28e4c000 r--p 0002c000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f4f28e4c000-7f4f28e4d000 r--p 00000000 103:0e 3304170
/usr/lib/locale/C.utf8/LC_COLLATE
7f4f28e4d000-7f4f28e4f000 r--p 00037000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f4f28e4f000-7f4f28e51000 rw-p 00039000 103:0e 3296069
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffd9b933000-7ffd9b954000 rw-p 00000000 00:00 0
 [stack]
7ffd9b9dc000-7ffd9b9e0000 r--p 00000000 00:00 0
 [vvar]
7ffd9b9e0000-7ffd9b9e2000 r-xp 00000000 00:00 0
 [vdso]
+ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0
 [vsyscall]

Can you explain both observations?

Thanks,

Moshe


On Thu, Sep 26, 2024 at 11:51 AM Florian Weimer <fweimer@redhat.com> wrote:

> * Moshe Rubin:
>
> > The fatal error was caused when the above functions were passed a
> > FILE* of stdin/stdout/stderr.  Calling p_fwrite(stdout) is essentially
> > calling the fwrite() function in the second namespace with a FILE*
> > created in the default namespace.  This is precisely what the
> > _IO_vtable_check() function checks for.  When this occurred, the fatal
> > error was triggered.
>
> But this actually expected to work.  The slow path of the vtable
> verification checks for an inner libc.
>
> void attribute_hidden
> _IO_vtable_check (void)
> {
> #ifdef SHARED
>   /* Honor the compatibility flag.  */
>   void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
>   PTR_DEMANGLE (flag);
>   if (flag == &_IO_vtable_check)
>     return;
>
>   /* In case this libc copy is in a non-default namespace, we always
>      need to accept foreign vtables because there is always a
>      possibility that FILE * objects are passed across the linking
>      boundary.  */
>   {
>     Dl_info di;
>     struct link_map *l;
>     if (!rtld_active ()
>         || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
>             && l->l_ns != LM_ID_BASE))
>       return;
>   }
>
> #else /* !SHARED */
>   /* We cannot perform vtable validation in the static dlopen case
>      because FILE * handles might be passed back and forth across the
>      boundary.  Therefore, we disable checking in this case.  */
>   if (__dlopen != NULL)
>     return;
> #endif
>
>   __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
> }
>
> It's not clear to me why this doesn't return before failing because
> _IO_vtable_check is in a secondary namespace.
>
> You could try this patch to replace the _dl_addr with a simpler check,
> using the __libc_initial flag that was added after vtable verification
> was introduced:
>
> diff --git a/libio/vtables.c b/libio/vtables.c
> index 8a2c726bf5..2711f2700f 100644
> --- a/libio/vtables.c
> +++ b/libio/vtables.c
> @@ -16,7 +16,7 @@
>     License along with the GNU C Library; if not, see
>     <https://www.gnu.org/licenses/>.  */
>
> -#include <dlfcn.h>
> +#include <libc-internal.h>
>  #include <libioP.h>
>  #include <stdio.h>
>  #include <ldsodefs.h>
> @@ -514,14 +514,8 @@ _IO_vtable_check (void)
>       need to accept foreign vtables because there is always a
>       possibility that FILE * objects are passed across the linking
>       boundary.  */
> -  {
> -    Dl_info di;
> -    struct link_map *l;
> -    if (!rtld_active ()
> -        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
> -            && l->l_ns != LM_ID_BASE))
> -      return;
> -  }
> +  if (!__libc_initial)
> +    return;
>
>  #else /* !SHARED */
>    /* We cannot perform vtable validation in the static dlopen case
>
> Anyway, this does look like a bug, and we'd appreciate a reduced test
> case.
>
> Thanks,
> Florian
>
>


More information about the Libc-help mailing list