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

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

Created on 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/ no version information available (required by usr/bin/lldb-argdumper)
usr/bin/lldb-argdumper: /usr/lib/ no version information available (required by /tmp/tmp.8oiyW382Pu/usr/bin/../lib/
usr/bin/lldb-argdumper: /usr/lib/ no version information available (required by /tmp/tmp.8oiyW382Pu/usr/bin/../lib/
usr/bin/lldb-argdumper: /usr/lib/ no version information available (required by /tmp/tmp.8oiyW382Pu/usr/bin/../lib/

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 link

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! didn't work. Time to dig in the code.

Exploring the glibc source code link

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 binary link

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

First off, I located my dynamic linker:

 ryan@DevPC-archLX  ~  patchelf --print-interpreter /bin/sh
 ryan@DevPC-archLX  ~  realpath /lib64/
 ryan@DevPC-archLX  ~  mkdir ld-hack
 ryan@DevPC-archLX  ~  cd ld-hack
 ryan@DevPC-archLX  ~/ld-hack  cp /usr/lib/

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

printf is used to send the bytes to dd , which will write them to 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 -bo 'di -F intel -n _dl_check_all_versions'
Current executable set to '' (x86_64).
(lldb) di -F intel -n _dl_check_all_versions`_dl_check_all_versions:
(...)[0x111d8] <+56>:  xor   esi, esi[0x111da] <+58>:  nop


Using the new dynamic linker link

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/ usr/bin/lldb-argdumper

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

Using qldv link

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