ligurio / luzer

A coverage-guided, native Lua fuzzing engine.
ISC License
39 stars 3 forks source link

__sanitizer_cov_8bit_counters_init never invoked for interpreter #12

Open azanegin opened 9 months ago

azanegin commented 9 months ago

Will provide PR to fix in some time.

The __sanitizer_cov_8bit_counters_init() is a hook that LLVM SanitizerCoverage uses to point client code to inline counters that are incremented by instrumented code (well, when used 8bit-inline-ctrs instrumentation). Counters are usually allocated as a separate DSO section.

00000000000558c3 d __start___sancov_cntrs
0000000000055968 d __start___sancov_pcs
0000000000055961 d __stop___sancov_cntrs
0000000000056348 d __stop___sancov_pcs
000000000000e100 t sancov.module_ctor_8bit_counters

LibFuzzer uses this hook to a add those counters to monitored list. Global DSO-wide constructor inside sancov.module_ctor_8bit_counters in each loaded module calls this hook with pointers to ctrs section.

Code located here https://github.com/ligurio/luzer/blob/0179547ff0fefbc18aee2372d0fed63784e9b4dd/luzer/luzer.c#L232 also invoke __sanitizer_cov_8bit_counters_init(). This is most likely intended for pointing to libfuzzer the region of counters that is used for interpreted code of the lua script.

Running test script outputs the following:

Starting program: /usr/bin/lua ../../luzer/tests/test_e2e.lua
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2552143800
INFO: Loaded 1 modules   (158 inline 8-bit counters): 158 [0x7ffff7c2f8c3, 0x7ffff7c2f961),
INFO: Loaded 1 PC tables (158 PCs): 158 [0x7ffff7c2f968,0x7ffff7c30348),
[New Thread 0x7ffff622a6c0 (LWP 298)]
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2      INITED cov: 32 ft: 33 corp: 1/1b exec/s: 0 rss: 36Mb
#7      NEW    cov: 33 ft: 34 corp: 2/2b lim: 4 exec/s: 0 rss: 36Mb L: 1/1 MS: 5 ShuffleBytes-ChangeByte-ShuffleBytes-CrossOver-ChangeBit-
#30     NEW    cov: 34 ft: 36 corp: 3/3b lim: 4 exec/s: 0 rss: 36Mb L: 1/1 MS: 3 ChangeByte-CopyPart-ChangeByte-
        NEW_FUNC[1/1]: 0x7ffff7be9161
#76     NEW    cov: 36 ft: 39 corp: 4/5b lim: 4 exec/s: 0 rss: 36Mb L: 2/2 MS: 1 InsertByte-
#78     NEW    cov: 36 ft: 42 corp: 5/7b lim: 4 exec/s: 0 rss: 36Mb L: 2/2 MS: 2 CrossOver-EraseBytes-
#124    NEW    cov: 36 ft: 44 corp: 6/8b lim: 4 exec/s: 0 rss: 36Mb L: 1/2 MS: 1 EraseBytes-

This creates a false impression that mmap-ed counters for lua code are working. As I pointed out above, each instrumented DSO should call the hook once. Then, luzer.so code should call the hook for dynamic counters. This is not the case.

