DynamoRIO / dynamorio

Dynamic Instrumentation Tool Platform
Other
2.64k stars 560 forks source link

Usage of Private Code Cache and Clean Calls confuses DR when main cache is thread private. #3746

Open johnfxgalea opened 5 years ago

johnfxgalea commented 5 years ago

Problem: Undefined behaviour is encountered when a client places a clean call inside a private shared code cache and has DR options set to use private caches (via -thread_private) for the main cache. At a high level, DynamoRIO gets confused, as it treats the insertion of the clean call as an insertion to a thread-private cache. However, the private cache is NOT thread-private but is shared.

The problem mainly occurs on 32-bit platforms.

Test Case: The following code is a modified version of the memtrace sample. I could have used the original code of memtrace to show the bug, but this version removed unnecessary overhead for quick testing.

#include <stdio.h>
#include <string.h> /* for memset */
#include <stddef.h> /* for offsetof */
#include "dr_api.h"
#include "drmgr.h"
#include "drreg.h"
#include "drutil.h"
#include "utils.h"

reg_id_t tls_raw_reg;
uint tls_raw_base;
static size_t page_size;
static app_pc code_cache;

static void event_exit(void);
static dr_emit_flags_t
event_bb_insert(void *drcontext, void *tag, instrlist_t *bb, instr_t *instr,
        bool for_trace, bool translating, void *user_data);

static void
code_cache_init(void);
static void
code_cache_exit(void);

DR_EXPORT void dr_client_main(client_id_t id, int argc, const char *argv[]) {

    page_size = dr_page_size();
    drmgr_init();
    drutil_init();

    dr_register_exit_event(event_exit);
    if (!drmgr_register_bb_instrumentation_event(NULL, event_bb_insert, NULL)) {
        /* something is wrong: can't continue */
        DR_ASSERT(false);
        return;
    }

    dr_raw_tls_calloc(&(tls_raw_reg), &(tls_raw_base), 1, 0);
    code_cache_init();
}

static void event_exit() {

    dr_raw_tls_cfree(tls_raw_base, 1);

    code_cache_exit();

    if (!drmgr_unregister_bb_insertion_event(event_bb_insert))
        DR_ASSERT(false);

    drutil_exit();
    drmgr_exit();
}

int test_counter = 0;

static void clean_call(void) {
    test_counter++;
}

static void code_cache_init(void) {
    void *drcontext;
    instrlist_t *ilist;
    instr_t *where;
    byte *end;

    drcontext = dr_get_current_drcontext();
    code_cache = dr_nonheap_alloc(page_size,
    DR_MEMPROT_READ | DR_MEMPROT_WRITE | DR_MEMPROT_EXEC);
    ilist = instrlist_create(drcontext);

    opnd_t opnd1 = opnd_create_far_base_disp_ex(tls_raw_reg,
    REG_NULL, REG_NULL, 1, tls_raw_base + (0 * (sizeof(void *))), OPSZ_4, false,
    true, false);

    where = INSTR_CREATE_jmp_ind(drcontext, opnd1);
    instrlist_meta_append(ilist, where);
    /* clean call */
    dr_insert_clean_call(drcontext, ilist, where, (void *) clean_call, false,
            0);

    end = instrlist_encode(drcontext, ilist, code_cache, false);
    DR_ASSERT((size_t )(end - code_cache) < page_size);
    instrlist_clear_and_destroy(drcontext, ilist);
    dr_memory_protect(code_cache, page_size, DR_MEMPROT_READ | DR_MEMPROT_EXEC);
}

static void code_cache_exit(void) {
    dr_nonheap_free(code_cache, page_size);
}

