Getting rid of version warnings: an experiment at hacking the Linux/glibc dynamic linker to shut up
2018-01-02 ⋅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. */
_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
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
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