root@trixie:~/luzer/build/luzer# gdb /usr/bin/lua
GNU gdb (Debian 13.2-1) 13.2
8<==============cut==================
(No debugging symbols found in /usr/bin/lua)
(gdb) b __sanitizer_cov_8bit_counters_init
Function "__sanitizer_cov_8bit_counters_init" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (__sanitizer_cov_8bit_counters_init) pending.
(gdb) run ../../luzer/tests/test_e2e.lua
Starting program: /usr/bin/lua ../../luzer/tests/test_e2e.lua
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x00007ffff7c1a3e8 in __sanitizer_cov_8bit_counters_init () from ./luzer.so
(gdb) bt
#0  0x00007ffff7c1a3e8 in __sanitizer_cov_8bit_counters_init () from ./luzer.so
#1  0x00007ffff7be8117 in sancov.module_ctor_8bit_counters () from ./luzer.so
#2  0x00007ffff7fcfd2e in call_init (env=0x7fffffffed10, argv=0x7fffffffecf8, argc=2, l=<optimized out>) at ./elf/dl-init.c:90
#3  call_init (l=<optimized out>, argc=2, argv=0x7fffffffecf8, env=0x7fffffffed10) at ./elf/dl-init.c:27
#4  0x00007ffff7fcfe14 in _dl_init (main_map=0x555555592280, argc=2, argv=0x7fffffffecf8, env=0x7fffffffed10) at ./elf/dl-init.c:137
#5  0x00007ffff7fcc516 in __GI__dl_catch_exception (exception=exception@entry=0x0, operate=operate@entry=0x7ffff7fd6570 <call_dl_init>, args=args@entry=0x7fffffffe0b0) at ./elf/dl-catch.c:211
#6  0x00007ffff7fd650e in dl_open_worker (a=a@entry=0x7fffffffe250) at ./elf/dl-open.c:808
#7  0x00007ffff7fcc489 in __GI__dl_catch_exception (exception=exception@entry=0x7fffffffe230, operate=operate@entry=0x7ffff7fd6480 <dl_open_worker>, args=args@entry=0x7fffffffe250) at ./elf/dl-catch.c:237
#8  0x00007ffff7fd68a8 in _dl_open (file=0x555555592158 "./luzer.so", mode=<optimized out>, caller_dlopen=0x55555557972b, nsid=<optimized out>, argc=2, argv=0x7fffffffecf8, env=0x7fffffffed10) at ./elf/dl-open.c:884
#9  0x00007ffff7d236f8 in dlopen_doit (a=a@entry=0x7fffffffe4c0) at ./dlfcn/dlopen.c:56
#10 0x00007ffff7fcc489 in __GI__dl_catch_exception (exception=exception@entry=0x7fffffffe420, operate=0x7ffff7d236a0 <dlopen_doit>, args=0x7fffffffe4c0) at ./elf/dl-catch.c:237
#11 0x00007ffff7fcc5af in _dl_catch_error (objname=0x7fffffffe478, errstring=0x7fffffffe480, mallocedp=0x7fffffffe477, operate=<optimized out>, args=<optimized out>) at ./elf/dl-catch.c:256
#12 0x00007ffff7d231e7 in _dlerror_run (operate=operate@entry=0x7ffff7d236a0 <dlopen_doit>, args=args@entry=0x7fffffffe4c0) at ./dlfcn/dlerror.c:138
#13 0x00007ffff7d237a9 in dlopen_implementation (dl_caller=<optimized out>, mode=<optimized out>, file=<optimized out>) at ./dlfcn/dlopen.c:71
#14 ___dlopen (file=<optimized out>, mode=<optimized out>) at ./dlfcn/dlopen.c:81
#15 0x000055555557972b in ?? ()
8<=======cut============
#37 0x000055555555a7ba in ?? ()
(gdb) c
Continuing.
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3118463730
INFO: Loaded 1 modules   (158 inline 8-bit counters): 158 [0x7ffff7c2f8c3, 0x7ffff7c2f961),
INFO: Loaded 1 PC tables (158 PCs): 158 [0x7ffff7c2f968,0x7ffff7c30348),
[New Thread 0x7ffff622a6c0 (LWP 391)]
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2      INITED cov: 32 ft: 33 corp: 1/1b exec/s: 0 rss: 70Mb
#23     NEW    cov: 33 ft: 34 corp: 2/2b lim: 4 exec/s: 0 rss: 70Mb L: 1/1 MS: 1 ChangeBinInt-
        NEW_FUNC[1/1]: 0x7ffff7be9161
#77     NEW    cov: 35 ft: 37 corp: 3/5b lim: 4 exec/s: 0 rss: 70Mb L: 3/3 MS: 4 CrossOver-InsertByte-ChangeByte-ChangeByte-
8<=====cut========
/usr/bin/lua: ../../luzer/tests/test_e2e.lua:7: assert has triggered
stack traceback:
        [C]: in function 'assert'
        ../../luzer/tests/test_e2e.lua:7: in function <../../luzer/tests/test_e2e.lua:3>
        [C]: in function 'Fuzz'
        ../../luzer/tests/test_e2e.lua:15: in main chunk
        [C]: ?
==390== ERROR: libFuzzer: fuzz target exited
[Thread 0x7ffff622a6c0 (LWP 391) exited]
[Inferior 1 (process 390) exited with code 0115]

As we can see here, _init() hook is called just once. And this call is from none other place than luzer.so DSO constructor. No call for mmap-ed counters ever made. Why?

https://github.com/ligurio/luzer/blob/0179547ff0fefbc18aee2372d0fed63784e9b4dd/luzer/counters.c#L126

This condition is always true, as (contrast to atheris) in this code no counters are ever registered.

Basically, right now the Lua "debug hook instrumentation" is useless. Only way it ever reaches LibFuzzer is via another bug: https://github.com/ligurio/luzer/issues/11

But luzer works for compiled, native lua modules, that were build with SanCov and register their own counters.