re:fi.64

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/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 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! Except...it 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.  */
	  _dl_exception_create_format
	    (&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 link

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
/lib64/ld-linux-x86-64.so.2
 ryan@DevPC-archLX  ~  realpath /lib64/ld-linux-x86-64.so.2
/usr/lib/ld-2.26.so
 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`_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`_dl_check_all_versions:
(...)
ld.so[0x111d8] <+56>:  xor   esi, esi
ld.so[0x111da] <+58>:  nop

Viola!

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/ld.so 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 ld.so