Getting rid of version warnings: an experiment at hacking the Linux/glibc dynamic linker to shut up

2018-01-02 ⋅ Comments

If you use a non-Debian/Ubuntu distro (I recently switched to Arch), you've probably had a moment where you downloaded some binaries and tried running them, only to get an error like this:

$ lldb-argdumper -h
usr/bin/lldb-argdumper: /usr/lib/libtinfo.so.5: no version information available (required by usr/bin/lldb-argdumper)
usr/bin/lldb-argdumper: /usr/lib/libtinfo.so.5: no version information available (required by /tmp/tmp.8oiyW382Pu/usr/bin/../lib/liblldb.so.4)
usr/bin/lldb-argdumper: /usr/lib/libtinfo.so.5: no version information available (required by /tmp/tmp.8oiyW382Pu/usr/bin/../lib/liblldb.so.4)
usr/bin/lldb-argdumper: /usr/lib/libpanel.so.5: no version information available (required by /tmp/tmp.8oiyW382Pu/usr/bin/../lib/liblldb.so.4)

Ugh. Normally these warnings are nothing more than an annoyance. However, recently I started trying to get Swift working on my new Arch install. With Swift, the warnings suddenly turned much more lethal: some part of swift package build assumes that, if one of the commands outputs anything (including these warnings), it has failed, and the build will be aborted.

Obviously, I couldn't stand for this. I mean, how hard could this be to fix?

(Spoiler alert: if you want to cut to the chase, I created a tool called qldv that does everything listed below already.)

Starting the search: LD_NOWARN

When I started Googling, all I could find where Stack Overflow posts where the accepted answer was, upgrade your packages . Of course, that only works if your distro uses versioned shared libraries. Guess What? Arch doesn't .

I then discovered the LD_NOWARN environment variable. This looked like the perfect solution! Except...it didn't work. Time to dig in the code.

Exploring the glibc source code

A quick GitHub search led me to find dl-version.c , the file where the warning is emitted. This is what the code looks like:

  if (__glibc_unlikely (map->l_info[VERSYMIDX (DT_VERDEF)] == NULL))
      /* The file has no symbol versioning.  I.e., the dependent
	 object was linked against another version of this file.  We
	 only print a message if verbose output is requested.  */
      if (verbose)
	  /* XXX We cannot translate the messages.  */
	    (&exception, DSO_FILENAME (map->l_name),
	     "no version information available (required by %s)", name);
	  goto call_cerror;
      return 0;

Looks pretty simple, right? This is inside the function match_symbol , which takes an argument named verbose . I figured all I had to do was figure out how to make verbose 0/false.

A further search showed that match_symbol is called by _dl_check_map_versions , which passes down the verbose argument. That function is called by _dl_check_all_versions , which again is passing down a verbose argument.

_dl_check_all_versions is in turn called by version_check_doit located in rtld.c . This is the code:

static void
version_check_doit (void *a)
  struct version_check_args *args = (struct version_check_args *) a;
  if (_dl_check_all_versions (GL(dl_ns)[LM_ID_BASE]._ns_loaded, 1,
			      args->dotrace) && args->doexit)
    /* We cannot start the application.  Abort now.  */
    _exit (1);

See the constant 1 argument that can't be changed? Yup, that's the verbose argument.

Hacking the ld.so binary

This seems impossible to overcome. Unless, of course, you modify the ld.so binary, right?

First off, I located my dynamic linker:

 ryan@DevPC-archLX  ~  patchelf --print-interpreter /bin/sh
 ryan@DevPC-archLX  ~  realpath /lib64/ld-linux-x86-64.so.2
 ryan@DevPC-archLX  ~  mkdir ld-hack
 ryan@DevPC-archLX  ~  cd ld-hack
 ryan@DevPC-archLX  ~/ld-hack  cp /usr/lib/ld-2.26.so ld.so

Now that I had a copy of the linker, I used lldb to print the assembler code inside of the _dl_check_all_versions (this seemed like an easy target to change):

 ryan@DevPC-archLX  ~/ld-hack  lldb ld.so -bo 'di -F intel -n _dl_check_all_versions'