static dr_emit_flags_t event_bb_insert(void *drcontext, void *tag,
        instrlist_t *bb, instr_t *where, bool for_trace, bool translating,
        void *user_data) {

    instr_t *instr, *restore;
    opnd_t opnd1, opnd2;
    restore = INSTR_CREATE_label(drcontext);

    /* mov restore DR_REG_XCX */
    opnd1 = opnd_create_far_base_disp_ex(tls_raw_reg,
    REG_NULL, REG_NULL, 1, tls_raw_base + (0 * (sizeof(void *))), OPSZ_4, false,
    true, false);
    opnd2 = opnd_create_instr(restore);
    instr = INSTR_CREATE_mov_imm(drcontext, opnd1, opnd2);
    instrlist_meta_preinsert(bb, where, instr);

    opnd1 = opnd_create_pc(code_cache);
    instr = INSTR_CREATE_jmp(drcontext, opnd1);
    instrlist_meta_preinsert(bb, where, instr);

    instrlist_meta_preinsert(bb, where, restore);

    return DR_EMIT_DEFAULT;
}

The code is fairly simple. For every instruction, the client jumps inside a shared private code cache created via _dr_nonheapalloc. The cache has a clean call inserted inside it.

The application under test needs to be multi-threaded. The bug fails on applications such as Apache. However, I recommend pigz for testings as it is quite a light-weight application and the bug is triggered in a matter of seconds.

I run the the client as follows:

/home/john/dynamorio/install/bin32/drrun -thread_private -disable_traces -opt_cleancall 2 -c libmemtrace_test.so -- pigz -k -d /home/john/generated.zip

Back-Trace: The back-trace does not provide much hints towards the root cause of the bug. It indicates that the bug is triggered in _monitor_cacheenter, but the function is unrelated. I had cases where the top function changes on multiple runs.

(gdb) bt
#0  0x7113268f in monitor_cache_enter (dcontext=0x0, f=0x458ee9f0)
    at /home/john/dynamorio/core/monitor.c:1891
#1  0x7108f181 in d_r_dispatch (dcontext=0x0)
    at /home/john/dynamorio/core/dispatch.c:197
#2  0x00000246 in ?? ()
(gdb) info registers
eax            0x0  0
ecx            0x4578a704   1165534980
edx            0xc  12
ebx            0x71365000   1899384832
esp            0x457ece90   0x457ece90
ebp            0x457ecf7c   0x457ecf7c
esi            0x71365000   1899384832
edi            0x4578a704   1165534980
eip            0x7113268f   0x7113268f <monitor_cache_enter+26>
eflags         0x10216  [ PF AF IF RF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x43 67
gs             0x33 51

I also analysed the debug logs but did not find anything suspicious at the crash point - the master signal is just triggered to handle the fault.

Root Cause: Eventually, I made some progress by inspecting code related to inserting clean calls, particularly the _prepare_for_callex and _cleanup_after_callex. These functions set the runtime usage of drcontext statically or dynamically depending whether caches are shared and whether the architecture is 64-bit. This check is done via _SCRATCH_ALWAYSTLS

# define SCRATCH_ALWAYS_TLS() (DYNAMO_OPTION(private_ib_in_tls))

The code essentially determines whether the _private_ib_intls parameter is set. Note, this parameter is enabled by default for shared caches and for 64-bit, but not when dealing with a 32-bit application and thread private caches are used.

Regarding the bug, when the clean call is inserted inside the private cache, an absolute address pertaining to the thread's drcontext is used. However, the private code cache is shared, and when another thread enters the cache, it's not dynamically loading its drcontext, but modifying the drcontext of some other thread.

The place where the absolute address is set is in the _dcontext_opndcommon function, where the the absolute parameter is passed as true.

dcontext_opnd_common(dcontext_t *dcontext, bool absolute, reg_id_t basereg, int offs,
                     opnd_size_t size)

Solutions: One quick solution is to document this limitation and tell the user to pass the _private_ib_intls runtime parameter when clean calls are in shared private caches - the client does not crash with this parameter. The second, perhaps more convoluted solution with regards to code changes, is to always use tls slots and dynamically load the drcontext, thus removing the _private_ib_intls parameter all together.

derekbruening commented 5 years ago

Thank you for the detailed writeup. To turn on -private_ib_in_tls for -thread_private we would want to double-check that there's no performance difference, and consciously decide to abandon !HAVE_TLS platforms (probably fine).