Current executable set to 'ld.so' (x86_64).
(lldb) di -F intel -n _dl_check_all_versions
ld.so[0x111a0] <+0>:   push   r13
ld.so[0x111a2] <+2>:   push   r12
ld.so[0x111a4] <+4>:   push   rbp
ld.so[0x111a5] <+5>:   push   rbx
ld.so[0x111a6] <+6>:   sub    rsp, 0x8
ld.so[0x111aa] <+10>:  test   rdi, rdi
ld.so[0x111ad] <+13>:  je     0x11200                   ; <+96>
ld.so[0x111af] <+15>:  mov    rbx, rdi
ld.so[0x111b2] <+18>:  mov    r12d, esi
ld.so[0x111b5] <+21>:  mov    r13d, edx
ld.so[0x111b8] <+24>:  xor    ebp, ebp
ld.so[0x111ba] <+26>:  jmp    0x111c9                   ; <+41>
ld.so[0x111bc] <+28>:  nop    dword ptr [rax]
ld.so[0x111c0] <+32>:  mov    rbx, qword ptr [rbx + 0x18]
ld.so[0x111c4] <+36>:  test   rbx, rbx
ld.so[0x111c7] <+39>:  je     0x111f3                   ; <+83>
ld.so[0x111c9] <+41>:  test   byte ptr [rbx + 0x315], 0x2
ld.so[0x111d0] <+48>:  jne    0x111c0                   ; <+32>
ld.so[0x111d2] <+50>:  mov    rdi, rbx
ld.so[0x111d5] <+53>:  mov    edx, r13d
ld.so[0x111d8] <+56>:  mov    esi, r12d
ld.so[0x111db] <+59>:  call   0x10d30                   ; _dl_check_map_versions
ld.so[0x111e0] <+64>:  mov    rbx, qword ptr [rbx + 0x18]
ld.so[0x111e4] <+68>:  test   eax, eax
ld.so[0x111e6] <+70>:  setne  al
ld.so[0x111e9] <+73>:  movzx  eax, al
ld.so[0x111ec] <+76>:  or     ebp, eax
ld.so[0x111ee] <+78>:  test   rbx, rbx
ld.so[0x111f1] <+81>:  jne    0x111c9                   ; <+41>
ld.so[0x111f3] <+83>:  add    rsp, 0x8
ld.so[0x111f7] <+87>:  mov    eax, ebp
ld.so[0x111f9] <+89>:  pop    rbx
ld.so[0x111fa] <+90>:  pop    rbp
ld.so[0x111fb] <+91>:  pop    r12
ld.so[0x111fd] <+93>:  pop    r13
ld.so[0x111ff] <+95>:  ret
ld.so[0x11200] <+96>:  add    rsp, 0x8
ld.so[0x11204] <+100>: xor    ebp, ebp
ld.so[0x11206] <+102>: pop    rbx
ld.so[0x11207] <+103>: mov    eax, ebp
ld.so[0x11209] <+105>: pop    rbp
ld.so[0x1120a] <+106>: pop    r12
ld.so[0x1120c] <+108>: pop    r13
ld.so[0x1120e] <+110>: ret

_dl_check_all_versions calls _dl_check_map_versions at offset 0x111db : call 0x10d30 . Look at the instruction immediately before it (at 0x111d8 ): mov esi, r12d . With the System-V x86_64 ABI, esi is the register used to hold the second argument. Therefore, this instruction is the one that gets the verbose argument ready to pass to _dl_check_map_versions .

In order to make verbose 0, this instruction needs to be replaced with one that assigns it to 0. In addition, this instruction is 3 bytes in size. The replacement therefore needs to be either 3 bytes or smaller (it can be padded with extra nop s). A quick experiment shows that xor esi, esi is the way to go:

 ryan@DevPC-archLX  ~/ld-hack  echo -e 'mov esi, 0\nxor esi, esi' > x.asm
 ryan@DevPC-archLX  ~/ld-hack  nasm -f elf64 -o x.o x.asm
 ryan@DevPC-archLX  ~/ld-hack  objdump -Mintel -D x.o

x.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <.text>:
   0:	be 00 00 00 00       	mov    esi,0x0
   5:	31 f6                	xor    esi,esi

(Technically, shr esi, 1 would've also done the trick, since 1 >> 1 == 0 .)

Now's to patch the linker to replace the instruction with xor esi, esi ( 0x31 0xf6 , as shown above) followed by a nop> (which is 0x90 ). printf + dd can be used for this:

 ryan@DevPC-archLX  ~/ld-hack  printf '\x31\xf6\x90' | dd of=ld.so bs=1 seek=$((0x111d8)) count=3 conv=notrunc

printf is used to send the bytes to dd , which will write them to ld.so at the given offset (the $((...)) syntax is used to convert the hex location to decimal). count=3 is passed to ensure only 3 bytes are written, and conv=notrunc prevents dd from truncating the rest of the file.

Now, if you run lldb again, you'll see the changed bytes:

 ryan@DevPC-archLX  ~/ld-hack  lldb ld.so -bo 'di -F intel -n _dl_check_all_versions'
Current executable set to 'ld.so' (x86_64).
(lldb) di -F intel -n _dl_check_all_versions
ld.so[0x111d8] <+56>:  xor   esi, esi
ld.so[0x111da] <+58>:  nop


Using the new dynamic linker

Of course, our application is still using the old linker. Let's use patchelf to force use of the new one:

 ryan@DevPC-archLX  ~/ld-hack  patchelf --set-interpreter $PWD/ld.so usr/bin/lldb-argdumper

Now you can try the executable again, and there will be no warnings this time!

Using qldv

This is all a bit tedious, so I created a tool for this: qldv . With qldv, this all is reduced to:

 ryan@DevPC-archLX  ~/ld-hack  qldv -set usr/bin/lldb-argdumer ld.so