diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index d1a65ec7..e1094ec7 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -1,11 +1,20 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O3 -Wno-format") -add_library(Fastgen STATIC fastgen.cpp) -target_compile_options(Fastgen PRIVATE -stdlib=libc++) +# Fastgen backend (dfsan symbolic execution + out-of-process solving) +add_library(Fastgen STATIC fastgen.cpp solver_common.cpp) target_include_directories(Fastgen PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../runtime + ${CMAKE_CURRENT_SOURCE_DIR} ) -install (TARGETS Fastgen DESTINATION ${SYMSAN_LIB_DIR}) + +# Thoroupy backend (ucsan under-constrained execution + fastgen dfsan callbacks) +add_library(Thoroupy STATIC thoroupy.cpp fastgen.cpp solver_common.cpp) +target_include_directories(Thoroupy PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime + ${CMAKE_CURRENT_SOURCE_DIR} +) + +install (TARGETS Fastgen Thoroupy DESTINATION ${SYMSAN_LIB_DIR}) diff --git a/backend/fastgen.cpp b/backend/fastgen.cpp index abe3be7c..cafe2d2b 100644 --- a/backend/fastgen.cpp +++ b/backend/fastgen.cpp @@ -16,65 +16,7 @@ */ -#include "sanitizer_common/sanitizer_common.h" -#include "sanitizer_common/sanitizer_file.h" -#include "sanitizer_common/sanitizer_posix.h" -#include "dfsan/dfsan.h" - -using namespace __dfsan; - -static uint32_t __instance_id; -static uint32_t __session_id; -static int __pipe_fd; - -// filter? -SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint32_t __taint_trace_callstack; - -static inline void __solve_cond(dfsan_label label, uint8_t result, - uint8_t add_nested, uint8_t loop_flag, - uint32_t cid, void *addr) { - - if (__pipe_fd < 0) - return; - - uint16_t flags = 0; - if (add_nested) flags |= F_ADD_CONS; - - // set the loop flags according to branching results - switch (loop_flag) { - case TrueBranchLoopExit: - flags |= result ? F_LOOP_EXIT : F_LOOP_LATCH; - break; - case TrueBranchLoopLatch: - flags |= result ? F_LOOP_LATCH : F_LOOP_EXIT; - break; - case FalseBranchLoopExit: - flags |= result ? F_LOOP_LATCH : F_LOOP_EXIT; - break; - case FalseBranchLoopLatch: - flags |= result ? F_LOOP_EXIT : F_LOOP_LATCH; - break; - default: - // No loop flag or unrecognized flag, do nothing - break; - } - - // send info - pipe_msg msg = { - .msg_type = cond_type, - .flags = flags, - .instance_id = __instance_id, - .addr = (uptr)addr, - .context = __taint_trace_callstack, - .id = cid, - .label = label, - .result = result - }; - - if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { - Die(); - } -} +#include "solver_common.h" static inline void __send_ubi(dfsan_label label, uint64_t result, uint32_t cid, void *addr) { @@ -140,7 +82,7 @@ __taint_trace_cmp(dfsan_label op1, dfsan_label op2, uint32_t size, __switch_true_case.cid = cid; } else { // solve without add_nested - __solve_cond(temp, r, 0, 0, cid, addr); + __taint_send_cond(temp, r, 0, 0, cid, addr); } } @@ -160,7 +102,7 @@ __taint_trace_switch_end(uint32_t cid) { __switch_true_case.label, cid, addr); // solve the true case - __solve_cond(__switch_true_case.label, 1, 1, 0, cid, addr); + __taint_send_cond(__switch_true_case.label, 1, 1, 0, cid, addr); __switch_true_case.label = 0; } @@ -190,7 +132,7 @@ __taint_trace_cond(dfsan_label label, bool r, uint8_t flag, uint32_t cid) { uint8_t loop_flag = flag & LoopFlagMask; // always add nested - __solve_cond(label, r, add_nested, loop_flag, cid, addr); + __taint_send_cond(label, r, add_nested, loop_flag, cid, addr); } extern "C" SANITIZER_INTERFACE_ATTRIBUTE dfsan_label @@ -217,18 +159,18 @@ __taint_trace_select(dfsan_label cond_label, dfsan_label true_label, if (true_label != 0 && false_op == 0) { dfsan_label land = dfsan_union(cond_label, true_label, And, 1, r, true_op); uint8_t lr = (r && true_op) ? 1 : 0; - __solve_cond(land, lr, 1, 0, cid, addr); + __taint_send_cond(land, lr, 1, 0, cid, addr); return land; } else if (false_label != 0 && true_op == 1) { // logical OR: select cond, true, label dfsan_label lor = dfsan_union(cond_label, false_label, Or, 1, r, false_op); uint8_t lr = (r || false_op) ? 1 : 0; - __solve_cond(lor, lr, 1, 0, cid, addr); + __taint_send_cond(lor, lr, 1, 0, cid, addr); return lor; } else { // normal select? AOUT("normal select?!\n"); - __solve_cond(cond_label, r, 1, 0, cid, addr); + __taint_send_cond(cond_label, r, 1, 0, cid, addr); return r ? true_label : false_label; } } @@ -307,9 +249,109 @@ __taint_trace_gep(dfsan_label ptr_label, uint64_t ptr, extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_offset(dfsan_label offset_label, s64 offset, unsigned size) { + // use add_constraint_type to send offset constraints + if (offset_label == 0) + return; + + void *addr = __builtin_return_address(0); + + AOUT("tainted offset: %ld = %d, size: %u @%p\n", + offset, offset_label, size, addr); + + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = add_constraint_type, + .flags = 0, + .instance_id = __instance_id, + .addr = (uptr)addr, + .context = __taint_trace_callstack, + .label = offset_label, // just in case + .result = (uint64_t)offset + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + + return; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_add_constraint(dfsan_label label, uint8_t result) { + if (label == 0) + return; + + void *addr = __builtin_return_address(0); + + AOUT("tainted add_constraint: %d, result: %u @%p\n", label, result, addr); + + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = add_constraint_type, + .flags = 0, + .instance_id = __instance_id, + .addr = (uptr)addr, + .context = __taint_trace_callstack, + .label = label, + .result = (uint64_t)result + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + return; } +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_minimize_label(dfsan_label label, u64 size, dfsan_label bounds) { + if (label == 0 || label == kInitializingLabel) + return; + + void *addr = __builtin_return_address(0); + + AOUT("minimize label: %d, bounds: %d, size: %lu\n", label, bounds, size); + + if (bounds != 0) { + dfsan_label_info *bounds_info = get_label_info(bounds); + if (bounds_info->op == __dfsan::Alloca) { + AOUT("update size label from %d to %d\n", bounds_info->l2, label); + bounds_info->l2 = label; + } + } + + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = minimize_type, + .flags = 0, + .instance_id = __instance_id, + .addr = 0, + .context = __taint_trace_callstack, + .label = label, + .result = 0 + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + + if (!flags().allow_zero_size_alloc && size == 0) { + // Emit this after the minimize message so the manager records the hint + // before solving the synthetic nonzero condition. + static constexpr uint32_t kMinimizeNonzeroCid = 12; + dfsan_label_info *size_info = get_label_info(label); + dfsan_label nonzero_label = + dfsan_union(label, 0, (__dfsan::bvneq << 8) | ICmp, size_info->size, 0, 0); + __taint_send_cond(nonzero_label, 0, 1, 0, kMinimizeNonzeroCid, addr); + } +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_memcmp(dfsan_label label) { if (label == 0) @@ -403,8 +445,9 @@ __taint_trace_memerr(dfsan_label ptr_label, uptr ptr, dfsan_label size_label, } } -extern "C" void InitializeSolver() { +extern "C" void InitializeSymSanSolver() { __instance_id = flags().instance_id; __session_id = flags().session_id; __pipe_fd = flags().pipe_fd; + __control_pipe_fd = flags().control_pipe_fd; } diff --git a/backend/solver_common.cpp b/backend/solver_common.cpp new file mode 100644 index 00000000..c85b50f9 --- /dev/null +++ b/backend/solver_common.cpp @@ -0,0 +1,78 @@ +/* + Common code shared between fastgen and thoroupy solvers. + + ------------------------------------------------ + + Written by Chengyu Song and + Ju Chen + + Copyright 2021-2025 UC Riverside. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + */ + +#include "solver_common.h" + +//===----------------------------------------------------------------------===// +// Shared Global State +//===----------------------------------------------------------------------===// + +uint32_t __instance_id; +uint32_t __session_id; +int __pipe_fd; +int __control_pipe_fd; + +//===----------------------------------------------------------------------===// +// Shared Helper Functions +//===----------------------------------------------------------------------===// + +void __taint_send_cond(dfsan_label label, uint8_t result, + uint8_t add_nested, uint8_t loop_flag, + uint32_t cid, void *addr) { + + if (__pipe_fd < 0) + return; + + uint16_t flags = 0; + if (add_nested) flags |= F_ADD_CONS; + + // set the loop flags according to branching results + switch (loop_flag) { + case TrueBranchLoopExit: + flags |= result ? F_LOOP_EXIT : F_LOOP_LATCH; + break; + case TrueBranchLoopLatch: + flags |= result ? F_LOOP_LATCH : F_LOOP_EXIT; + break; + case FalseBranchLoopExit: + flags |= result ? F_LOOP_LATCH : F_LOOP_EXIT; + break; + case FalseBranchLoopLatch: + flags |= result ? F_LOOP_EXIT : F_LOOP_LATCH; + break; + default: + // No loop flag or unrecognized flag, do nothing + break; + } + + // send info + pipe_msg msg = { + .msg_type = cond_type, + .flags = flags, + .instance_id = __instance_id, + .addr = (uptr)addr, + .context = __taint_trace_callstack, + .id = cid, + .label = label, + .result = result + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } +} diff --git a/backend/solver_common.h b/backend/solver_common.h new file mode 100644 index 00000000..a1cc3f06 --- /dev/null +++ b/backend/solver_common.h @@ -0,0 +1,52 @@ +/* + Common code shared between fastgen and thoroupy solvers. + + ------------------------------------------------ + + Written by Chengyu Song and + Ju Chen + + Copyright 2021-2025 UC Riverside. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + */ + +#ifndef SOLVER_COMMON_H +#define SOLVER_COMMON_H + +#include "sanitizer_common/sanitizer_common.h" +#include "sanitizer_common/sanitizer_file.h" +#include "sanitizer_common/sanitizer_posix.h" +#include "dfsan/dfsan.h" + +using namespace __dfsan; + +//===----------------------------------------------------------------------===// +// Shared Global State +//===----------------------------------------------------------------------===// + +extern uint32_t __instance_id; +extern uint32_t __session_id; +extern int __pipe_fd; +extern int __control_pipe_fd; + +// filter, defined in dfsan.cpp +extern SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint32_t __taint_trace_callstack; + +//===----------------------------------------------------------------------===// +// Shared Helper Functions +//===----------------------------------------------------------------------===// + +// Note: get_const_result() is defined in dfsan.h + +// Send conditional branch info to solver +void __taint_send_cond(dfsan_label label, uint8_t result, + uint8_t add_nested, uint8_t loop_flag, + uint32_t cid, void *addr); + +#endif // SOLVER_COMMON_H diff --git a/backend/thoroupy.cpp b/backend/thoroupy.cpp new file mode 100644 index 00000000..b9624db9 --- /dev/null +++ b/backend/thoroupy.cpp @@ -0,0 +1,417 @@ +/* + The code is for out-of-process constraints solving with thoroupy. + + ------------------------------------------------ + + Written by Chengyu Song + Ju Chen and + Mingjun Yin + + Copyright 2021-2026 UC Riverside. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + */ + +#include "solver_common.h" +#include "dfsan/ucsan.h" +#include +#include +#include +#include +#include +#include +#include + +#define __handle_segfualt__ +#define LOOP_COUNTER_SIZE 1024 + +using namespace __dfsan; +using __ucsan::ucsan_tainted; +using __ucsan::ucsan_flags; + +struct ucsan_ticket { + u32 msg_type; + u32 instance_id; + u32 session_id; + u64 payload_size; + u8 payload[0]; +} __attribute__((packed)); + +static int __loop_threshold = 20; +int __stack_threshold = 20; +static uint32_t __loop_counter[LOOP_COUNTER_SIZE]; +static uint32_t __loop_bid[LOOP_COUNTER_SIZE]; +static int __current_loop_depth = 0; +static int __previous_loop_depth = 0; +static int __loop_depth_stack_current[LOOP_COUNTER_SIZE]; +static int __loop_depth_stack_base[LOOP_COUNTER_SIZE]; +static int __loop_depth_stack_previous[LOOP_COUNTER_SIZE]; +static int __loop_depth_stack_top = 0; +static int __base_loop_depth = 0; + +extern void* __ucsan_null_deref_flag; + +// based on https://github.com/Cyan4973/xxHash +// simplified since we only have 12 bytes info +static inline uint32_t xxhash(uint32_t h1, uint32_t h2, uint32_t h3) { + const uint32_t PRIME32_1 = 2654435761U; + const uint32_t PRIME32_2 = 2246822519U; + const uint32_t PRIME32_3 = 3266489917U; + const uint32_t PRIME32_4 = 668265263U; + const uint32_t PRIME32_5 = 374761393U; + + #define XXH_rotl32(x,r) ((x << r) | (x >> (32 - r))) + uint32_t h32 = PRIME32_5; + h32 += h1 * PRIME32_3; + h32 = XXH_rotl32(h32, 17) * PRIME32_4; + h32 += h2 * PRIME32_3; + h32 = XXH_rotl32(h32, 17) * PRIME32_4; + h32 += h3 * PRIME32_3; + h32 = XXH_rotl32(h32, 17) * PRIME32_4; + #undef XXH_rotl32 + + h32 ^= h32 >> 15; + h32 *= PRIME32_2; + h32 ^= h32 >> 13; + h32 *= PRIME32_3; + h32 ^= h32 >> 16; + + return h32; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_loop_push_stack() { + __loop_depth_stack_current[__loop_depth_stack_top] = __current_loop_depth; + __loop_depth_stack_base[__loop_depth_stack_top] = __base_loop_depth; + __loop_depth_stack_previous[__loop_depth_stack_top] = __previous_loop_depth; + __base_loop_depth += __current_loop_depth; + __current_loop_depth = 0; + __loop_depth_stack_top++; + AOUT("loop push stack: %u %u %u\n", __current_loop_depth, __base_loop_depth, __previous_loop_depth); +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_loop_pop_stack() { + __loop_depth_stack_top--; + __current_loop_depth = __loop_depth_stack_current[__loop_depth_stack_top]; + __base_loop_depth = __loop_depth_stack_base[__loop_depth_stack_top]; + __previous_loop_depth = __loop_depth_stack_previous[__loop_depth_stack_top]; + AOUT("loop pop stack: %u %u %u\n", __current_loop_depth, __base_loop_depth, __previous_loop_depth); +} + +#define get_nested_loop_depth() (__base_loop_depth + __current_loop_depth) + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_loop(uint32_t bid, int depth) { + void *addr = __builtin_return_address(0); + + if (ucsan_flags().disable_loop_bounds) { + return; + } + + AOUT("loop header: %u depth: %d current: %d base: %d previous: %d, top bid = %u, @%p\n", + bid, depth, __current_loop_depth, __base_loop_depth, __previous_loop_depth, + __loop_bid[get_nested_loop_depth()], addr); + + if (depth > 0) { + // loop header + if (bid == __loop_bid[get_nested_loop_depth()]) { + // same loop, increase the loop counter + __loop_counter[get_nested_loop_depth()]++; + AOUT("Increasing loop counter for loop %u, depth: %u, counter: %u\n", + bid, __current_loop_depth, __loop_counter[get_nested_loop_depth()]); + + if (__loop_counter[get_nested_loop_depth()] > __loop_threshold) { + AOUT("loop threshold reached, exiting\n"); + internal__exit(exit_reason::REASON_LOOP_OOB); + } + } else { + // different loop + if (depth + __base_loop_depth > __previous_loop_depth) { // enter a new loop + // assert(depth == __current_loop_depth + 1); + __current_loop_depth += 1; // update the current loop depth + __loop_counter[get_nested_loop_depth()] = 0; // reset the loop counter + __loop_bid[get_nested_loop_depth()] = bid; + + __previous_loop_depth = depth + __base_loop_depth; + AOUT("enter new loop: %u depth: %u\n", bid, __current_loop_depth); + } else { + AOUT("WARNING: loop depth %d is less than current loop depth %d\n", + depth + __base_loop_depth, __previous_loop_depth); + internal__exit(exit_reason::REASON_LOOP_OOB); + } + } + } else if (__current_loop_depth + depth >= 0) { + // exit a loop + if (bid == __loop_bid[get_nested_loop_depth()]) { + // exiting the current loop + // reset bid + for (int i = 0; i > depth; --i) { + __loop_bid[get_nested_loop_depth() + i] = 0; + __loop_counter[get_nested_loop_depth() + i] = 0; + } + __current_loop_depth += depth; + __previous_loop_depth += depth; + AOUT("exit to outer loop: %u depth: %u\n", bid, __current_loop_depth); + } else { + AOUT("WARNING: loop exit bid %u does not match current loop bid %u\n", + bid, __loop_bid[get_nested_loop_depth()]); + return; + } + } else { + return; // no loop to exit + } + + // send loop info + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = loop_type, + .flags = 0, + .instance_id = __instance_id, + .addr = (uptr)addr, + .context = __taint_trace_callstack, + .id = bid, + .label = 0, + .result = (uint64_t)get_nested_loop_depth() + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + + return; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_event_addr(dfsan_label label, uint32_t event_id, uint64_t info, + void* addr, uint32_t info2) { + AOUT("event: %u %u %llu @%p\n", label, event_id, info, addr); + + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = event_type, + .flags = 0, + .instance_id = __instance_id, + .addr = (uptr)addr, + .context = event_id, + .id = info2, + .label = label, + .result = info + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + + return; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_bb(uint32_t function_index, uint32_t bb_index) { + AOUT("bb: %u %llu\n", function_index, bb_index); + + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = bb_type, + .addr = (uint64_t)__builtin_return_address(0), + .id = function_index, + .result = bb_index, + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + + return; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_global_var(uint32_t obj_id, uint64_t offset, uint64_t size, void *gv) { + AOUT("global var: obj_id=%u, offset=%lu, size: %lu @%p\n", obj_id, offset, size, gv); + + if (__pipe_fd < 0) + return; + + pipe_msg msg = { + .msg_type = gv_type, + .flags = 0, + .instance_id = __instance_id, + .addr = offset, + .context = __taint_trace_callstack, + .id = obj_id, + .label = 0, + .result = size + }; + + if (internal_write(__pipe_fd, &msg, sizeof(msg)) < 0) { + Die(); + } + // FIXME: assuming single writer so msg will arrive in the same order + if (internal_write(__pipe_fd, gv, size) < 0) { + Die(); + } + + return; +} + +static void segfault_handler(int sig, siginfo_t *si, void *unused) +{ + if (__ucsan_null_deref_flag) { + AOUT("Poential Null dereference detected\n"); + __taint_trace_event_addr(0, EVENT_NULL_DEREF, 0, __ucsan_null_deref_flag, 0); + internal__exit(EVENT_NULL_DEREF); + } + AOUT("Segmentation fault at address: %p, access type: %d\n", si->si_addr, si->si_code); + Die(); +} + +static void fpe_handler(int sig, siginfo_t *si, void *unused) +{ + void *addr = si->si_addr; + if (si->si_code == FPE_INTDIV) { + AOUT("Integer division by zero at address: %p\n", addr); + __taint_trace_event_addr(0, EVENT_DIV_BY_ZERO, 0, addr, 0); + internal__exit(EVENT_DIV_BY_ZERO); + } else if (si->si_code == FPE_INTOVF) { + AOUT("Integer overflow at address: %p\n", addr); + __taint_trace_event_addr(0, EVENT_INT_OVERFLOW, 0, addr, 0); + internal__exit(EVENT_INT_OVERFLOW); + } + AOUT("Floating point exception at address: %p, code: %d\n", addr, si->si_code); + Die(); +} + +void RegisterSegFault () { +#ifdef __handle_segfualt__ + struct sigaction sa; + + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + sa.sa_sigaction = segfault_handler; + if (sigaction(SIGSEGV, &sa, NULL) == -1) + Die(); + sa.sa_sigaction = fpe_handler; + if (sigaction(SIGFPE, &sa, NULL) == -1) + Die(); +#endif +} + +extern "C" void InitializeUCSanSolver() { + + RegisterSegFault(); + + // initialize pipe fds + if (internal_strcmp(flags().pipe_name, "") != 0) { + __pipe_fd = internal_open(flags().pipe_name, O_WRONLY); + } else { + __pipe_fd = flags().pipe_fd; + } + // communication pipe fd can be -1, meaning no out-of-process solving + // tracing only + + if (internal_strcmp(flags().control_pipe_name, "") != 0) { + __control_pipe_fd = internal_open(flags().control_pipe_name, O_RDONLY); + } else { + __control_pipe_fd = flags().control_pipe_fd; + } + // control pipe fd must be valid + if (__control_pipe_fd == -1) { + Printf("FATAL: control_fd not set, control_pipe_fd %d, control_fd_name %s\n", + flags().control_pipe_fd, flags().control_pipe_name); + Die(); + } + Printf("Thouroupy solver fork server pipe_fd: %d\n", __control_pipe_fd); + + ucsan_ticket ticket; + uint64_t payload_size; + char *content; + uptr ret; + memset(__loop_counter, 0, LOOP_COUNTER_SIZE * sizeof(uint32_t)); + + // fork server loop + while (true) { + ret = read(__control_pipe_fd, &ticket, sizeof(ucsan_ticket)); + if (ret != sizeof(ucsan_ticket)) { + Printf("Fork server: read ticket failed, exiting\n"); + internal__exit(1); + } + + switch (ticket.msg_type){ // exit + case 0: { + Printf("Fork server: exiting\n"); + internal__exit(0); + } + break; + + case 1: + case 2: { + AOUT("Fork server: received a new session, session id: %d, instance id: %d\n", + ticket.session_id, ticket.instance_id); + + int pid = fork(); + if (pid == 0) { // child + __instance_id = ticket.instance_id; + __session_id = ticket.session_id; + + if (ticket.msg_type == 1) return; // no payload + payload_size = ticket.payload_size; + + content = (char*)malloc(payload_size); + if (!content) internal__exit(1); + + internal_read(__control_pipe_fd, content, payload_size); + // free previous allocation if any + if (ucsan_tainted.buf) free((void*)ucsan_tainted.buf); + + // parse the input content + ucsan_tainted.load(content, payload_size); + + return; // jump out to the main function + } else if (pid > 0) { // parent + AOUT("Fork server: Wait for the child process\n"); + int status; + waitpid(pid, &status, 0); + pipe_msg msg = { + .msg_type = exit_type, + .flags = 0, + .instance_id = __instance_id, + .addr = 0, + .context = 0, + .label = 0, + .result = (uint64_t)status + }; + AOUT("Fork server: report exit status: %d\n", status); + internal_write(__pipe_fd, &msg, sizeof(msg)); + } else { + AOUT("Fork server: fork failed, exiting\n"); + internal__exit(1); + } + } + break; + + case 3: { // adjust loop threshold + internal_read(__control_pipe_fd, &__loop_threshold, sizeof(uint32_t)); + AOUT("Fork server: received loop threshold: %d\n", __loop_threshold); + } + break; + + case 4: { // adjust stack threshold + internal_read(__control_pipe_fd, &__stack_threshold, sizeof(uint32_t)); + AOUT("Fork server: received stack threshold: %d\n", __stack_threshold); + } + break; + } + } + +} diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 90c18925..adb282f2 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -5,3 +5,4 @@ add_custom_command(TARGET KOClang POST_BUILD COMMAND ln -sf "ko-clang" "ko-clang++") install (TARGETS KOClang DESTINATION ${SYMSAN_BIN_DIR}) install (FILES ${CMAKE_CURRENT_BINARY_DIR}/ko-clang++ DESTINATION ${SYMSAN_BIN_DIR}) +install (PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/ucsan_opt DESTINATION ${SYMSAN_BIN_DIR}) diff --git a/compiler/ko_clang.c b/compiler/ko_clang.c index 428223a3..0efbae0c 100644 --- a/compiler/ko_clang.c +++ b/compiler/ko_clang.c @@ -34,11 +34,18 @@ static char *obj_path; /* Path to runtime libraries */ static char *taint_path; /* Path to the taint pass */ +static char *ucsan_path; /* Path to the ucsan pass */ static char **cc_params; /* Parameters passed to the real CC */ static u32 cc_par_cnt = 1; /* Param count, including argv0 */ static u8 is_cxx = 0; static u8 use_native_cxx = 0; static u8 use_native_zlib = 1; /* Use system zlib by default */ +static u8 use_ucsan = 0; +static u8 use_z3_runtime = 0; +static u8 use_fastgen = 0; +static u8 use_thoroupy = 0; +static u8 use_ucsan_only = 0; +static u8 use_symsan_only = 0; /* Try to find the executable from PATH */ static char *find_executable_in_path(const char *filename) { @@ -113,6 +120,10 @@ static void find_obj(const char *argv0) { } else { FATAL("Unable to find 'TaintPass.so' at %s", path); } + ucsan_path = alloc_printf("%s/../lib/symsan/UCSanPass.so", dir); + if (access(ucsan_path, R_OK)) { + FATAL("Unable to find 'UCSanPass.so' at %s", path); + } ck_free(dir); } @@ -124,19 +135,29 @@ static void check_type(char *name) { } } -static u8 check_if_assembler(u32 argc, char **argv) { - /* Check if a file with an assembler extension ("s" or "S") appears in argv */ +static u8 should_skip_instrumentation(u32 argc, char **argv) { + /* + * Skip instrumentation if (1) doing assembler: + * a file with an assembler extension ("s" or "S") appears in argv + * or (2) no source file is present (linking only) + */ + u8 has_source_file = 0; while (--argc) { const char *cur = *(++argv); const char *ext = strrchr(cur, '.'); - if (ext && (!strcmp(ext + 1, "s") || !strcmp(ext + 1, "S"))) { - return 1; + if (ext) { + if (!strcmp(ext + 1, "s") || !strcmp(ext + 1, "S")) { + return 1; + } else if (!strcmp(ext + 1, "c") || !strcmp(ext + 1, "cc") || + !strcmp(ext + 1, "cpp") || !strcmp(ext + 1, "cxx")) { + has_source_file = 1; + } } } - return 0; + return has_source_file ? 0 : 1; } static void add_runtime() { @@ -144,24 +165,54 @@ static void add_runtime() { cc_params[cc_par_cnt++] = alloc_printf("-L%s", getenv("KO_LIBRARY_PATH")); } + // Select runtime library based on environment variables: + // - KO_USE_UCSAN_ONLY: standalone UCSan (under-constrained execution only) + // - KO_USE_SYMSAN_ONLY: standalone SymSan (symbolic execution only) + // - has METADATA and !KO_USE_UCSAN_ONLY: combined UCSan+SymSan + // - Default: standalone SymSan + + u8 use_both = use_ucsan && !use_ucsan_only; + + const char *runtime_lib; + if (use_ucsan_only) { + runtime_lib = "libucsan_rt-x86_64.a"; + } else if (use_symsan_only) { + runtime_lib = "libsymsan_rt-x86_64.a"; + } else if (use_both) { + runtime_lib = "libdfsan_rt-x86_64.a"; + } else { + runtime_lib = "libsymsan_rt-x86_64.a"; + } + cc_params[cc_par_cnt++] = "-Wl,--whole-archive"; - cc_params[cc_par_cnt++] = alloc_printf("%s/libdfsan_rt-x86_64.a", obj_path); + cc_params[cc_par_cnt++] = alloc_printf("%s/%s", obj_path, runtime_lib); cc_params[cc_par_cnt++] = "-Wl,--no-whole-archive"; cc_params[cc_par_cnt++] = alloc_printf("-Wl,--dynamic-list=%s/libdfsan_rt-x86_64.a.syms", obj_path); cc_params[cc_par_cnt++] = alloc_printf("-Wl,-T%s/taint.ld", obj_path); - if (is_cxx && !use_native_cxx) { - // cc_params[cc_par_cnt++] = "-Wl,--whole-archive"; + if (is_cxx && use_ucsan_only) { + // UCSan-only: link the plain (uninstrumented) EH runtime so C++ exception + // handling resolves to the real __cxa_*/personality/unwinder symbols that + // the UCSanPass EH passthrough calls. The taint-instrumented libc++abi + // would only export ".taint"-mangled versions, so it cannot satisfy them. + // All of libc++ (the STL) is out-of-scope and dangled, so it is not linked. + cc_params[cc_par_cnt++] = alloc_printf("%s/libc++abi-native.a", obj_path); + cc_params[cc_par_cnt++] = alloc_printf("%s/libunwind-native.a", obj_path); + } else if (is_cxx && !use_native_cxx) { + // Instrumented static libc++ for C++ builds. The EH subsystem inside + // libc++abi/libunwind is kept concrete via the abilist (the unwinder and + // __cxa_*/personality functions are marked uninstrumented) so exceptions + // unwind correctly while the STL stays instrumented for taint tracking. cc_params[cc_par_cnt++] = alloc_printf("%s/libc++.a", obj_path); cc_params[cc_par_cnt++] = alloc_printf("%s/libc++abi.a", obj_path); cc_params[cc_par_cnt++] = alloc_printf("%s/libunwind.a", obj_path); - // cc_params[cc_par_cnt++] = "-Wl,--no-whole-archive"; } else { - cc_params[cc_par_cnt++] = "-lc++"; - cc_params[cc_par_cnt++] = "-lc++abi"; - cc_params[cc_par_cnt++] = "-l:libunwind.so"; + // System static libc++ for C builds (avoids shared lib dependency) + cc_params[cc_par_cnt++] = "-l:libc++.a"; + cc_params[cc_par_cnt++] = "-l:libc++abi.a"; + cc_params[cc_par_cnt++] = "-l:libunwind.a"; } cc_params[cc_par_cnt++] = "-lrt"; @@ -175,24 +226,27 @@ static void add_runtime() { cc_params[cc_par_cnt++] = "-lz"; } - if (getenv("KO_USE_Z3")) { + if (use_fastgen) { + cc_params[cc_par_cnt++] = "-Wl,--whole-archive"; + cc_params[cc_par_cnt++] = alloc_printf("%s/libFastgen.a", obj_path); + cc_params[cc_par_cnt++] = "-Wl,--no-whole-archive"; + } else if (use_thoroupy) { + cc_params[cc_par_cnt++] = "-Wl,--whole-archive"; + cc_params[cc_par_cnt++] = alloc_printf("%s/libThoroupy.a", obj_path); + cc_params[cc_par_cnt++] = "-Wl,--no-whole-archive"; + } else if (use_z3_runtime) { cc_params[cc_par_cnt++] = "-Wl,--whole-archive"; cc_params[cc_par_cnt++] = alloc_printf("%s/libZ3Solver.a", obj_path); cc_params[cc_par_cnt++] = "-Wl,--no-whole-archive"; - cc_params[cc_par_cnt++] = "-L/usr/local/lib"; + cc_params[cc_par_cnt++] = "-L/usr/local/lib"; // prefer local cc_params[cc_par_cnt++] = "-lz3"; + cc_params[cc_par_cnt++] = "-lc++"; + cc_params[cc_par_cnt++] = "-lc++abi"; cc_params[cc_par_cnt++] = "-Wl,-rpath,/usr/local/lib"; } - - if (getenv("KO_USE_FASTGEN")) { - cc_params[cc_par_cnt++] = "-Wl,--whole-archive"; - cc_params[cc_par_cnt++] = alloc_printf("%s/libFastgen.a", obj_path); - cc_params[cc_par_cnt++] = "-Wl,--no-whole-archive"; - } } static void add_taint_pass() { - cc_params[cc_par_cnt++] = "-fexperimental-new-pass-manager"; cc_params[cc_par_cnt++] = alloc_printf("-fplugin=%s", taint_path); // to enable options cc_params[cc_par_cnt++] = alloc_printf("-fpass-plugin=%s", taint_path); cc_params[cc_par_cnt++] = "-mllvm"; @@ -220,6 +274,11 @@ static void add_taint_pass() { cc_params[cc_par_cnt++] = "-taint-solve-ub=true"; } + if (use_ucsan) { + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = "-taint-with-ucsan=true"; + } + if (is_cxx && use_native_cxx) { cc_params[cc_par_cnt++] = "-mllvm"; cc_params[cc_par_cnt++] = @@ -227,10 +286,46 @@ static void add_taint_pass() { } } +static void add_ucsan_pass() { + cc_params[cc_par_cnt++] = alloc_printf("-fplugin=%s", ucsan_path); // to enable options + cc_params[cc_par_cnt++] = alloc_printf("-fpass-plugin=%s", ucsan_path); + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = + alloc_printf("-ucsan-abilist=%s/ucsan_abilist.txt", obj_path); + + if (getenv("KO_NO_TRACE_BOUND")) { + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = "-ucsan-trace-bound=false"; + } + + if (getenv("KO_TRACE_BB")) { + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = "-ucsan-trace-bb=true"; + } + + const char *uscan_cfg = getenv("KO_DUMP_CFG"); + if (uscan_cfg) { + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = alloc_printf("-ucsan-dump-cfg=%s", uscan_cfg); + } + + const char *type_table = getenv("KO_TYPE_TABLE"); + if (type_table) { + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = alloc_printf("-ucsan-type-table=%s", type_table); + } + + if (!use_ucsan_only) { + // TaintPass will run after UCSanPass + cc_params[cc_par_cnt++] = "-mllvm"; + cc_params[cc_par_cnt++] = "-ucsan-with-taint=true"; + } +} + static void edit_params(u32 argc, char **argv) { u8 fortify_set = 0, asan_set = 0, x_set = 0, maybe_linking = 1, bit_mode = 0; - u8 maybe_assembler = 0; + u8 skip_instrumentation = 0; char *name; cc_params = ck_alloc((argc + 128) * sizeof(char *)); @@ -250,11 +345,7 @@ static void edit_params(u32 argc, char **argv) { cc_params[0] = alt_cc ? alt_cc : "clang"; } - maybe_assembler = check_if_assembler(argc, argv); - - use_native_cxx = getenv("KO_USE_NATIVE_LIBCXX") ? 1 : 0; - - use_native_zlib = getenv("KO_NO_NATIVE_ZLIB") ? 0 : 1; + skip_instrumentation = should_skip_instrumentation(argc, argv); /* Detect stray -v calls from ./configure scripts. */ if (argc == 1 && !strcmp(argv[1], "-v")) @@ -328,8 +419,40 @@ static void edit_params(u32 argc, char **argv) { return; } - if (!maybe_assembler) { - add_taint_pass(); + use_native_cxx = getenv("KO_USE_NATIVE_LIBCXX") ? 1 : 0; + + use_native_zlib = getenv("KO_NO_NATIVE_ZLIB") ? 0 : 1; + + use_ucsan = getenv("METADATA") ? 1 : 0; + + use_ucsan_only = getenv("KO_USE_UCSAN_ONLY") ? 1 : 0; + use_symsan_only = getenv("KO_USE_SYMSAN_ONLY") ? 1 : 0; + + use_z3_runtime = getenv("KO_USE_Z3") ? 1 : 0; + use_fastgen = getenv("KO_USE_FASTGEN") ? 1 : 0; + use_thoroupy = getenv("KO_USE_THOROUPY") ? 1 : 0; + + // sanity checks + if (use_ucsan_only && !use_ucsan) { + FATAL("KO_USE_UCSAN_ONLY requires METADATA to be set"); + } + if (use_ucsan_only && use_symsan_only) { + FATAL("KO_USE_UCSAN_ONLY and KO_USE_SYMSAN_ONLY cannot be set together"); + } + if (use_symsan_only && use_ucsan) { + FATAL("KO_USE_SYMSAN_ONLY cannot be set together with METADATA"); + } + + if (!skip_instrumentation) { + cc_params[cc_par_cnt++] = "-fexperimental-new-pass-manager"; + // add UCSanPass first, if specified + if (use_ucsan) { + add_ucsan_pass(); + } + // Then add TaintPass + if (!use_ucsan_only) { + add_taint_pass(); + } } cc_params[cc_par_cnt++] = "-pie"; @@ -363,7 +486,6 @@ static void edit_params(u32 argc, char **argv) { if (!getenv("KO_DONT_OPTIMIZE")) { cc_params[cc_par_cnt++] = "-g"; cc_params[cc_par_cnt++] = "-O3"; - cc_params[cc_par_cnt++] = "-funroll-loops"; } if (is_cxx && !use_native_cxx) { @@ -374,6 +496,22 @@ static void edit_params(u32 argc, char **argv) { if (maybe_linking) { + // sanity checks + if (use_ucsan) { + if (use_z3_runtime) { + FATAL("KO_USE_Z3 cannot be set together with METADATA"); + } else if (use_fastgen) { + FATAL("KO_USE_FASTGEN cannot be set together with METADATA"); + } + } else if (use_thoroupy) { + FATAL("Thoroupy runtime requires METADATA to be set"); + } + + if (use_z3_runtime + use_fastgen + use_thoroupy > 1) { + FATAL("Multiple symbolic execution runtimes selected: z3=%d, fastgen=%d, thoroupy=%d", + use_z3_runtime, use_fastgen, use_thoroupy); + } + if (x_set) { cc_params[cc_par_cnt++] = "-x"; cc_params[cc_par_cnt++] = "none"; diff --git a/compiler/ucsan_opt b/compiler/ucsan_opt new file mode 100755 index 00000000..261e4b7d --- /dev/null +++ b/compiler/ucsan_opt @@ -0,0 +1,154 @@ +#!/usr/bin/python3 + +import os +import argparse +import yaml +__path__ = os.path.dirname(os.path.abspath(__file__)) +path_to_install = os.path.abspath(os.path.join(__path__, "..")) + +ucsan_pass_path = f"{path_to_install}/lib/symsan/UCSanPass.so" +if os.path.exists(ucsan_pass_path): + pass +else: + print(f"Cannot find {ucsan_pass_path}, please make sure you have run this script in the right directory(the directory ko-clang installed)") + os._exit(1) + +KO_FLAGS = ["KO_CC=clang-14"] +ko_flags = ' '.join(KO_FLAGS) + +# Base opt-14 command: load UCSan pass +# Load library twice for LLVM-14: +# -load: registers cl::opt definitions (command-line options) +# -load-pass-plugin: registers the new pass manager plugin +cc = f"opt-14 -load {path_to_install}/lib/symsan/UCSanPass.so" \ + f" -load-pass-plugin={path_to_install}/lib/symsan/UCSanPass.so" \ + f" -ucsan-abilist={path_to_install}/lib/symsan/ucsan_abilist.txt" + +ko_cc = f"{path_to_install}/bin/ko-clang" + +argparser = argparse.ArgumentParser() + +argparser.add_argument('-m', '--metadata', help="Path to the yaml that describe analysis scope" ,type=str, required=True) +argparser.add_argument('-o', '--output', help="Path to the output file" ,type=str, default='a.ucsan') +argparser.add_argument('-O', '--optimization', action="store_true", help="Enable optimization",default=False) +argparser.add_argument('-b', '--tracebb', action="store_true", help="Enable basic block tracing",default=False) +argparser.add_argument('-t', '--taint', action="store_true", help="Enable TaintPass (combined UCSan+SymSan)",default=False) +argparser.add_argument('-i', '--indirect-call', action="store_true", help="Enable indirect call wrapping",default=False) +argparser.add_argument('-c', '--clean', action="store_false", help="Clean up the temporary files",default=True) +argparser.add_argument('-r', '--resign-ptrargs', action="store_true", help="Resign pointer arguments",default=False) +argparser.add_argument('--type-table', type=str, default=None, help="Path to emit the typeid->type table (UCSanPass -ucsan-type-table)") +argparser.add_argument('-v', '--verbose', action="count", help="Verbose level",default=0) +argparser.add_argument('-s', '--cov', type=str, help="Path to the coverage file", default=None) +argparser.add_argument('files', action="extend", help="Path to llvm bitcode files to analyse. Note that you may add 'files' in the metadata to specify target" ,type=str, nargs='*') + +log_on = lambda level, msg: print(msg) if args.verbose >= level else None + +args = argparser.parse_args() + +metadata = args.metadata +files = args.files +output = args.output + +KO_FLAGS.append(f"METADATA={metadata}.tmp") + +yaml_file = open(metadata, 'r') +metadata = yaml.load(yaml_file, Loader=yaml.FullLoader) +yaml_file.close() +if 'files' in metadata: + files.extend(metadata['files']) + metadata.pop('files') +log_on(1, f"Files: {files}") +log_on(2, f"Writing metadata to {args.metadata}.tmp") +open(f"{args.metadata}.tmp", 'w').write(yaml.dump(metadata)) +target = os.path.basename(args.metadata).replace(".yaml", "") + +if not files: + argparser.print_usage() + os._exit(1) + +# Note: With LLVM-14 new pass manager, we don't add -O# flags to opt +# Optimization is controlled via KO_DONT_OPTIMIZE and handled by ko-clang +if not args.optimization: + KO_FLAGS.append("KO_DONT_OPTIMIZE=1") + +if args.tracebb: + KO_FLAGS.append("KO_TRACE_BB=1") + cc += " -ucsan-trace-bb=true" + +if args.type_table: + cc += f" -ucsan-type-table={args.type_table}" + +if args.indirect_call: + KO_FLAGS.append("KO_WRAP_INDIRECT_CALL=1") + +if args.resign_ptrargs: + KO_FLAGS.append("KO_RESIGN_PTRARGS=1") + +if args.cov: + os.remove(args.cov) if os.path.exists(args.cov) else None + KO_FLAGS.append(f"KO_COV={args.cov}") + +# Build the pass pipeline: UCSan first, then optionally TaintPass +# This mirrors ko-clang's order (add_ucsan_pass then add_taint_pass) +if args.taint: + KO_FLAGS.append("KO_USE_THOROUPY=1") + taint_pass_path = f"{path_to_install}/lib/symsan/TaintPass.so" + if not os.path.exists(taint_pass_path): + print(f"Cannot find {taint_pass_path}") + os._exit(1) + # Tell UCSanPass that TaintPass will run after it + cc += f" -ucsan-with-taint=true" + # Load TaintPass library for cl::opt and pass registration + cc += f" -load {taint_pass_path}" \ + f" -load-pass-plugin={taint_pass_path}" \ + f" -taint-abilist={path_to_install}/lib/symsan/dfsan_abilist.txt" \ + f" -taint-with-ucsan=true" \ + f" -taint-solve-ub=true" + # Pipeline: ucsan then taint + cc += " -passes=ucsan,taint" +else: + # UCSan only + cc += " -passes=ucsan" + +cc += " -S -disable-verify" + +tmp_files = [f"{args.metadata}.tmp"] +obj_files = [] +def command(cmd): + log_on(1, f"Running command: {cmd}") + return os.system(cmd) + +def cleanup(): + if not args.clean: + print("Temporary files are not cleaned up due to the option") + print("Temporary files:\n\t" + '\n\t'.join(tmp_files)) + return + for file in tmp_files + obj_files: + try: + os.remove(file) + except: + pass + +for file in files: + bc_file = f"{file}-{target}.ucsan.s" + obj_file = f"{file}-{target}.ucsan.o" + ret = command(f"{' '.join(KO_FLAGS)} {cc} {file} -o {bc_file}") + if ret != 0: + print(f"Failed to compile {file}, check the error message above") + cleanup() + os._exit(1) + tmp_files.append(bc_file) + ret = command(f"llc-14 -filetype=obj --relocation-model=pic -o {obj_file} {bc_file}") + if ret != 0: + print(f"Failed to compile {file}, check the error message above") + cleanup() + os._exit(1) + obj_files.append(obj_file) + +ret = command(f"{' '.join(KO_FLAGS)} {ko_cc} {' '.join(obj_files)} -o {output}") +if ret != 0: + print(f"Failed to compile {output}, check the error message above") + cleanup() + os._exit(1) + +cleanup() diff --git a/driver/CMakeLists.txt b/driver/CMakeLists.txt index a3b22674..0a2b4c07 100644 --- a/driver/CMakeLists.txt +++ b/driver/CMakeLists.txt @@ -24,6 +24,7 @@ endif() install (CODE "MESSAGE(STATUS \"Build & Install: libSymsanProxy.o\")") install (CODE "execute_process(COMMAND \ + ${CMAKE_COMMAND} -E env KO_CC=${LLVM_TOOLS_BINARY_DIR}/clang \ ${CMAKE_INSTALL_PREFIX}/${SYMSAN_BIN_DIR}/ko-clang \ -c ${CMAKE_CURRENT_SOURCE_DIR}/harness-proxy.c \ -o ${CMAKE_INSTALL_PREFIX}/${SYMSAN_LIB_DIR}/libSymsanProxy.o)") diff --git a/driver/aflpp/symsan.cpp b/driver/aflpp/symsan.cpp index 51a1e1c3..9496853a 100644 --- a/driver/aflpp/symsan.cpp +++ b/driver/aflpp/symsan.cpp @@ -505,7 +505,7 @@ extern "C" u32 afl_custom_fuzz_count(my_mutator_t *data, const u8 *buf, data->parser->record_memcmp(msg.label, mmsg->content, msg.result); free(mmsg); break; - case fsize_type: + case add_constraint_type: break; case memerr_type: WARNF("Memory error detected @%p, type = %d\n", (void*)msg.addr, msg.flags); diff --git a/driver/fgtest.cpp b/driver/fgtest.cpp index 9f31a056..510f5a15 100644 --- a/driver/fgtest.cpp +++ b/driver/fgtest.cpp @@ -341,7 +341,14 @@ int main(int argc, char* const argv[]) { __z3_parser->record_memcmp(msg.label, mmsg->content, msg.result); free(mmsg); break; - case fsize_type: + case add_constraint_type: + if (__z3_parser->add_constraints(msg.label, msg.result) != 0) { + fprintf(stderr, "Failed to add constraint %d = %lu @%p\n", + msg.label, msg.result, (void*)msg.addr); + } + break; + case minimize_type: + __z3_parser->record_minimize(msg.label); break; default: break; diff --git a/include/parse-rgd.h b/include/parse-rgd.h index dd01b52d..bb3845f6 100644 --- a/include/parse-rgd.h +++ b/include/parse-rgd.h @@ -17,7 +17,7 @@ class RGDAstParser : public symsan::ASTParser { solve_nested_(solve_nested), max_ast_size_(max_ast_size) {} ~RGDAstParser() {} - int restart(std::vector &inputs) override; + int restart(std::vector &inputs, bool copy_input = false) override; int parse_cond(dfsan_label label, bool result, bool add_nested, std::vector &tasks) override; int parse_gep(dfsan_label ptr_label, uptr ptr, diff --git a/include/parse-z3.h b/include/parse-z3.h index 46c3e118..fcc8bfbb 100644 --- a/include/parse-z3.h +++ b/include/parse-z3.h @@ -3,6 +3,7 @@ #include "parse.h" #include +#include namespace symsan { @@ -19,7 +20,9 @@ class Z3AstParser : public ASTParser { } } - int restart(std::vector &inputs) override; + int restart(std::vector &inputs, bool copy_input = false) override; + /// @brief Update input cache without clearing deps + int update_input(std::vector &inputs, bool copy_input = false); int parse_cond(dfsan_label label, bool result, bool add_nested, std::vector &tasks) override; int parse_gep(dfsan_label ptr_label, uptr ptr, @@ -29,15 +32,59 @@ class Z3AstParser : public ASTParser { std::vector &tasks) override; int add_constraints(dfsan_label label, uint64_t result) override; + int record_minimize(dfsan_label label, bool allow_zero = true) override; protected: z3::context &context_; const char* input_name_format; const char* atoi_name_format; const char* strlen_name_format; + const char* str_name_format; + const char* int_name_format; + + // Auxiliary constraints generated during serialization (e.g., Int variable bounds) + std::vector aux_constraints_; + + // Expressions to minimize during solving (e.g., malloc sizes) + // Each entry: (expr to minimize, set of input offsets it depends on) + struct minimize_hint_t { + z3::expr expr; + bool allow_zero; // whether to allow zero as a valid solution + std::unordered_set deps; + }; + std::vector minimize_hints_; + + // String range entry with cached str- expr for linking constraints + struct string_range_t { + uint32_t start; + uint32_t end; + z3::expr str_expr; // cached str-X-Y-Z expr (z3::expr handles refcount) + + string_range_t(uint32_t s, uint32_t e, z3::expr expr) + : start(s), end(e), str_expr(expr) {} + }; + + // Transparent comparator: order by (start, end) so ranges with the same + // start but different lengths coexist. Heterogeneous lookup by uint32_t + // still works for upper_bound (compares against start only). + struct string_range_cmp { + using is_transparent = void; // Enable heterogeneous lookup + + bool operator()(const string_range_t &a, const string_range_t &b) const { + if (a.start != b.start) return a.start < b.start; + return a.end < b.end; + } + bool operator()(const string_range_t &a, uint32_t offset) const { + return a.start < offset; + } + bool operator()(uint32_t offset, const string_range_t &b) const { + return offset < b.start; + } + }; - // String ranges for null-byte post-processing (input_id -> list of (start, end)) - std::unordered_map>> string_ranges_; + // String ranges for null-byte post-processing and linking constraints + // vector indexed by input_id, each contains a sorted set of ranges + std::vector> string_ranges_; // String info cache: label -> (input_id, offset, length) struct string_info_t { @@ -48,9 +95,12 @@ class Z3AstParser : public ASTParser { std::unordered_map string_info_cache_; private: - // Original input cache + // Original input cache (stores pointers to input data) std::vector inputs_cache_; + // Copied input data (when copy_input=true, owns the data) + std::vector> inputs_copy_; + // fsize flag bool has_fsize; @@ -64,6 +114,10 @@ class Z3AstParser : public ASTParser { std::vector value_cache_; static const size_t SIZE_INCREMENT = 2048; + // Label-level tracking: what type of variables does each expression involve? + std::vector is_label_bv_; // involves bitvec variables (input-X-Y) + std::vector is_label_seq_; // involves string/seq variables (str-X-Y-Z) + // dependencies struct expr_hash { std::size_t operator()(const z3::expr &expr) const { @@ -76,20 +130,76 @@ class Z3AstParser : public ASTParser { } }; using expr_set_t = std::unordered_set; - struct branch_dependency { + // Comparison info stored for Int mirroring of BV nested constraints + struct cmp_info_t { + dfsan_label l1; // left operand label + dfsan_label l2; // right operand label + uint16_t predicate; // comparison predicate (e.g., bvsle) + bool result; // concrete result (true/false) + }; + + struct branch_dependency_t { expr_set_t expr_deps; input_dep_set_t input_deps; + bool used_in_bv = false; // any saved constraint involves bitvec + bool used_in_seq = false; // any saved constraint involves string/seq + z3::expr input_expr; // cached input-X-Y expr (z3::expr handles refcount) + std::vector cmp_deps; // ICmp constraints for Int mirroring + + // Only constructor: must have input_expr (linear scan guarantees this) + branch_dependency_t(z3::expr e) : input_expr(e) {} }; - using branch_dep_t = std::unique_ptr; + + // Cache of int-* variables: label -> Int z3 expr + // Populated when int-* variables are created in convert_bv_to_int or fsubstr handler + std::unordered_map int_var_cache_; + using branch_dep_t = std::unique_ptr; using offset_dep_t = std::vector; std::vector branch_deps_; + // Separate storage for negative offsets (container_of pattern). + // Negative offset -N (encoded as uint32_t > INT32_MAX) maps to index N-1. + std::vector neg_branch_deps_; - inline struct branch_dependency* get_branch_dep(offset_t off) { + static inline bool is_negative_offset(uint32_t off) { + return (int32_t)off < 0; + } + + static inline uint32_t neg_index(uint32_t off) { + return (uint32_t)(-(int32_t)off) - 1; + } + + inline struct branch_dependency_t* get_branch_dep(offset_t off) { + if (is_negative_offset(off.second)) { + if (off.first >= neg_branch_deps_.size()) { + return nullptr; + } + auto &deps = neg_branch_deps_.at(off.first); + if (neg_index(off.second) >= deps.size()) { + return nullptr; + } + return deps.at(neg_index(off.second)).get(); + } + if (off.first >= branch_deps_.size()) { + return nullptr; + } auto &offset_deps = branch_deps_.at(off.first); + if (off.second >= offset_deps.size()) { + return nullptr; + } return offset_deps.at(off.second).get(); } inline void set_branch_dep(offset_t off, branch_dep_t dep) { + if (is_negative_offset(off.second)) { + if (off.first >= neg_branch_deps_.size()) + neg_branch_deps_.resize(off.first + 1); + auto &deps = neg_branch_deps_[off.first]; + uint32_t idx = neg_index(off.second); + if (idx >= deps.size()) + deps.resize(idx + 1); + deps[idx] = std::move(dep); + return; + } auto &offset_deps = branch_deps_.at(off.first); if (off.second >= offset_deps.size()) { offset_deps.resize(off.second + 1); @@ -124,7 +234,10 @@ class Z3AstParser : public ASTParser { z3::expr read_concrete(dfsan_label label, uint16_t size); z3::expr serialize(dfsan_label label, input_dep_set_t &deps); + uint64_t serialize_input(dfsan_label label, uint32_t input, uint32_t offset, + uint32_t bytes, input_dep_set_t &input_deps); inline void collect_more_deps(input_dep_set_t &deps); + inline void mark_expr_type(dfsan_label label, input_dep_set_t &inputs); inline size_t add_nested_constraints(input_dep_set_t &deps, z3_task_t *task); inline void save_constraint(z3::expr expr, input_dep_set_t &inputs); void construct_index_tasks(z3::expr &index, uint64_t curr, @@ -135,6 +248,18 @@ class Z3AstParser : public ASTParser { z3::expr build_string_from_label(dfsan_label content_label, input_dep_set_t &deps); z3::expr get_byte_expr(uint32_t input, uint32_t offset, input_dep_set_t &deps); bool label_contains_indexof(dfsan_label label); + + // Helper for linking bitvec and string constraints on shared offsets + void add_string_bitvec_link(offset_t off, z3_task_t *task); + + // Register a string range and add linking constraints for overlapping ranges + void register_string_range(uint32_t input, uint32_t start, uint32_t end, + z3::expr str_var); + + // Constrain a (UC) string-search haystack base pointer to be non-null. The + // pointer label is carried in the high bits of a string op's op2. No-op for + // concrete/bounds (Alloca/Free) pointers. Pulls the pointer bytes into deps. + void add_haystack_ptr_nonnull(dfsan_label ptr_label, input_dep_set_t &deps); }; class Z3ParserSolver : public Z3AstParser { @@ -154,7 +279,7 @@ class Z3ParserSolver : public Z3AstParser { struct solution_val { solution_op_t op; uint32_t id; // input id - uint32_t offset; // position in file + int32_t offset; // position in file (signed for container_of negative offsets) union { uint8_t val; // for SET: the byte value uint32_t len; // for DELETE: number of bytes to delete @@ -163,15 +288,15 @@ class Z3ParserSolver : public Z3AstParser { // Constructors for convenience // SET: set single byte at offset - solution_val(uint32_t id, uint32_t offset, uint8_t val) + solution_val(uint32_t id, int32_t offset, uint8_t val) : op(solution_op_t::SET), id(id), offset(offset), val(val) {} // INSERT: insert bytes at offset - solution_val(uint32_t id, uint32_t offset, std::vector data) + solution_val(uint32_t id, int32_t offset, std::vector data) : op(solution_op_t::INSERT), id(id), offset(offset), data(std::move(data)) {} // DELETE: delete len bytes at offset - solution_val(solution_op_t op, uint32_t id, uint32_t offset, uint32_t len) + solution_val(solution_op_t op, uint32_t id, int32_t offset, uint32_t len) : op(op), id(id), offset(offset), len(len) {} }; @@ -189,6 +314,12 @@ class Z3ParserSolver : public Z3AstParser { using solution_t = std::vector; solving_status solve_task(uint64_t task_id, unsigned timeout, solution_t &solutions); + /// @brief Export task constraints to SMT2 format + /// @param task_id the task to export + /// @param fd file descriptor to write to + /// @return 0 on success, -1 on failure + int export_task_smt2(uint64_t task_id, int fd); + private: void generate_solution(z3::model &m, solution_t &solutions); diff --git a/include/parse.h b/include/parse.h index 98d595fd..ea5c0704 100644 --- a/include/parse.h +++ b/include/parse.h @@ -37,8 +37,9 @@ class ASTParser { prev_task_id_(0) {} virtual ~ASTParser() {} - virtual int restart(std::vector &inputs) { + virtual int restart(std::vector &inputs, bool copy_input = false) { (void)inputs; + (void)copy_input; memcmp_cache_.clear(); return 0; } @@ -73,6 +74,15 @@ class ASTParser { /// @return 0 on success, -1 on failure virtual int add_constraints(dfsan_label label, uint64_t result) = 0; + /// @brief Record a label to minimize during solving (e.g., malloc size) + /// @param label symbolic label to minimize + /// @param allow_zero whether to allow zero as a valid solution + /// @return 0 on success, -1 on failure + virtual int record_minimize(dfsan_label label, bool allow_zero) { + (void)label; (void)allow_zero; + return 0; + } + virtual int record_memcmp(dfsan_label label, uint8_t* buf, size_t size) { auto content = std::make_unique(size); memcpy(content.get(), buf, size); diff --git a/instrumentation/CMakeLists.txt b/instrumentation/CMakeLists.txt index ece12498..2afdd0a5 100644 --- a/instrumentation/CMakeLists.txt +++ b/instrumentation/CMakeLists.txt @@ -8,5 +8,16 @@ if(APPLE) endif(APPLE) include(AddLLVM) + +# Declare source files as optional to avoid LLVM CMake validation errors +set(LLVM_OPTIONAL_SOURCES + TaintPass.cpp + UCSanPass.cpp + LoopOutlinePass.cpp +) + add_llvm_pass_plugin(TaintPass TaintPass.cpp) -install (TARGETS TaintPass DESTINATION ${SYMSAN_LIB_DIR}) +add_llvm_pass_plugin(UCSanPass UCSanPass.cpp) +add_llvm_pass_plugin(LoopOutlinePass LoopOutlinePass.cpp) + +install (TARGETS TaintPass UCSanPass LoopOutlinePass DESTINATION ${SYMSAN_LIB_DIR}) diff --git a/instrumentation/LoopOutlinePass.cpp b/instrumentation/LoopOutlinePass.cpp new file mode 100644 index 00000000..cb47bb98 --- /dev/null +++ b/instrumentation/LoopOutlinePass.cpp @@ -0,0 +1,366 @@ +//===- LoopOutlinePass.cpp - outline one loop iteration -------------------===// +// +// New-PM module pass that outlines the *body* of each innermost loop (one +// iteration) into a standalone function, so the validation oracle can check a +// loop invariant inductively: `{I & guard} body {I}`. +// +// Approach (at -O0, where loop-carried state is in allocas, no header PHIs): +// take ALL of a loop's blocks (header included, so the guard and its variables +// like the bound `n` are in scope), but first REDIRECT the back-edge +// (latch -> header) to a fresh unreachable sink. The region is then acyclic, +// single-entry (header), and runs exactly ONE iteration before "returning" +// (the redirected back-edge and the loop exits all become region exits). +// CodeExtractor turns it into: +// __ucsan_loopbody__(&i, &n, &p, ...) -> exit-selector +// The loop-carried state crosses as alloca-pointer args (memory model), which +// ucsan symbolizes (= free havoc); the harness assumes I & guard on the loaded +// values, calls the body, and asserts I on the (memory-updated) values. +// +// Redirecting the back-edge destroys the original loop — fine, this transform +// is for the VALIDATION-only build (the oracle targets the outlined body as +// entry; the parent's correctness is irrelevant there). +// +// First cut: innermost loops only. Anything CodeExtractor declines (isEligible +// / null) is skipped — graceful degrade, never a miscompile (verifyModule gates). +// +//===----------------------------------------------------------------------===// + +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Analysis/LoopInfo.h" +#include "llvm/IR/BasicBlock.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/Dominators.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/InstrTypes.h" +#include "llvm/IR/Instructions.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/DebugInfoMetadata.h" +#include "llvm/IR/IntrinsicInst.h" +#include "llvm/IR/PassManager.h" +#include "llvm/IR/Verifier.h" +#include "llvm/Passes/PassBuilder.h" +#include "llvm/Passes/PassPlugin.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Transforms/Utils/CodeExtractor.h" + +#include +#include + +#define DEBUG_TYPE "loop-outline" + +using namespace llvm; + +static cl::opt ClVerbose( + "loop-outline-verbose", + cl::desc("Print loop-outline decisions"), cl::init(false), cl::Hidden); + +// Path to write the arg→source-var sidecar JSON. The outlined function's args +// are alloca POINTERS with numbered IR names; source names + types live only in +// debug info. The oracle reads this to build the invariant harness. +static cl::opt ClSidecar( + "loop-outline-sidecar", + cl::desc("Write arg→source-var map JSON to this path"), cl::init("")); + +// Render a DIType as a C type string (best-effort; common cases). Resolves +// const/volatile/typedef, pointers, basic types, structs. +static std::string diTypeToC(const DIType *T) { + if (!T) + return "void"; + if (auto *B = dyn_cast(T)) + return B->getName().empty() ? "int" : B->getName().str(); + if (auto *D = dyn_cast(T)) { + switch (D->getTag()) { + case dwarf::DW_TAG_pointer_type: + return diTypeToC(D->getBaseType()) + " *"; + case dwarf::DW_TAG_const_type: + case dwarf::DW_TAG_volatile_type: + case dwarf::DW_TAG_typedef: + case dwarf::DW_TAG_restrict_type: + return diTypeToC(D->getBaseType()); + default: + return "void"; + } + } + if (auto *C = dyn_cast(T)) { + if (C->getTag() == dwarf::DW_TAG_array_type) + return diTypeToC(C->getBaseType()) + " *"; // array decays to pointer + if (!C->getName().empty()) + return ("struct " + C->getName()).str(); + } + return "void"; +} + +namespace { + +// A loop selected for extraction (gathered before any mutation; BasicBlock* +// stay valid because innermost loops are disjoint). +struct Target { + Function *F; + BasicBlock *Header; + std::vector Blocks; // ALL loop blocks (header included) + SmallVector Latches; // back-edge sources (latch -> header) +}; + +static void gatherInnermost(Loop *L, Function &F, + std::vector &Out) { + if (!L->getSubLoops().empty()) { + for (Loop *Sub : *L) + gatherInnermost(Sub, F, Out); + return; // first cut: innermost only (nested handled bottom-up later) + } + Target T; + T.F = &F; + T.Header = L->getHeader(); + for (BasicBlock *BB : L->blocks()) + T.Blocks.push_back(BB); + L->getLoopLatches(T.Latches); + if (T.Blocks.empty() || T.Latches.empty()) + return; + Out.push_back(std::move(T)); +} + +// First debug line of a block (0 if none) — used to key the outlined body back +// to the source loop the contract's invariant references. +static unsigned dbgLine(BasicBlock *BB) { + for (Instruction &I : *BB) + if (const DebugLoc &DL = I.getDebugLoc()) + return DL.getLine(); + return 0; +} + +// C type for an outlined ARG: like diTypeToC but PRESERVES arrays as +// "elem [N]" (the outlined arg of a stack array is pointer-to-array, so the +// harness must declare `elem (*p)[N]` and materialize N*sizeof(elem), not decay +// it to a pointer). +static std::string diArgCType(const DIType *T) { + if (auto *C = dyn_cast_or_null(T)) { + if (C->getTag() == dwarf::DW_TAG_array_type) { + int64_t n = 0; + for (DINode *E : C->getElements()) { + if (auto *sr = dyn_cast(E)) { + if (auto *ci = sr->getCount().dyn_cast()) + n = ci->getSExtValue(); + break; + } + } + return diTypeToC(C->getBaseType()) + " [" + std::to_string(n) + "]"; + } + } + return diTypeToC(T); +} + +// Map each outlined-function arg to its source var name + C type, via the +// llvm.dbg.declare of the alloca passed in at the (single) call site. +struct ArgInfo { + unsigned idx; + std::string var; + std::string ctype; +}; +static std::vector buildArgMap(Function *Out, Function *F) { + DenseMap Dbg; + for (BasicBlock &BB : *F) + for (Instruction &I : BB) + if (auto *DDI = dyn_cast(&I)) + if (DDI->getAddress() && DDI->getVariable()) + Dbg[DDI->getAddress()] = DDI->getVariable(); + + CallBase *CB = nullptr; + for (User *U : Out->users()) + if ((CB = dyn_cast(U))) + break; + std::vector Out2; + if (!CB) + return Out2; + for (unsigned i = 0, e = CB->arg_size(); i < e; ++i) { + const Value *A = CB->getArgOperand(i)->stripPointerCasts(); + ArgInfo AI{i, "", ""}; + auto It = Dbg.find(A); + if (It != Dbg.end()) { + AI.var = It->second->getName().str(); + AI.ctype = diArgCType(It->second->getType()); + } + Out2.push_back(std::move(AI)); + } + return Out2; +} + +// C declarator for a global the harness must `extern`-declare (arrays keep +// `[]`, since `int a[]` and `int *a` are different symbols at link). +static std::string diGlobalDecl(StringRef name, const DIType *T) { + if (auto *C = dyn_cast_or_null(T)) + if (C->getTag() == dwarf::DW_TAG_array_type) + return diTypeToC(C->getBaseType()) + " " + name.str() + "[]"; + return diTypeToC(T) + " " + name.str(); +} + +// Globals referenced by the outlined body (name, C extern declarator). These +// are NOT outlined args (module-level), so the harness needs them to render +// invariants that mention a global (e.g. a loop filling a global array). +static std::vector> +collectGlobals(Function *Out) { + SetVector gvs; + for (BasicBlock &BB : *Out) + for (Instruction &I : BB) + for (Value *Op : I.operands()) + if (auto *gv = dyn_cast(Op->stripPointerCasts())) + if (!gv->getName().startswith("llvm.") && !gv->getName().empty()) + gvs.insert(gv); + std::vector> out; + for (GlobalVariable *gv : gvs) { + SmallVector dbgs; + gv->getDebugInfo(dbgs); + if (dbgs.empty()) + continue; // no debug info ⇒ compiler-internal (string literals, etc.) + const DIType *ty = dbgs[0]->getVariable()->getType(); + out.push_back({gv->getName().str(), diGlobalDecl(gv->getName(), ty)}); + } + return out; +} + +static Function *extractTarget(const Target &T, unsigned Idx) { + // Redirect each back-edge (latch -> header) to a fresh unreachable sink so + // the loop region becomes acyclic = exactly one iteration. (Validation-only + // transform; the original loop is intentionally destroyed.) + BasicBlock *Sink = BasicBlock::Create( + T.F->getContext(), "__oneiter_sink", T.F); + new UnreachableInst(T.F->getContext(), Sink); + for (BasicBlock *Latch : T.Latches) { + Instruction *Term = Latch->getTerminator(); + for (unsigned i = 0, e = Term->getNumSuccessors(); i < e; ++i) + if (Term->getSuccessor(i) == T.Header) + Term->setSuccessor(i, Sink); + } + + // Fresh DominatorTree after the CFG edit. + DominatorTree DT; + DT.recalculate(*T.F); + + CodeExtractorAnalysisCache CEAC(*T.F); + CodeExtractor CE(T.Blocks, &DT, /*AggregateArgs=*/false, + /*BFI=*/nullptr, /*BPI=*/nullptr, /*AC=*/nullptr, + /*AllowVarArgs=*/false, /*AllowAlloca=*/false, + /*Suffix=*/""); + if (!CE.isEligible()) { + if (ClVerbose) + errs() << "[loop-outline] skip " << T.F->getName() + << " loop#" << Idx << ": not eligible\n"; + return nullptr; + } + Function *Out = CE.extractCodeRegion(CEAC); + if (!Out) { + if (ClVerbose) + errs() << "[loop-outline] skip " << T.F->getName() + << " loop#" << Idx << ": extract failed\n"; + return nullptr; + } + Out->setName("__ucsan_loopbody_" + T.F->getName().str() + "_" + + std::to_string(Idx)); + // Keep it as a standalone, externally-visible symbol so METADATA `entry:` can + // target it and the inliner cannot fold it away. + Out->addFnAttr(Attribute::NoInline); + Out->setLinkage(GlobalValue::ExternalLinkage); + if (ClVerbose) + errs() << "[loop-outline] outlined " << Out->getName() << " (" + << Out->arg_size() << " args)\n"; + return Out; +} + +struct LoopOutlinePass : public PassInfoMixin { + PreservedAnalyses run(Module &M, ModuleAnalysisManager &) { + // Phase 1 (read-only): gather innermost-loop body regions. + std::vector Targets; + for (Function &F : M) { + if (F.isDeclaration() || F.getName().startswith("__ucsan_loopbody_")) + continue; + DominatorTree DT; + DT.recalculate(F); + LoopInfo LI; + LI.analyze(DT); + for (Loop *L : LI) + gatherInnermost(L, F, Targets); + } + if (Targets.empty()) + return PreservedAnalyses::all(); + + // Phase 2 (mutate): extract each region. Per-function counter for names. + std::string Sidecar; // JSON array, built incrementally + bool Changed = false; + Function *Prev = nullptr; + unsigned Idx = 0; + for (const Target &T : Targets) { + if (T.F != Prev) { + Prev = T.F; + Idx = 0; + } + unsigned HdrLine = dbgLine(T.Header); // capture before extraction moves it + Function *Out = extractTarget(T, Idx++); + if (!Out) + continue; + Changed = true; + // Sidecar entry: {fn, header_line, args:[{idx,var,ctype}]}. + if (!Sidecar.empty()) + Sidecar += ",\n"; + Sidecar += " {\"fn\": \"" + Out->getName().str() + + "\", \"header_line\": " + std::to_string(HdrLine) + + ", \"args\": ["; + bool first = true; + for (const ArgInfo &AI : buildArgMap(Out, T.F)) { + if (!first) + Sidecar += ", "; + first = false; + Sidecar += "{\"idx\": " + std::to_string(AI.idx) + ", \"var\": \"" + + AI.var + "\", \"ctype\": \"" + AI.ctype + "\"}"; + } + Sidecar += "], \"globals\": ["; + bool gfirst = true; + for (const auto &G : collectGlobals(Out)) { + if (!gfirst) + Sidecar += ", "; + gfirst = false; + Sidecar += "{\"name\": \"" + G.first + "\", \"decl\": \"" + + G.second + "\"}"; + } + Sidecar += "]}"; + } + + if (Changed && verifyModule(M, &errs())) { + // Should not happen for eligible regions; loud if it does. + errs() << "[loop-outline] ERROR: verifyModule failed after outlining\n"; + } + + if (!ClSidecar.empty()) { + std::error_code EC; + raw_fd_ostream OS(ClSidecar, EC, sys::fs::OF_Text); + if (EC) + errs() << "[loop-outline] cannot write sidecar " << ClSidecar + << ": " << EC.message() << "\n"; + else + OS << "[\n" << Sidecar << "\n]\n"; + } + return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); + } + + static bool isRequired() { return true; } +}; + +} // namespace + +extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK +llvmGetPassPluginInfo() { + return {LLVM_PLUGIN_API_VERSION, "LoopOutlinePass", "v0.1", + [](PassBuilder &PB) { + PB.registerPipelineParsingCallback( + [](StringRef Name, ModulePassManager &MPM, + ArrayRef) { + if (Name == "loop-outline") { + MPM.addPass(LoopOutlinePass()); + return true; + } + return false; + }); + }}; +} diff --git a/instrumentation/TaintPass.cpp b/instrumentation/TaintPass.cpp index 12a2929f..2ae406dc 100644 --- a/instrumentation/TaintPass.cpp +++ b/instrumentation/TaintPass.cpp @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// //#include "defs.h" +#include "UCSanSummary.h" #include "version.h" #include "llvm/ADT/DenseMap.h" @@ -25,10 +26,17 @@ #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Triple.h" #include "llvm/ADT/iterator.h" +#include "llvm/Analysis/AssumptionCache.h" +#include "llvm/Analysis/LoopInfo.h" +#include "llvm/Analysis/ScalarEvolution.h" +#include "llvm/Analysis/ScalarEvolutionExpressions.h" +#include "llvm/Analysis/TargetLibraryInfo.h" #include "llvm/Analysis/ValueTracking.h" +#include "llvm/Transforms/Utils/ScalarEvolutionExpander.h" #include "llvm/IR/Argument.h" #include "llvm/IR/Attributes.h" #include "llvm/IR/BasicBlock.h" +#include "llvm/IR/CFG.h" #include "llvm/IR/Constant.h" #include "llvm/IR/Constants.h" #include "llvm/IR/DataLayout.h" @@ -174,6 +182,12 @@ static cl::opt ClTraceBound( cl::desc("Trace buffer bound info."), cl::Hidden, cl::init(true)); +// SYMSAN specific flags, hoist bounds checks out of loops using SCEV +static cl::opt ClHoistBoundsChecks( + "taint-hoist-bounds-checks", + cl::desc("Hoist bounds checks out of loops using SCEV analysis."), + cl::Hidden, cl::init(true)); + // SYMSAN specific flags, enable generating solving tasks for undefined behaviour static cl::opt ClSolveUB( "taint-solve-ub", @@ -186,6 +200,12 @@ static cl::opt ClTraceAnnotatedBB( cl::desc("Only trace annotated basic blocks."), cl::Hidden, cl::init(false)); +// SYMSAN specific flags, if runs with UCSan +static cl::opt ClWithUCSan( + "taint-with-ucsan", + cl::desc("Performs under-constrained symbolic execution."), + cl::Hidden, cl::init(false)); + static StringRef getGlobalTypeString(const GlobalValue &G) { // Types of GlobalVariables are always pointer types. Type *GType = G.getValueType(); @@ -387,6 +407,7 @@ class Taint { FunctionType *TaintUnionFnTy; FunctionType *TaintUnionLoadFnTy; FunctionType *TaintUnionStoreFnTy; + FunctionType *TaintGEPOffsetFnTy; FunctionType *TaintUnimplementedFnTy; FunctionType *TaintSetLabelFnTy; FunctionType *TaintNonzeroLabelFnTy; @@ -398,19 +419,20 @@ class Taint { FunctionType *TaintTraceSelectFnTy; FunctionType *TaintTraceIndirectCallFnTy; FunctionType *TaintTraceGEPFnTy; - FunctionType *TaintTraceGEPPtrFnTy; FunctionType *TaintPushStackFrameFnTy; FunctionType *TaintPopStackFrameFnTy; FunctionType *TaintTraceAllocaFnTy; FunctionType *TaintCheckBoundsFnTy; FunctionType *TaintSolveBoundsFnTy; FunctionType *TaintSolveSizeFnTy; + FunctionType *TaintSolveStrBoundsFnTy; FunctionType *TaintTraceGlobalFnTy; FunctionType *TaintDebugFnTy; + FunctionType *TaintMinimizeLabelFnTy; FunctionCallee TaintUnionFn; - FunctionCallee TaintCheckedUnionFn; FunctionCallee TaintUnionLoadFn; FunctionCallee TaintUnionStoreFn; + FunctionCallee TaintGEPOffsetFn; FunctionCallee TaintUnimplementedFn; FunctionCallee TaintSetLabelFn; FunctionCallee TaintNonzeroLabelFn; @@ -422,15 +444,16 @@ class Taint { FunctionCallee TaintTraceSelectFn; FunctionCallee TaintTraceIndirectCallFn; FunctionCallee TaintTraceGEPFn; - FunctionCallee TaintTraceGEPPtrFn; FunctionCallee TaintPushStackFrameFn; FunctionCallee TaintPopStackFrameFn; FunctionCallee TaintTraceAllocaFn; FunctionCallee TaintCheckBoundsFn; FunctionCallee TaintSolveBoundsFn; FunctionCallee TaintSolveSizeFn; + FunctionCallee TaintSolveStrBoundsFn; FunctionCallee TaintTraceGlobalFn; FunctionCallee TaintDebugFn; + FunctionCallee TaintMinimizeLabelFn; SmallPtrSet TaintRuntimeFunctions; Constant *CallStack; MDNode *ColdCallWeights; @@ -562,6 +585,11 @@ struct TaintFunction { Value *getShadow(Value *V); void setShadow(Instruction *I, Value *Shadow); + /// Handle nosanitize __dfsw_* calls from UCSan: + /// emit minimize hints for alloc size args and load retval TLS for non-void. + /// Returns true if handled. + bool handleUCSanCall(CallInst *CI, Instruction *Next); + /// Returns the shadow value of a global variable GV. Value *getShadowForGlobal(GlobalVariable *GV, IRBuilder<> &IRB); @@ -579,6 +607,7 @@ struct TaintFunction { Value *visitAllocaInst(AllocaInst *I, Value *ArraySize, Type *ElTy); void checkBounds(Value *Ptr, Value *Size, Instruction *Pos); void solveBounds(Value *Ptr, Value *Size, Instruction *Pos); + void hoistBoundsChecks(); /// XXX: because we never collapse taint labels for aggregate types, /// we also do not expand taint labels from an aggreated primitive @@ -608,12 +637,12 @@ struct TaintFunction { private: /// Loads a primitive shadow label - Value *loadPrimitiveShadow(Value *Addr, uint64_t Size, uint64_t Align, - IRBuilder<> &IRB); + Value *loadPrimitiveShadow(Value *Addr, uint64_t Size, uint64_t SizeInBits, + uint64_t Align, IRBuilder<> &IRB); /// Loads shadow recursively for aggregate types - void loadShadowRecursive(Value *Shadow, SmallVector &Indices, - Type *SubTy, Value *Addr, uint64_t Size, - uint64_t Align, IRBuilder<> &IRB); + Value *loadShadowRecursive(Value *Shadow, SmallVector &Indices, + Type *SubTy, Value *Addr, uint64_t Size, + uint64_t Align, IRBuilder<> &IRB); /// Stores an aggregate shadow label void storeShadowRecursive(Value *Shadow, SmallVector &Indices, Type *SubShadowTy, Value *ShadowAddr, uint64_t Size, @@ -824,8 +853,14 @@ Type *Taint::getShadowTy(Value *V) { } uint32_t Taint::getInstructionId(Instruction *Inst) { - // check if there is a bbid annotation - if (MDNode *BBID = Inst->getMetadata("bbid")) { + // check if there is a bbid annotation from UCSan ("dfsan.bb") + MDNode *BBID = Inst->getMetadata("dfsan.bb"); + // For non-terminator instructions, try getting bbid from the block's terminator + if (!BBID && !Inst->isTerminator()) { + Instruction *Term = Inst->getParent()->getTerminator(); + BBID = Term->getMetadata("dfsan.bb"); + } + if (BBID) { auto C = dyn_cast(BBID->getOperand(0)); if (ConstantInt *CI = dyn_cast(C->getValue())) { uint64_t BBIDValue = CI->getZExtValue(); @@ -950,13 +985,17 @@ bool Taint::initializeModule(Module &M) { Int16Ty, Int16Ty, Int64Ty, Int64Ty}; TaintUnionFnTy = FunctionType::get( PrimitiveShadowTy, TaintUnionArgs, /*isVarArg=*/ false); - Type *TaintUnionLoadArgs[3] = { PrimitiveShadowPtrTy, IntptrTy, Int64Ty }; + Type *TaintUnionLoadArgs[4] = { PrimitiveShadowPtrTy, IntptrTy, Int64Ty, Int64Ty }; TaintUnionLoadFnTy = FunctionType::get( PrimitiveShadowTy, TaintUnionLoadArgs, /*isVarArg=*/ false); + // args: shadow_ptr, n (bytes), size_in_bits, align Type *TaintUnionStoreArgs[4] = { PrimitiveShadowTy, PrimitiveShadowPtrTy, IntptrTy, Int64Ty }; TaintUnionStoreFnTy = FunctionType::get( Type::getVoidTy(*Ctx), TaintUnionStoreArgs, /*isVarArg=*/ false); + TaintGEPOffsetFnTy = FunctionType::get( + PrimitiveShadowTy, + { PrimitiveShadowTy, VoidPtrTy, VoidPtrTy }, /*isVarArg=*/ false); TaintUnimplementedFnTy = FunctionType::get( Type::getVoidTy(*Ctx), Type::getInt8PtrTy(*Ctx), /*isVarArg=*/false); Type *TaintSetLabelArgs[3] = { PrimitiveShadowTy, Type::getInt8PtrTy(*Ctx), @@ -989,9 +1028,6 @@ bool Taint::initializeModule(Module &M) { Int64Ty, Int64Ty, Int64Ty, Int64Ty, Int32Ty }; TaintTraceGEPFnTy = FunctionType::get( Type::getVoidTy(*Ctx), TaintTraceGEPArgs, false); - // __taint_trace_gep_ptr(base_label, offset) -> new_label - TaintTraceGEPPtrFnTy = FunctionType::get( - Type::getVoidTy(*Ctx), { PrimitiveShadowTy, VoidPtrTy, VoidPtrTy }, false); TaintPushStackFrameFnTy = FunctionType::get( Type::getVoidTy(*Ctx), {}, false); TaintPopStackFrameFnTy = FunctionType::get( @@ -1008,6 +1044,10 @@ bool Taint::initializeModule(Module &M) { TaintSolveSizeFnTy = FunctionType::get( Type::getVoidTy(*Ctx), { PrimitiveShadowTy, Int64Ty, PrimitiveShadowTy, Int64Ty, Int32Ty }, false); + // __taint_solve_str_bounds(str_ptr, buf_label, buf_ptr, step) + TaintSolveStrBoundsFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + { Type::getInt8PtrTy(*Ctx), PrimitiveShadowTy, Int64Ty, Int64Ty }, false); TaintTraceGlobalFnTy = FunctionType::get( PrimitiveShadowTy, { Int64Ty, Int64Ty }, false); @@ -1015,6 +1055,9 @@ bool Taint::initializeModule(Module &M) { {PrimitiveShadowTy, PrimitiveShadowTy, PrimitiveShadowTy, PrimitiveShadowTy, PrimitiveShadowTy}, false); + TaintMinimizeLabelFnTy = FunctionType::get(Type::getVoidTy(*Ctx), + { PrimitiveShadowTy, Int64Ty, PrimitiveShadowTy }, false); + ColdCallWeights = MDBuilder(*Ctx).createBranchWeights(1, 1000); return true; } @@ -1170,15 +1213,6 @@ void Taint::initializeRuntimeFunctions(Module &M) { TaintUnionFn = Mod->getOrInsertFunction("__taint_union", TaintUnionFnTy, AL); } - { - AttributeList AL; - AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); - AL = AL.addRetAttribute(M.getContext(), Attribute::ZExt); - AL = AL.addParamAttribute(M.getContext(), 0, Attribute::ZExt); - AL = AL.addParamAttribute(M.getContext(), 1, Attribute::ZExt); - TaintCheckedUnionFn = - Mod->getOrInsertFunction("taint_union", TaintUnionFnTy, AL); - } { AttributeList AL; AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); @@ -1193,6 +1227,14 @@ void Taint::initializeRuntimeFunctions(Module &M) { TaintUnionStoreFn = Mod->getOrInsertFunction("__taint_union_store", TaintUnionStoreFnTy, AL); } + { + AttributeList AL; + AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); + AL = AL.addRetAttribute(M.getContext(), Attribute::ZExt); + AL = AL.addParamAttribute(M.getContext(), 0, Attribute::ZExt); + TaintGEPOffsetFn = + Mod->getOrInsertFunction("__taint_gep_offset", TaintGEPOffsetFnTy, AL); + } { TaintUnimplementedFn = Mod->getOrInsertFunction("__dfsan_unimplemented", TaintUnimplementedFnTy); @@ -1215,25 +1257,34 @@ void Taint::initializeRuntimeFunctions(Module &M) { TaintDebugFn = Mod->getOrInsertFunction("__taint_debug", TaintDebugFnTy); } + { + AttributeList AL; + AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); + AL = AL.addParamAttribute(M.getContext(), 0, Attribute::ZExt); + TaintMinimizeLabelFn = + Mod->getOrInsertFunction("__taint_minimize_label", TaintMinimizeLabelFnTy, AL); + } TaintRuntimeFunctions.insert( TaintUnionFn.getCallee()->stripPointerCasts()); - TaintRuntimeFunctions.insert( - TaintCheckedUnionFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintUnionLoadFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintUnionStoreFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( - TaintSetLabelFn.getCallee()->stripPointerCasts()); + TaintGEPOffsetFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintUnimplementedFn.getCallee()->stripPointerCasts()); + TaintRuntimeFunctions.insert( + TaintSetLabelFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintNonzeroLabelFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintVarargWrapperFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintDebugFn.getCallee()->stripPointerCasts()); + TaintRuntimeFunctions.insert( + TaintMinimizeLabelFn.getCallee()->stripPointerCasts()); } // Initializes event callback functions and declare them in the module @@ -1297,13 +1348,6 @@ void Taint::initializeCallbackFunctions(Module &M) { TaintTraceGEPFn = Mod->getOrInsertFunction("__taint_trace_gep", TaintTraceGEPFnTy, AL); } - { - AttributeList AL; - AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); - AL = AL.addParamAttribute(M.getContext(), 0, Attribute::ZExt); - TaintTraceGEPPtrFn = - Mod->getOrInsertFunction("__taint_trace_gep_ptr", TaintTraceGEPPtrFnTy, AL); - } { AttributeList AL; AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); @@ -1358,6 +1402,14 @@ void Taint::initializeCallbackFunctions(Module &M) { TaintSolveSizeFn = Mod->getOrInsertFunction("__taint_solve_size", TaintSolveSizeFnTy, AL); } + { + AttributeList AL; + AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); + AL = AL.addFnAttribute(M.getContext(), Attribute::NoMerge); + AL = AL.addParamAttribute(M.getContext(), 1, Attribute::ZExt); + TaintSolveStrBoundsFn = + Mod->getOrInsertFunction("__taint_solve_str_bounds", TaintSolveStrBoundsFnTy, AL); + } TaintRuntimeFunctions.insert( TaintTraceCmpFn.getCallee()->stripPointerCasts()); @@ -1373,8 +1425,6 @@ void Taint::initializeCallbackFunctions(Module &M) { TaintTraceIndirectCallFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintTraceGEPFn.getCallee()->stripPointerCasts()); - TaintRuntimeFunctions.insert( - TaintTraceGEPPtrFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintPushStackFrameFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( @@ -1389,6 +1439,8 @@ void Taint::initializeCallbackFunctions(Module &M) { TaintSolveBoundsFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( TaintSolveSizeFn.getCallee()->stripPointerCasts()); + TaintRuntimeFunctions.insert( + TaintSolveStrBoundsFn.getCallee()->stripPointerCasts()); } bool Taint::runImpl(Module &M) { @@ -1446,7 +1498,8 @@ bool Taint::runImpl(Module &M) { for (Function &F : M) { if (!F.isIntrinsic() && !TaintRuntimeFunctions.count(&F) && - !IFuncs.count(&F)) { + !IFuncs.count(&F) && + !F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation)) { FnsToInstrument.push_back(&F); if (F.hasPersonalityFn()) PersonalityFns.insert(F.getPersonalityFn()); @@ -1473,6 +1526,10 @@ bool Taint::runImpl(Module &M) { if (!F) continue; + // Skip functions with nosanitize metadata + if (F->hasFnAttribute(Attribute::DisableSanitizerInstrumentation)) + continue; + bool GAInst = isInstrumented(&GA), FInst = isInstrumented(F); if (GAInst && FInst) { addGlobalNameSuffix(&GA); @@ -1581,34 +1638,42 @@ bool Taint::runImpl(Module &M) { // TaintVisitor may create new basic blocks, which confuses df_iterator. // Build a copy of the list before iterating over it. SmallVector BBList(depth_first(&F->getEntryBlock())); + std::unordered_map> LoopExits; for (BasicBlock *BB : BBList) { // check for loop header - if (ClTraceLoop) { - if (TF.LI->isLoopHeader(BB)) { - // This is a loop header - Instruction *FI = &*(BB->getFirstInsertionPt()); - ConstantInt *CID = ConstantInt::get(Int32Ty, getInstructionId(FI)); - ConstantInt *LoopDepth = ConstantInt::get(Int32Ty, TF.LI->getLoopDepth(BB)); - IRBuilder<> IRB(FI); - IRB.CreateCall(TaintTraceLoopFn, {CID, LoopDepth}); - } + if (ClTraceLoop && TF.LI) { Loop *L = TF.LI->getLoopFor(BB); if (L) { + auto *Header = L->getHeader(); + uint32_t LoopIdVal = getInstructionId(Header->getTerminator()); + ConstantInt *LoopID = ConstantInt::get(Int32Ty, LoopIdVal); + if (Header == BB) { + // This is a loop header + Instruction *FI = &*(BB->getFirstInsertionPt()); + ConstantInt *LoopDepth = ConstantInt::get(Int32Ty, TF.LI->getLoopDepth(BB)); + IRBuilder<> IRB(FI); + IRB.CreateCall(TaintTraceLoopFn, {LoopID, LoopDepth}); + } + // try to find exits, we do this because predecessors could be incomplete for (BasicBlock *Succ : successors(BB)) { if (!L->contains(Succ)) { - Instruction *FI = &*(Succ->getFirstInsertionPt()); - IRBuilder<> IRB(FI); - ConstantInt *CID = ConstantInt::get(Int32Ty, getInstructionId(FI)); - Loop *SuccL = TF.LI->getLoopFor(Succ); - int succ_depth = SuccL ? SuccL->getLoopDepth() : 0; - int depth = L->getLoopDepth(); - ConstantInt *LoopDepth = ConstantInt::get(Int32Ty, succ_depth - depth); - IRB.CreateCall(TaintTraceLoopFn, {CID, LoopDepth}); + auto &Exits = LoopExits[LoopIdVal]; + if (Exits.insert(Succ).second) { + // only instrument once + Instruction *FI = &*(Succ->getFirstInsertionPt()); + IRBuilder<> IRB(FI); + Loop *SuccL = TF.LI->getLoopFor(Succ); + int succ_depth = SuccL ? SuccL->getLoopDepth() : 0; + int depth = L->getLoopDepth(); + ConstantInt *LoopDepth = ConstantInt::get(Int32Ty, succ_depth - depth); + IRB.CreateCall(TaintTraceLoopFn, {LoopID, LoopDepth}); + } } } } } + Instruction *Inst = &BB->front(); while (true) { // TaintVisitor may split the current basic block, changing the current @@ -1618,7 +1683,13 @@ bool Taint::runImpl(Module &M) { // TaintVisitor may delete Inst, so keep track of whether it was a // terminator. bool IsTerminator = Inst->isTerminator(); - if (!TF.SkipInsts.count(Inst)) + // Handle nosanitize __dfsw_* calls from UCSan + if (ClWithUCSan && Inst->getMetadata("nosanitize")) { + if (auto *CI = dyn_cast(Inst)) { + TF.handleUCSanCall(CI, Next); + } + } + if (!TF.SkipInsts.count(Inst) && !Inst->getMetadata("nosanitize")) TaintVisitor(TF).visit(Inst); if (IsTerminator) break; @@ -1637,6 +1708,10 @@ bool Taint::runImpl(Module &M) { Val, TF.getShadow(P.Phi->getIncomingValue(Val))); } } + + // Hoist bounds checks out of loops + if (ClTraceBound && ClHoistBoundsChecks) + TF.hoistBoundsChecks(); } return Changed || !FnsToInstrument.empty() || @@ -1727,6 +1802,85 @@ void TaintFunction::setShadow(Instruction *I, Value *Shadow) { ValShadowMap[I] = Shadow; } +bool TaintFunction::handleUCSanCall(CallInst *CI, Instruction *Next) { + Function *Callee = CI->getCalledFunction(); + if (!Callee) + return false; + StringRef FName = Callee->getName(); + if (!FName.startswith("__dfsw_")) + return false; + + StringRef BaseName = FName.drop_front(7); // skip "__dfsw_" + + auto GetFakeIORetShadow = [&]() -> Value * { + // ucsan_custom.cpp simulates reads without forwarding to libc. For these + // fake full-read paths the return value is derived from the requested + // count, so keep the return label tied to that argument instead of the + // wrapper's concrete retval TLS. + if (BaseName == "read" || BaseName == "pread" || + BaseName == "pread64") { + if (CI->arg_size() > 2) + return getShadow(CI->getArgOperand(2)); + } else if (BaseName == "fread" || BaseName == "fread_unlocked") { + if (CI->arg_size() > 2) + return getShadow(CI->getArgOperand(2)); + } + return nullptr; + }; + + // Emit minimize hints for allocation size arguments + SmallVector SizeArgIndices; + if (BaseName == "malloc" || BaseName == "__libc_malloc" || + BaseName == "valloc" || BaseName == "__libc_valloc" || + BaseName == "pvalloc" || BaseName == "__libc_pvalloc" || + BaseName == "kmalloc_large") { + SizeArgIndices.push_back(0); + } else if (BaseName == "calloc" || BaseName == "__libc_calloc") { + SizeArgIndices.push_back(0); + SizeArgIndices.push_back(1); + } else if (BaseName == "realloc" || BaseName == "__libc_realloc" || + BaseName == "aligned_alloc" || + BaseName == "memalign" || BaseName == "__libc_memalign" || + BaseName == "kmalloc" || BaseName == "__kmalloc") { + SizeArgIndices.push_back(1); + } else if (BaseName == "reallocarray" || BaseName == "__libc_reallocarray") { + SizeArgIndices.push_back(1); + SizeArgIndices.push_back(2); + } else if (BaseName == "posix_memalign") { + SizeArgIndices.push_back(2); + } + + // Load return shadow from retval TLS for non-void __dfsw_* calls + LoadInst *LI = nullptr; + if (!CI->getType()->isVoidTy()) { + IRBuilder<> NextIRB(Next); + if (Value *RetShadow = GetFakeIORetShadow()) { + setShadow(CI, RetShadow); + } else { + LI = NextIRB.CreateAlignedLoad( + TT.getShadowTy(CI), getRetvalTLS(CI->getType(), NextIRB), + ShadowTLSAlignment, "_dfsret"); + SkipInsts.insert(LI); + setShadow(CI, LI); + } + } + + if (!SizeArgIndices.empty()) { + IRBuilder<> IRB(LI ? LI->getNextNode() : Next); + for (unsigned Idx : SizeArgIndices) { + Value *Size = CI->getArgOperand(Idx); + Value *Shadow = getShadow(Size); + Value *Bounds = LI; + if (!Bounds) Bounds = ConstantInt::get(TT.getShadowTy(CI), 0); + if (!TT.isZeroShadow(Shadow)) { + IRB.CreateCall(TT.TaintMinimizeLabelFn, {Shadow, Size, Bounds}); + } + } + } + + return true; +} + /// Compute the integer shadow offset that corresponds to a given /// application address. /// @@ -1873,8 +2027,11 @@ void TaintFunction::checkBounds(Value *Ptr, Value* Size, Instruction *Pos) { IRBuilder<> IRB(Pos); // another place to check for global variable as the ptr Value *PtrShadow = nullptr; + Value *PtrBase = getUnderlyingObject(Ptr); if (GlobalVariable *GV = dyn_cast(Ptr->stripPointerCasts())) { PtrShadow = getShadowForGlobal(GV, IRB); + } else if (GlobalVariable *GV = dyn_cast(PtrBase)) { + PtrShadow = getShadowForGlobal(GV, IRB); } else { PtrShadow = getShadow(Ptr); } @@ -1883,7 +2040,7 @@ void TaintFunction::checkBounds(Value *Ptr, Value* Size, Instruction *Pos) { if (!TT.isZeroShadow(PtrShadow)) { Value *Addr = IRB.CreatePtrToInt(Ptr, TT.Int64Ty); Value *Size64 = IRB.CreateZExtOrTrunc(Size, TT.Int64Ty); - IRB.CreateCall(TT.TaintCheckBoundsFn, {PtrShadow, Addr, SizeShadow, Size}); + IRB.CreateCall(TT.TaintCheckBoundsFn, {PtrShadow, Addr, SizeShadow, Size64}); } } @@ -1896,8 +2053,11 @@ void TaintFunction::solveBounds(Value *Ptr, Value* Size, Instruction *Pos) { IRBuilder<> IRB(Pos); // another place to check for global variable as the ptr Value *PtrShadow = nullptr; + Value *PtrBase = getUnderlyingObject(Ptr); if (GlobalVariable *GV = dyn_cast(Ptr->stripPointerCasts())) { PtrShadow = getShadowForGlobal(GV, IRB); + } else if (GlobalVariable *GV = dyn_cast(PtrBase)) { + PtrShadow = getShadowForGlobal(GV, IRB); } else { PtrShadow = getShadow(Ptr); } @@ -1908,22 +2068,948 @@ void TaintFunction::solveBounds(Value *Ptr, Value* Size, Instruction *Pos) { {PtrShadow, Addr, SizeShadow, Size64, CID}); } +// Collect all SCEVUnknown values from a SCEV expression. +static void collectSCEVUnknowns(const SCEV *S, + SmallVectorImpl &Unknowns) { + if (auto *U = dyn_cast(S)) { + Unknowns.push_back(U->getValue()); + } else if (auto *NAry = dyn_cast(S)) { + for (const SCEV *Op : NAry->operands()) + collectSCEVUnknowns(Op, Unknowns); + } else if (auto *Cast = dyn_cast(S)) { + collectSCEVUnknowns(Cast->getOperand(), Unknowns); + } else if (auto *UDiv = dyn_cast(S)) { + collectSCEVUnknowns(UDiv->getLHS(), Unknowns); + collectSCEVUnknowns(UDiv->getRHS(), Unknowns); + } + // SCEVConstant has no unknowns +} + +// Hoist __taint_check_bounds / __taint_solve_bounds calls out of loops using +// SCEV analysis. +// For each loop with a computable backedge-taken count, find bounds checks +// where the pointer shadow (bounds label) is loop-invariant and the address +// follows an affine recurrence {start, +, stride}. Replace N per-iteration +// checks with a single summary check in the preheader covering the full +// access range [start, start + BTC * stride + elem_size). +// When the summarized range is symbolic, also emit a __taint_solve_size call +// so the solver can find loop bounds that cause OOB. +void TaintFunction::hoistBoundsChecks() { + // Recalculate analyses after TaintVisitor may have split blocks + DT.recalculate(*F); + delete LI; + LI = new LoopInfo(DT); + + if (LI->empty()) return; + + Module *M = F->getParent(); + TargetLibraryInfoImpl TLII(Triple(M->getTargetTriple())); + TargetLibraryInfo TLI(TLII, F); + AssumptionCache AC(*F); + ScalarEvolution SE(*F, TLI, AC, DT, *LI); + + // Process innermost loops first so hoisted checks can be further + // hoisted when processing outer loops + SmallVector Loops(LI->getLoopsInPreorder()); + + for (Loop *L : reverse(Loops)) { + BasicBlock *Preheader = L->getLoopPreheader(); + if (!Preheader) { + // No canonical preheader (predecessor has multiple successors). + // Split the edge to create one. + BasicBlock *Pred = L->getLoopPredecessor(); + if (!Pred) continue; + Preheader = SplitEdge(Pred, L->getHeader(), &DT, LI); + if (!Preheader) continue; + } + + const SCEV *BTC = SE.getBackedgeTakenCount(L); + if (isa(BTC)) { + struct UCSanCallSummary { + CallInst *CI; + symsan::ucsan::MemoryAccessSummary Summary; + }; + SmallVector UCSanCallSummaries; + if (ClWithUCSan) { + for (BasicBlock *BB : L->blocks()) { + if (LI->getLoopFor(BB) != L) continue; + for (Instruction &I : *BB) { + auto *CI = dyn_cast(&I); + if (!CI) continue; + Function *Callee = CI->getCalledFunction(); + if (!Callee) continue; + MDNode *Summaries = symsan::ucsan::getMemoryAccessSummaries(*Callee); + if (!Summaries) continue; + for (const MDOperand &Op : Summaries->operands()) { + auto *SummaryNode = dyn_cast_or_null(Op.get()); + symsan::ucsan::MemoryAccessSummary Summary; + if (!symsan::ucsan::parseMemoryAccessSummary(SummaryNode, + Summary)) + continue; + if (!Summary.IsWrite || Summary.AccessSize == 0 || + Summary.ArgNo >= CI->arg_size()) + continue; + UCSanCallSummaries.push_back({CI, Summary}); + } + } + } + } + + auto GetSinglePredGuardCount = [&](CallInst *CI) -> Value * { + BasicBlock *BB = CI->getParent(); + if (std::distance(pred_begin(BB), pred_end(BB)) != 1) + return nullptr; + BasicBlock *Pred = *pred_begin(BB); + auto *BI = dyn_cast(Pred->getTerminator()); + if (!BI || !BI->isConditional()) + return nullptr; + if (BI->getSuccessor(0) != BB && BI->getSuccessor(1) != BB) + return nullptr; + auto *Cmp = dyn_cast(BI->getCondition()); + if (!Cmp) + return nullptr; + + Value *LHS = Cmp->getOperand(0); + Value *RHS = Cmp->getOperand(1); + if (auto *C = dyn_cast(RHS)) { + if (C->isZero()) + return LHS; + } + if (auto *C = dyn_cast(LHS)) { + if (C->isZero()) + return RHS; + } + return nullptr; + }; + + bool EmittedUnknownTripSummary = false; + for (const UCSanCallSummary &CallSummary : UCSanCallSummaries) { + CallInst *CI = CallSummary.CI; + const auto &Summary = CallSummary.Summary; + Value *Arg = CI->getArgOperand(Summary.ArgNo)->stripPointerCasts(); + if (!L->isLoopInvariant(Arg) || !isa(Arg)) + continue; + + Value *Count = GetSinglePredGuardCount(CI); + if (!Count) + continue; + + IRBuilder<> IRB(CI); + Type *I8PtrTy = Type::getInt8PtrTy(F->getContext()); + Type *I8PtrPtrTy = PointerType::getUnqual(I8PtrTy); + Value *ArgI8 = IRB.CreateBitCast(Arg, I8PtrTy); + Value *FieldAddr = ArgI8; + if (Summary.FieldOffset != 0) { + FieldAddr = IRB.CreateGEP( + IRB.getInt8Ty(), ArgI8, + ConstantInt::get(TT.Int64Ty, Summary.FieldOffset, true)); + } + Value *FieldAddrPtr = IRB.CreateBitCast(FieldAddr, I8PtrPtrTy); + Value *AddrLabel = loadShadow(I8PtrTy, FieldAddrPtr, + M->getDataLayout().getTypeStoreSize(I8PtrTy), + Align(1), CI); + Value *LoadedPtr = IRB.CreateLoad(I8PtrTy, FieldAddrPtr); + Value *StartVal = IRB.CreatePtrToInt(LoadedPtr, TT.Int64Ty); + Value *Count64 = IRB.CreateZExtOrTrunc(Count, TT.Int64Ty); + Value *TotalVal = Count64; + if (Summary.AccessSize != 1) + TotalVal = IRB.CreateMul( + Count64, ConstantInt::get(TT.Int64Ty, Summary.AccessSize)); + Value *SizeShadow = getShadow(Count); + + // Soft solve_size hint only: the guard value is a heuristic for the + // write count, not a proven bound, so a hard check_bounds here would + // risk spurious OOB aborts. + ConstantInt *CID = ConstantInt::get(TT.Int32Ty, + TT.getInstructionId(CI)); + IRB.CreateCall(TT.TaintSolveSizeFn, + {AddrLabel, StartVal, SizeShadow, TotalVal, CID}); + EmittedUnknownTripSummary = true; + } + + if (EmittedUnknownTripSummary) + continue; + + // SCEV can't compute trip count — check for strlen-bounded loop: + // while (ptr[i] != 0) { ... __taint_check_bounds(...) ... } + // Detect: loop exit is (icmp ne (load i8 (gep i8* base, iv)), 0) + BasicBlock *Header = L->getHeader(); + BranchInst *HeaderBr = dyn_cast(Header->getTerminator()); + if (!HeaderBr || !HeaderBr->isConditional()) continue; + + ICmpInst *Cmp = dyn_cast(HeaderBr->getCondition()); + if (!Cmp) continue; + + // Match: icmp ne i8 %val, 0 or icmp eq i8 %val, 0 + Value *LoadedVal = nullptr; + if (Cmp->getPredicate() == ICmpInst::ICMP_NE && + isa(Cmp->getOperand(1)) && + cast(Cmp->getOperand(1))->isZero()) + LoadedVal = Cmp->getOperand(0); + else if (Cmp->getPredicate() == ICmpInst::ICMP_EQ && + isa(Cmp->getOperand(1)) && + cast(Cmp->getOperand(1))->isZero()) + LoadedVal = Cmp->getOperand(0); + else + continue; + + // Strip zext/trunc to find the load + if (auto *ZE = dyn_cast(LoadedVal)) + LoadedVal = ZE->getOperand(0); + if (auto *TR = dyn_cast(LoadedVal)) + LoadedVal = TR->getOperand(0); + + // Find the base string pointer from the loop header. + Value *StrBase = nullptr; + + CallInst *StrUCChk = nullptr; + if (ClWithUCSan) { + // TaintPass runs after UCSanPass, so the load goes through + // ucsan_check_pointer. Scan the header + // for a GEP matching: gep i8, base, iv + for (Instruction &I : *Header) { + auto *GEP = dyn_cast(&I); + if (!GEP || GEP->getNumIndices() != 1) continue; + if (!GEP->getSourceElementType()->isIntegerTy(8)) continue; + if (!L->isLoopInvariant(GEP->getPointerOperand())) continue; + Value *Idx = GEP->getOperand(1); + if (auto *PN = dyn_cast(Idx)) { + if (L->contains(PN->getParent())) { + StrBase = GEP->getPointerOperand(); + break; + } + } + } + // Find the ucsan_check_pointer call on StrBase's GEP in the loop + if (StrBase) { + for (BasicBlock *BB : L->blocks()) { + if (StrUCChk) break; + for (Instruction &I : *BB) { + auto *CI = dyn_cast(&I); + if (!CI) continue; + Function *Fn = CI->getCalledFunction(); + if (Fn && Fn->getName() == "ucsan_check_pointer") { + Value *Ptr = CI->getArgOperand(0); + if (auto *GEP = dyn_cast(Ptr)) { + if (GEP->getPointerOperand() == StrBase) { + StrUCChk = CI; + break; + } + } + } + } + } + } + } else { + // Without UCSan, the load directly uses the GEP + if (auto *ZE = dyn_cast(LoadedVal)) + LoadedVal = ZE->getOperand(0); + if (auto *TR = dyn_cast(LoadedVal)) + LoadedVal = TR->getOperand(0); + auto *LI_load = dyn_cast(LoadedVal); + if (!LI_load || !LI_load->getType()->isIntegerTy(8)) + continue; + auto *GEP = dyn_cast(LI_load->getPointerOperand()); + if (!GEP || GEP->getNumIndices() != 1) continue; + if (!L->isLoopInvariant(GEP->getPointerOperand())) continue; + StrBase = GEP->getPointerOperand(); + } + if (!StrBase) continue; + + // Collect __taint_check_bounds calls in this loop + SmallVector BoundsChecks; + for (BasicBlock *BB : L->blocks()) { + if (LI->getLoopFor(BB) != L) continue; + for (Instruction &I : *BB) { + auto *CI = dyn_cast(&I); + if (!CI) continue; + Function *Callee = CI->getCalledFunction(); + if (Callee && Callee->getName() == "__taint_check_bounds") + BoundsChecks.push_back(CI); + } + } + if (BoundsChecks.empty()) continue; + + // For each bounds check, extract the buffer pointer and emit + // __taint_solve_str_bounds(str_ptr, buf_label, buf_ptr, step) + Instruction *InsertPt = Preheader->getTerminator(); + IRBuilder<> IRB(InsertPt); + Value *StrPtr = IRB.CreateBitCast(StrBase, Type::getInt8PtrTy(*TT.Ctx)); + if (StrUCChk) { + // Hoist ucsan_check_pointer with deref=1 so the string object + // is materialized before __taint_solve_str_bounds dereferences it + Value *UCLabel = StrUCChk->getArgOperand(1); + if (!L->isLoopInvariant(UCLabel)) + UCLabel = ConstantInt::get(UCLabel->getType(), 0); + StrPtr = IRB.CreateCall(StrUCChk->getCalledFunction(), + {StrPtr, UCLabel, StrUCChk->getArgOperand(2), + ConstantInt::getTrue(*TT.Ctx), StrUCChk->getArgOperand(4)}); + } + + DenseSet EmittedBufs; + for (CallInst *CI : BoundsChecks) { + Value *AddrLabel = CI->getArgOperand(0); // buf shadow (bounds info) + Value *Addr = CI->getArgOperand(1); // buf concrete address + Value *Size = CI->getArgOperand(3); // access size (step) + + // Resolve loop-variant AddrLabel to loop-invariant base + if (!L->isLoopInvariant(AddrLabel)) { + Value *Resolved = AddrLabel; + bool Found = false; + for (int Depth = 0; Depth < 4 && !Found; ++Depth) { + if (L->isLoopInvariant(Resolved)) { + Found = true; + } else if (auto *PN = dyn_cast(Resolved)) { + for (unsigned i = 0, e = PN->getNumIncomingValues(); i != e; ++i) { + if (!L->contains(PN->getIncomingBlock(i))) { + Resolved = PN->getIncomingValue(i); + break; + } + } + } else if (auto *GepCall = dyn_cast(Resolved)) { + Function *GepFn = GepCall->getCalledFunction(); + if (GepFn && GepFn->getName() == "__taint_gep_offset") + Resolved = GepCall->getArgOperand(0); + else + break; + } else { + break; + } + } + if (!Found && L->isLoopInvariant(Resolved)) + Found = true; + if (!Found) continue; + AddrLabel = Resolved; + } + + // One call per unique buffer + if (!EmittedBufs.insert(AddrLabel).second) continue; + + // Resolve buf_ptr to loop-invariant start address + Value *BufPtr = Addr; + if (!L->isLoopInvariant(BufPtr)) { + // Try to find the base address from SCEV or PHI + if (auto *PTI = dyn_cast(BufPtr)) { + Value *PtrOp = PTI->getOperand(0); + if (ClWithUCSan) { + if (auto *ChkCall = dyn_cast(PtrOp)) { + Function *ChkFn = ChkCall->getCalledFunction(); + if (ChkFn && ChkFn->getName() == "ucsan_check_pointer") + PtrOp = ChkCall->getArgOperand(0); + } + } + if (auto *BufGEP = dyn_cast(PtrOp)) { + BufPtr = IRB.CreatePtrToInt(BufGEP->getPointerOperand(), TT.Int64Ty); + } + } + if (!L->isLoopInvariant(BufPtr)) continue; + } + + auto *SizeC = dyn_cast(Size); + uint64_t Step = SizeC ? SizeC->getZExtValue() : 1; + + IRB.CreateCall(TT.TaintSolveStrBoundsFn, + {StrPtr, AddrLabel, + BufPtr, ConstantInt::get(TT.Int64Ty, Step)}); + } + + // Remove per-iteration bounds checks + for (CallInst *CI : BoundsChecks) + CI->eraseFromParent(); + + continue; + } + + // Collect bounds calls at this loop level (skip subloops). + SmallVector BoundsChecks; + SmallVector SolveBoundsCalls; + SmallVector UCSanPointerChecks; + struct UCSanCallSummary { + CallInst *CI; + symsan::ucsan::MemoryAccessSummary Summary; + }; + SmallVector UCSanCallSummaries; + for (BasicBlock *BB : L->blocks()) { + if (LI->getLoopFor(BB) != L) continue; + for (Instruction &I : *BB) { + auto *CI = dyn_cast(&I); + if (!CI) continue; + Function *Callee = CI->getCalledFunction(); + if (!Callee) continue; + if (Callee->getName() == "__taint_check_bounds") + BoundsChecks.push_back(CI); + else if (Callee->getName() == "__taint_solve_bounds") + SolveBoundsCalls.push_back(CI); + else if (ClWithUCSan && Callee->getName() == "ucsan_check_pointer") + UCSanPointerChecks.push_back(CI); + if (ClWithUCSan) { + if (MDNode *Summaries = + symsan::ucsan::getMemoryAccessSummaries(*Callee)) { + for (const MDOperand &Op : Summaries->operands()) { + auto *SummaryNode = dyn_cast_or_null(Op.get()); + symsan::ucsan::MemoryAccessSummary Summary; + if (!symsan::ucsan::parseMemoryAccessSummary(SummaryNode, + Summary)) + continue; + if (!Summary.IsWrite || Summary.AccessSize == 0 || + Summary.ArgNo >= CI->arg_size()) + continue; + UCSanCallSummaries.push_back({CI, Summary}); + } + } + } + } + } + if (BoundsChecks.empty() && SolveBoundsCalls.empty() && + UCSanPointerChecks.empty() && UCSanCallSummaries.empty()) + continue; + + auto ResolveLoopInvariantLabel = [&](Value *Label) -> Value * { + if (L->isLoopInvariant(Label)) + return Label; + + Value *Resolved = Label; + bool Found = false; + for (int Depth = 0; Depth < 4 && !Found; ++Depth) { + if (L->isLoopInvariant(Resolved)) { + Found = true; + } else if (auto *PN = dyn_cast(Resolved)) { + for (unsigned i = 0, e = PN->getNumIncomingValues(); i != e; ++i) { + if (!L->contains(PN->getIncomingBlock(i))) { + Resolved = PN->getIncomingValue(i); + break; + } + } + } else if (auto *GepCall = dyn_cast(Resolved)) { + Function *GepFn = GepCall->getCalledFunction(); + if (GepFn && GepFn->getName() == "__taint_gep_offset") + Resolved = GepCall->getArgOperand(0); + else + break; + } else { + break; + } + } + if (!Found && L->isLoopInvariant(Resolved)) + Found = true; + return Found ? Resolved : nullptr; + }; + + // Group hoistable checks by AddrLabel (ptr bounds shadow). + // For each group, compute the merged range covering all accesses, + // emit one summary check + one solve_size call. + struct HoistCandidate { + const SCEVAddRecExpr *AR; + uint64_t ElemSize; + CallInst *CI; + }; + // ptrtoint instructions materialized only to query SCEV; erased at the end + // of this loop if they end up unused. + SmallVector SCEVTemps; + + DenseMap> Groups; + for (CallInst *CI : BoundsChecks) { + Value *AddrLabel = CI->getArgOperand(0); // ptr bounds shadow + Value *Addr = CI->getArgOperand(1); // concrete address + Value *Size = CI->getArgOperand(3); // access size + + // AddrLabel may be loop-variant due to: + // 1. __taint_gep_offset(base_shadow, gep, base) called per-iteration + // 2. PHI nodes for pointer-incrementing loops (buf++) + // Walk through these to find the loop-invariant base allocation shadow. + if (!L->isLoopInvariant(AddrLabel)) { + Value *Resolved = ResolveLoopInvariantLabel(AddrLabel); + if (!Resolved) continue; + AddrLabel = Resolved; + } + + auto *SizeC = dyn_cast(Size); + if (!SizeC) continue; + + // With UCSan, Addr is ptrtoint(ucsan_check_pointer(orig_ptr, ...)). + // ucsan_check_pointer is opaque to SCEV; use the original pointer. + Value *AddrForSCEV = Addr; + if (ClWithUCSan) { + if (auto *PTI = dyn_cast(Addr)) { + if (auto *ChkCall = dyn_cast(PTI->getPointerOperand())) { + Function *ChkFn = ChkCall->getCalledFunction(); + if (ChkFn && ChkFn->getName() == "ucsan_check_pointer") { + IRBuilder<> IRB(CI); + AddrForSCEV = IRB.CreatePtrToInt(ChkCall->getArgOperand(0), TT.Int64Ty); + SCEVTemps.push_back(cast(AddrForSCEV)); + } + } + } + } + + const SCEV *AddrSCEV = SE.getSCEV(AddrForSCEV); + auto *AR = dyn_cast(AddrSCEV); + if (!AR || AR->getLoop() != L) continue; + + const SCEV *Step = AR->getStepRecurrence(SE); + auto *StepC = dyn_cast(Step); + if (!StepC) continue; + int64_t StepVal = StepC->getAPInt().getSExtValue(); + if (StepVal <= 0) continue; + + Groups[AddrLabel].push_back({AR, SizeC->getZExtValue(), CI}); + } + + // UCSan may guard the concrete access directly with ucsan_check_pointer + // without a matching __taint_check_bounds on the checked pointer. Treat + // affine checked pointers as solve-hint candidates, but keep the original + // runtime checks in place. + for (CallInst *CI : UCSanPointerChecks) { + if (CI->arg_size() < 5) + continue; + + auto *SizeC = dyn_cast(CI->getArgOperand(2)); + auto *DerefC = dyn_cast(CI->getArgOperand(3)); + if (!SizeC || !DerefC || !DerefC->isOne()) + continue; + + Value *AddrLabel = getShadow(CI); + if (TT.isZeroShadow(AddrLabel)) + AddrLabel = getShadow(CI->getArgOperand(0)); + if (TT.isZeroShadow(AddrLabel)) + continue; + + AddrLabel = ResolveLoopInvariantLabel(AddrLabel); + if (!AddrLabel) + continue; + + IRBuilder<> LocalIRB(CI); + Value *PtrAsInt = + LocalIRB.CreatePtrToInt(CI->getArgOperand(0), TT.Int64Ty); + SCEVTemps.push_back(cast(PtrAsInt)); + const SCEV *AddrSCEV = SE.getSCEV(PtrAsInt); + auto *AR = dyn_cast(AddrSCEV); + if (!AR || AR->getLoop() != L) + continue; + + const SCEV *Step = AR->getStepRecurrence(SE); + auto *StepC = dyn_cast(Step); + if (!StepC || StepC->getAPInt().getSExtValue() <= 0) + continue; + + Groups[AddrLabel].push_back({AR, SizeC->getZExtValue(), CI}); + } + + SCEVExpander Expander(SE, M->getDataLayout(), "bounds.hoist"); + Instruction *InsertPt = Preheader->getTerminator(); + DenseSet HoistedSolveBounds; + + for (auto &KV : Groups) { + Value *AddrLabel = KV.first; + auto &Candidates = KV.second; + + // Find the minimum start and maximum end across all accesses + // to emit a single check covering the entire range. + const SCEV *MinStart = Candidates[0].AR->getStart(); + const SCEV *MaxEnd = nullptr; // end = BTC * stride + elem_size + CallInst *FirstCI = Candidates[0].CI; + + for (auto &C : Candidates) { + const SCEV *Start = C.AR->getStart(); + const SCEV *Step = C.AR->getStepRecurrence(SE); + // end of this access: start + BTC * stride + elem_size + const SCEV *End = SE.getAddExpr( + Start, + SE.getAddExpr( + SE.getMulExpr(BTC, Step), + SE.getConstant(BTC->getType(), C.ElemSize) + ) + ); + if (SE.isKnownPredicate(ICmpInst::ICMP_ULT, Start, MinStart)) + MinStart = Start; + if (!MaxEnd || SE.isKnownPredicate(ICmpInst::ICMP_UGT, End, MaxEnd)) + MaxEnd = End; + } + + // total = MaxEnd - MinStart + const SCEV *TotalSCEV = SE.getMinusSCEV(MaxEnd, MinStart); + + Value *StartVal = Expander.expandCodeFor(MinStart, TT.Int64Ty, InsertPt); + Value *TotalVal = Expander.expandCodeFor(TotalSCEV, TT.Int64Ty, InsertPt); + + // Emit one summary bounds check for the group + IRBuilder<> IRB(InsertPt); + IRB.CreateCall(TT.TaintCheckBoundsFn, + {AddrLabel, StartVal, TT.ZeroPrimitiveShadow, TotalVal}); + + // If the summarized range is symbolic, emit one solve_size outside the + // loop. Looking at TotalSCEV catches both symbolic trip counts and + // symbolic extents folded into the address/range expression. + SmallVector Unknowns; + collectSCEVUnknowns(TotalSCEV, Unknowns); + Value *SizeShadow = TT.ZeroPrimitiveShadow; + for (Value *V : Unknowns) { + Value *S = getShadow(V); + if (!TT.isZeroShadow(S)) + SizeShadow = S; + } + if (!TT.isZeroShadow(SizeShadow)) { + ConstantInt *CID = ConstantInt::get(TT.Int32Ty, + TT.getInstructionId(FirstCI)); + IRB.CreateCall(TT.TaintSolveSizeFn, + {AddrLabel, StartVal, SizeShadow, TotalVal, CID}); + } + + // Remove original __taint_check_bounds checks, but do not remove + // ucsan_check_pointer calls because they perform the runtime access + // validation and may materialize under-constrained objects. + for (auto &C : Candidates) { + Function *Callee = C.CI->getCalledFunction(); + if (Callee && Callee->getName() == "__taint_check_bounds") + C.CI->eraseFromParent(); + } + } + + // UCSan callee summaries let us see stores hidden behind a call inside the + // loop. For a summary "callee loads a pointer from arg+field_offset and + // writes access_size bytes through it", emit a caller-side upper-bound + // check from the loop preheader. This is intentionally conservative: it + // only uses loop-invariant alloca-backed arguments so the inserted field + // load is safe without adding new UCSan instrumentation. + for (const UCSanCallSummary &CallSummary : UCSanCallSummaries) { + CallInst *CI = CallSummary.CI; + const auto &Summary = CallSummary.Summary; + Value *Arg = CI->getArgOperand(Summary.ArgNo)->stripPointerCasts(); + if (!L->isLoopInvariant(Arg)) + continue; + if (!isa(Arg)) + continue; + + const SCEV *TripCount = + SE.getAddExpr(BTC, SE.getConstant(BTC->getType(), 1)); + const SCEV *TotalSCEV = SE.getMulExpr( + TripCount, SE.getConstant(TripCount->getType(), Summary.AccessSize)); + + IRBuilder<> IRB(InsertPt); + Type *I8PtrTy = Type::getInt8PtrTy(F->getContext()); + Type *I8PtrPtrTy = PointerType::getUnqual(I8PtrTy); + Value *ArgI8 = IRB.CreateBitCast(Arg, I8PtrTy); + Value *FieldAddr = ArgI8; + if (Summary.FieldOffset != 0) { + FieldAddr = IRB.CreateGEP( + IRB.getInt8Ty(), ArgI8, + ConstantInt::get(TT.Int64Ty, Summary.FieldOffset, true)); + } + Value *FieldAddrPtr = IRB.CreateBitCast(FieldAddr, I8PtrPtrTy); + Value *AddrLabel = loadShadow(I8PtrTy, FieldAddrPtr, + M->getDataLayout().getTypeStoreSize(I8PtrTy), + Align(1), InsertPt); + Value *LoadedPtr = IRB.CreateLoad(I8PtrTy, FieldAddrPtr); + Value *StartVal = IRB.CreatePtrToInt(LoadedPtr, TT.Int64Ty); + Value *TotalVal = Expander.expandCodeFor(TotalSCEV, TT.Int64Ty, InsertPt); + + // Soft solve_size hint only. The summary records a fixed field offset and + // access size with no per-iteration stride, so trip_count * access_size is + // not a proven extent; a hard check_bounds here could abort spuriously. + SmallVector Unknowns; + collectSCEVUnknowns(TotalSCEV, Unknowns); + Value *SizeShadow = TT.ZeroPrimitiveShadow; + for (Value *V : Unknowns) { + Value *S = getShadow(V); + if (!TT.isZeroShadow(S)) + SizeShadow = S; + } + ConstantInt *CID = ConstantInt::get(TT.Int32Ty, + TT.getInstructionId(CI)); + IRB.CreateCall(TT.TaintSolveSizeFn, + {AddrLabel, StartVal, SizeShadow, TotalVal, CID}); + } + + // Some accesses do not have a matching check_bounds call with an affine + // address. With UCSan this can happen when the load pointer is the + // ucsan_check_pointer result. Summarize solve_bounds directly from its + // base pointer and GEP index. + struct SolveCandidate { + const SCEVAddRecExpr *AR; + uint64_t ElemSize; + CallInst *CI; + }; + struct NonAffineSolveCandidate { + Value *AddrLabel; + Value *BasePtr; + Value *Index; + uint64_t ElemSize; + uint64_t Offset; + CallInst *CI; + }; + DenseMap> SolveGroups; + DenseMap> + NonAffineSolveGroups; + for (CallInst *CI : SolveBoundsCalls) { + Value *AddrLabel = CI->getArgOperand(0); + if (TT.isZeroShadow(AddrLabel)) + continue; + AddrLabel = ResolveLoopInvariantLabel(AddrLabel); + if (!AddrLabel) + continue; + + Value *BasePtr = CI->getArgOperand(1); + Value *BasePtrForSCEV = BasePtr; + if (auto *PTI = dyn_cast(BasePtrForSCEV)) { + Value *PtrOp = PTI->getPointerOperand(); + if (ClWithUCSan) { + if (auto *ChkCall = dyn_cast(PtrOp)) { + Function *ChkFn = ChkCall->getCalledFunction(); + if (ChkFn && ChkFn->getName() == "ucsan_check_pointer") + PtrOp = ChkCall->getArgOperand(0); + } + } + if (L->isLoopInvariant(PtrOp)) { + IRBuilder<> IRB(InsertPt); + BasePtrForSCEV = IRB.CreatePtrToInt(PtrOp, TT.Int64Ty); + } + } + if (!L->isLoopInvariant(BasePtrForSCEV)) + continue; + + auto *ElemSizeC = dyn_cast(CI->getArgOperand(5)); + auto *OffsetC = dyn_cast(CI->getArgOperand(6)); + if (!ElemSizeC || !OffsetC) + continue; + uint64_t ElemSize = ElemSizeC->getZExtValue(); + if (ElemSize == 0) + continue; + + NonAffineSolveGroups[AddrLabel].push_back( + {AddrLabel, BasePtrForSCEV, CI->getArgOperand(3), ElemSize, + OffsetC->getZExtValue(), CI}); + + const SCEV *IndexSCEV = SE.getSCEV(CI->getArgOperand(3)); + const SCEV *AddrSCEV = SE.getAddExpr( + SE.getSCEV(BasePtrForSCEV), + SE.getAddExpr( + SE.getMulExpr(IndexSCEV, + SE.getConstant(IndexSCEV->getType(), ElemSize)), + SE.getConstant(IndexSCEV->getType(), OffsetC->getZExtValue()))); + auto *AR = dyn_cast(AddrSCEV); + if (!AR || AR->getLoop() != L) { + continue; + } + + const SCEV *Step = AR->getStepRecurrence(SE); + auto *StepC = dyn_cast(Step); + if (!StepC) { + continue; + } + if (StepC->getAPInt().getSExtValue() <= 0) { + continue; + } + + SolveGroups[AddrLabel].push_back({AR, ElemSize, CI}); + } + + for (auto &KV : SolveGroups) { + Value *AddrLabel = KV.first; + auto &Candidates = KV.second; + const SCEV *MinStart = Candidates[0].AR->getStart(); + const SCEV *MaxEnd = nullptr; + CallInst *FirstCI = Candidates[0].CI; + + for (auto &C : Candidates) { + const SCEV *Start = C.AR->getStart(); + const SCEV *Step = C.AR->getStepRecurrence(SE); + const SCEV *End = SE.getAddExpr( + Start, + SE.getAddExpr(SE.getMulExpr(BTC, Step), + SE.getConstant(BTC->getType(), C.ElemSize))); + if (SE.isKnownPredicate(ICmpInst::ICMP_ULT, Start, MinStart)) + MinStart = Start; + if (!MaxEnd || SE.isKnownPredicate(ICmpInst::ICMP_UGT, End, MaxEnd)) + MaxEnd = End; + } + + const SCEV *TotalSCEV = SE.getMinusSCEV(MaxEnd, MinStart); + Value *StartVal = Expander.expandCodeFor(MinStart, TT.Int64Ty, InsertPt); + Value *TotalVal = Expander.expandCodeFor(TotalSCEV, TT.Int64Ty, InsertPt); + + IRBuilder<> IRB(InsertPt); + IRB.CreateCall(TT.TaintCheckBoundsFn, + {AddrLabel, StartVal, TT.ZeroPrimitiveShadow, TotalVal}); + + SmallVector Unknowns; + collectSCEVUnknowns(TotalSCEV, Unknowns); + Value *SizeShadow = TT.ZeroPrimitiveShadow; + for (Value *V : Unknowns) { + Value *S = getShadow(V); + if (!TT.isZeroShadow(S)) + SizeShadow = S; + } + if (!TT.isZeroShadow(SizeShadow)) { + ConstantInt *CID = ConstantInt::get(TT.Int32Ty, + TT.getInstructionId(FirstCI)); + IRB.CreateCall(TT.TaintSolveSizeFn, + {AddrLabel, StartVal, SizeShadow, TotalVal, CID}); + } + + for (auto &C : Candidates) + HoistedSolveBounds.insert(C.CI); + } + + auto FindHeaderPhi = [&](Value *V) -> PHINode * { + SmallVector Worklist; + SmallPtrSet Seen; + Worklist.push_back(V); + while (!Worklist.empty()) { + Value *Cur = Worklist.pop_back_val(); + if (!Seen.insert(Cur).second) + continue; + if (auto *PN = dyn_cast(Cur)) { + if (PN->getParent() == L->getHeader()) + return PN; + continue; + } + if (auto *Cast = dyn_cast(Cur)) { + Worklist.push_back(Cast->getOperand(0)); + continue; + } + if (auto *BO = dyn_cast(Cur)) { + Worklist.push_back(BO->getOperand(0)); + Worklist.push_back(BO->getOperand(1)); + } + } + return nullptr; + }; + + auto GetLoopStartValue = [&](PHINode *PN) -> Value * { + for (unsigned i = 0, e = PN->getNumIncomingValues(); i != e; ++i) { + if (!L->contains(PN->getIncomingBlock(i))) + return PN->getIncomingValue(i); + } + return nullptr; + }; + + auto CountCandidatesOnPath = + [&](ArrayRef Candidates, + BasicBlock *Succ) -> unsigned { + unsigned Count = 0; + for (auto &C : Candidates) { + if (DT.dominates(Succ, C.CI->getParent())) + ++Count; + } + return Count; + }; + + uint64_t ConstantIterations = 0; + if (auto *BTCConst = dyn_cast(BTC)) { + ConstantIterations = BTCConst->getAPInt().getZExtValue() + 1; + } else if (BasicBlock *Latch = L->getLoopLatch()) { + if (auto *LatchBr = dyn_cast(Latch->getTerminator())) { + if (LatchBr->isConditional()) { + if (auto *Cmp = dyn_cast(LatchBr->getCondition())) { + if (auto *C = dyn_cast(Cmp->getOperand(1))) { + if (Cmp->getPredicate() == ICmpInst::ICMP_EQ || + Cmp->getPredicate() == ICmpInst::ICMP_UGE || + Cmp->getPredicate() == ICmpInst::ICMP_ULT) + ConstantIterations = C->getZExtValue(); + } else if (auto *C = dyn_cast(Cmp->getOperand(0))) { + if (Cmp->getPredicate() == ICmpInst::ICMP_EQ || + Cmp->getPredicate() == ICmpInst::ICMP_ULE || + Cmp->getPredicate() == ICmpInst::ICMP_UGT) + ConstantIterations = C->getZExtValue(); + } + } + } + } + } + BranchInst *HeaderBr = dyn_cast(L->getHeader()->getTerminator()); + if (ConstantIterations != 0 && HeaderBr && HeaderBr->isConditional() && + L->isLoopInvariant(HeaderBr->getCondition())) { + for (auto &KV : NonAffineSolveGroups) { + auto &Candidates = KV.second; + if (Candidates.empty()) + continue; + + PHINode *IndexPN = FindHeaderPhi(Candidates[0].Index); + if (!IndexPN) + continue; + Value *StartIndex = GetLoopStartValue(IndexPN); + if (!StartIndex) + continue; + + uint64_t ElemSize = Candidates[0].ElemSize; + uint64_t Offset = Candidates[0].Offset; + Value *BasePtr = Candidates[0].BasePtr; + bool SameShape = true; + for (auto &C : Candidates) { + if (C.ElemSize != ElemSize || C.Offset != Offset) { + SameShape = false; + break; + } + } + if (!SameShape) + continue; + + unsigned TrueCount = + CountCandidatesOnPath(Candidates, HeaderBr->getSuccessor(0)); + unsigned FalseCount = + CountCandidatesOnPath(Candidates, HeaderBr->getSuccessor(1)); + if (TrueCount == 0 && FalseCount == 0) + continue; + + IRBuilder<> IRB(InsertPt); + Value *StartIndex64 = IRB.CreateZExtOrTrunc(StartIndex, TT.Int64Ty); + Value *StartOffset = StartIndex64; + if (ElemSize != 1) + StartOffset = IRB.CreateMul( + StartOffset, ConstantInt::get(TT.Int64Ty, ElemSize)); + if (Offset != 0) + StartOffset = IRB.CreateAdd( + StartOffset, ConstantInt::get(TT.Int64Ty, Offset)); + Value *StartVal = IRB.CreateAdd(BasePtr, StartOffset); + + Value *TrueSize = ConstantInt::get(TT.Int64Ty, + ConstantIterations * TrueCount * ElemSize); + Value *FalseSize = ConstantInt::get(TT.Int64Ty, + ConstantIterations * FalseCount * ElemSize); + Value *TotalVal = IRB.CreateSelect(HeaderBr->getCondition(), + TrueSize, FalseSize); + + // Soft solve_size hint only: ConstantIterations and the per-path counts + // are heuristics, not a proven extent, so no hard check_bounds here. + ConstantInt *CID = ConstantInt::get(TT.Int32Ty, + TT.getInstructionId(Candidates[0].CI)); + IRB.CreateCall(TT.TaintSolveSizeFn, + {KV.first, StartVal, TT.ZeroPrimitiveShadow, TotalVal, + CID}); + + for (auto &C : Candidates) + HoistedSolveBounds.insert(C.CI); + } + } + + for (CallInst *CI : HoistedSolveBounds) + CI->eraseFromParent(); + + // Drop ptrtoint temporaries that were only needed for SCEV queries. + for (Instruction *T : SCEVTemps) + if (T->use_empty()) + T->eraseFromParent(); + } +} + // Generates IR to load shadow corresponding to bytes [Addr, Addr+Size), where // Addr has alignment Align, and take the union of each of those shadows. Value *TaintFunction::loadPrimitiveShadow(Value *Addr, uint64_t Size, - uint64_t Align, IRBuilder<> &IRB) { + uint64_t SizeInBits, uint64_t Align, + IRBuilder<> &IRB) { if (Size == 0) return TT.ZeroPrimitiveShadow; Value *ShadowAddr = TT.getShadowAddress(Addr, IRB); CallInst *FallbackCall = IRB.CreateCall( TT.TaintUnionLoadFn, {ShadowAddr, ConstantInt::get(TT.IntptrTy, Size), + ConstantInt::get(TT.Int64Ty, SizeInBits), ConstantInt::get(TT.IntptrTy, Align)}); FallbackCall->addRetAttr(Attribute::ZExt); return FallbackCall; } -void TaintFunction::loadShadowRecursive( +Value *TaintFunction::loadShadowRecursive( Value *Shadow, SmallVector &Indices, Type *SubTy, Value *Addr, uint64_t Size, uint64_t Align, IRBuilder<> &IRB) { auto &DL = F->getParent()->getDataLayout(); @@ -1931,12 +3017,12 @@ void TaintFunction::loadShadowRecursive( if (!isa(SubTy) && !isa(SubTy)) { uint64_t SubSize = DL.getTypeStoreSize(SubTy); assert(Size >= SubSize); + uint64_t SubSizeInBits = DL.getTypeSizeInBits(SubTy); Align = std::min(Align, (uint64_t)DL.getABITypeAlignment(SubTy)); // load a primitive shadow from address - Value *PrimitiveShadow = loadPrimitiveShadow(Addr, SubSize, Align, IRB); + Value *PrimitiveShadow = loadPrimitiveShadow(Addr, SubSize, SubSizeInBits, Align, IRB); // then insert the primitive shadow into the sub-field - IRB.CreateInsertValue(Shadow, PrimitiveShadow, Indices); - return; + return IRB.CreateInsertValue(Shadow, PrimitiveShadow, Indices); } if (ArrayType *AT = dyn_cast(SubTy)) { @@ -1949,11 +3035,11 @@ void TaintFunction::loadShadowRecursive( assert(Offset <= Size); // get the address of the array element Value *SubAddr = IRB.CreateConstGEP2_32(AT, Addr, 0, Idx); - loadShadowRecursive(Shadow, Indices, ElemTy, - SubAddr, Size - Offset, Align, IRB); + Shadow = loadShadowRecursive(Shadow, Indices, ElemTy, + SubAddr, Size - Offset, Align, IRB); Indices.pop_back(); } - return; + return Shadow; } if (StructType *ST = dyn_cast(SubTy)) { @@ -1966,11 +3052,11 @@ void TaintFunction::loadShadowRecursive( Type *ElemTy = ST->getElementType(Idx); // get the address of the struct field Value *SubAddr = IRB.CreateConstGEP2_32(ST, Addr, 0, Idx); - loadShadowRecursive(Shadow, Indices, ElemTy, - SubAddr, Size - Offset, Align, IRB); + Shadow = loadShadowRecursive(Shadow, Indices, ElemTy, + SubAddr, Size - Offset, Align, IRB); Indices.pop_back(); } - return; + return Shadow; } llvm_unreachable("Unexpected shadow type"); } @@ -2006,16 +3092,19 @@ Value *TaintFunction::loadShadow(Type *T, Value *Addr, uint64_t Size, return TT.ZeroPrimitiveShadow; const uint64_t ShadowAlign = getShadowAlign(Alignment).value(); + auto &DL = F->getParent()->getDataLayout(); // now check if we're loading an aggragate object - if (!isa(T) && !isa(T)) - return loadPrimitiveShadow(Addr, Size, ShadowAlign, IRB); + if (!isa(T) && !isa(T)) { + uint64_t SizeInBits = DL.getTypeSizeInBits(T); + return loadPrimitiveShadow(Addr, Size, SizeInBits, ShadowAlign, IRB); + } // if loading an aggregate object, load its shadow recursively SmallVector Indices; Type *ShadowTy = TT.getShadowTy(T); Value *Shadow = UndefValue::get(ShadowTy); - loadShadowRecursive(Shadow, Indices, T, Addr, Size, ShadowAlign, IRB); + Shadow = loadShadowRecursive(Shadow, Indices, T, Addr, Size, ShadowAlign, IRB); return Shadow; } @@ -2124,7 +3213,6 @@ void TaintVisitor::visitAtomicRMWInst(AtomicRMWInst &I) { } void TaintVisitor::visitLoadInst(LoadInst &LI) { - if (LI.getMetadata("nosanitize")) return; auto &DL = LI.getModule()->getDataLayout(); uint64_t Size = DL.getTypeStoreSize(LI.getType()); if (Size == 0) { @@ -2263,8 +3351,6 @@ void TaintFunction::storeShadow(Value *Addr, Type *T, uint64_t Size, } void TaintVisitor::visitStoreInst(StoreInst &SI) { - if (SI.getMetadata("nosanitize")) return; - auto &DL = SI.getModule()->getDataLayout(); Value *Val = SI.getValueOperand(); Type* VT = SI.getValueOperand()->getType(); @@ -2301,7 +3387,6 @@ void TaintVisitor::visitStoreInst(StoreInst &SI) { //} void TaintVisitor::visitBinaryOperator(BinaryOperator &BO) { - if (BO.getMetadata("nosanitize")) return; if (BO.getType()->isFloatingPointTy()) return; Value *CombinedShadow = TF.combineBinaryOperatorShadows(&BO, BO.getOpcode()); @@ -2309,7 +3394,6 @@ void TaintVisitor::visitBinaryOperator(BinaryOperator &BO) { } void TaintVisitor::visitCastInst(CastInst &CI) { - if (CI.getMetadata("nosanitize")) return; // Special case: if this is the bitcast (there is exactly 1 allowed) between // a musttail call and a ret, don't instrument. New instructions are not // allowed after a musttail call. @@ -2346,7 +3430,6 @@ void TaintFunction::visitCmpInst(CmpInst *I) { } void TaintVisitor::visitCmpInst(CmpInst &CI) { - if (CI.getMetadata("nosanitize")) return; // FIXME: integer only now if (!ClTraceFP && !isa(CI)) return; #if 0 //TODO make an option @@ -2386,7 +3469,6 @@ void TaintFunction::visitSwitchInst(SwitchInst *I) { } void TaintVisitor::visitSwitchInst(SwitchInst &SWI) { - if (SWI.getMetadata("nosanitize")) return; TF.visitSwitchInst(&SWI); } @@ -2412,19 +3494,12 @@ void TaintFunction::visitGEPInst(GetElementPtrInst *I) { IRBuilder<> IRB(I); Value *Base = I->getPointerOperand(); - Value *Bounds = TT.getZeroShadow(Base); - if (ClTraceBound) { - // get bounds info for base pointer - if (auto *GV = dyn_cast(Base->stripPointerCasts())) { - // if the base pointer is a global variable - // we can't get its shadow from the shadow map - Bounds = getShadowForGlobal(GV, IRB); - } else { - Bounds = getShadow(Base); - if (TT.isZeroShadow(Bounds)) { - // try striping the pointer cast - Bounds = getShadow(Base->stripPointerCasts()); - } + Value *Shadow = getShadow(Base->stripPointerCasts()); + if (auto *GV = dyn_cast(Base->stripPointerCasts())) { + // if the base pointer is a global variable, and without ucsan, + // we can't get its shadow from the shadow map + if (!ClWithUCSan) { + Shadow = getShadowForGlobal(GV, IRB); } } @@ -2466,8 +3541,8 @@ void TaintFunction::visitGEPInst(GetElementPtrInst *I) { CurrentOffset += arrayIdx * ElemSize; } else if (Index->getType()->isIntegerTy()) { // FIXEME: handle vector type // non-constant index, check if it's tainted - Value *Shadow = getShadow(Index); - if (!TT.isZeroShadow(Shadow)) { + Value *IndexShadow = getShadow(Index); + if (!TT.isZeroShadow(IndexShadow)) { Index = IRB.CreateZExtOrTrunc(Index, TT.Int64Ty); ConstantInt *Offset = ConstantInt::get(TT.Int64Ty, CurrentOffset); ConstantInt *NE = ConstantInt::get(TT.Int64Ty, NumElements); @@ -2480,11 +3555,11 @@ void TaintFunction::visitGEPInst(GetElementPtrInst *I) { // must be added before tracing GEP, otherwise index_label == index // will be added as nested constraint IRB.CreateCall(TT.TaintSolveBoundsFn, - {Bounds, Ptr, Shadow, Index, NE, ES, Offset, CID}); + {Shadow, Ptr, IndexShadow, Index, NE, ES, Offset, CID}); } if (ClTraceGEPOffset) { IRB.CreateCall(TT.TaintTraceGEPFn, - {Bounds, Ptr, Shadow, Index, NE, ES, Offset, CID}); + {Shadow, Ptr, IndexShadow, Index, NE, ES, Offset, CID}); } } else { break; @@ -2493,22 +3568,21 @@ void TaintFunction::visitGEPInst(GetElementPtrInst *I) { } } - if (ClTraceBound) { - // propagate bounds info - setShadow(I, Bounds); - } - - // For constant offset GEPs on string op pointers, create fstr_off label + // we need to check GEP for two reasons: + // 1. For constant offset GEPs on string op pointers, create fstr_off label // to track the offset (e.g., sep + 1 where sep is from strchr) - if (CurrentOffset != 0 && !TT.isZeroShadow(Bounds)) { + // 2. For symbolic ptr (e.g., from UCSan), we need to trace the offset + if (!TT.isZeroShadow(Shadow)) { IRBuilder<> IRB(I->getNextNode()); - Bounds = IRB.CreateCall(TT.TaintTraceGEPPtrFn, {Bounds, I, Base}); + Shadow = IRB.CreateCall(TT.TaintGEPOffsetFn, + {Shadow, IRB.CreateBitOrPointerCast(I, TT.VoidPtrTy), + IRB.CreateBitOrPointerCast(Base, TT.VoidPtrTy)}); } + + setShadow(I, Shadow); } void TaintVisitor::visitGetElementPtrInst(GetElementPtrInst &GEPI) { - if (!ClTraceGEPOffset && !ClTraceBound) return; - if (GEPI.getMetadata("nosanitize")) return; TF.visitGEPInst(&GEPI); } @@ -2525,8 +3599,6 @@ void TaintVisitor::visitShuffleVectorInst(ShuffleVectorInst &I) { } void TaintVisitor::visitExtractValueInst(ExtractValueInst &I) { - if (I.getMetadata("nosanitize")) return; - IRBuilder<> IRB(&I); Value *Agg = I.getAggregateOperand(); Value *AggShadow = TF.getShadow(Agg); @@ -2535,8 +3607,6 @@ void TaintVisitor::visitExtractValueInst(ExtractValueInst &I) { } void TaintVisitor::visitInsertValueInst(InsertValueInst &I) { - if (I.getMetadata("nosanitize")) return; - IRBuilder<> IRB(&I); Value *AggShadow = TF.getShadow(I.getAggregateOperand()); Value *InsShadow = TF.getShadow(I.getInsertedValueOperand()); @@ -3124,8 +4194,57 @@ void TaintVisitor::visitIntrinsicCallBase(Function *F, CallBase &CB) { if (!NeedsInstrumentation) return; - // FIXME: track intrinsic - return; + Intrinsic::ID IId = F->getIntrinsicID(); + + // bswap: decompose into Extract-per-byte then Concat in reverse order. + // __taint_union(l1=low, l2=high, Concat) → z3::concat(high, low) (little-endian) + if (IId == Intrinsic::bswap) { + Value *Shadow = TF.getShadow(CB.getArgOperand(0)); + unsigned TotalBits = CB.getArgOperand(0)->getType()->getIntegerBitWidth(); + unsigned NumBytes = TotalBits / 8; + + // Extract = last_llvm_op + 4 = 71, Concat = last_llvm_op + 5 = 72 (for LLVM 14) + const uint16_t OpExtract = 71; + const uint16_t OpConcat = 72; + + IRBuilder<> IRB(&CB); + Value *ZeroShadow = TF.TT.ZeroPrimitiveShadow; + Value *ExtractOp = ConstantInt::get(TF.TT.Int16Ty, OpExtract); + Value *ConcatOp = ConstantInt::get(TF.TT.Int16Ty, OpConcat); + Value *ByteSize = ConstantInt::get(TF.TT.Int16Ty, 8); + Value *Zero64 = ConstantInt::get(TF.TT.Int64Ty, 0); + + auto MakeUnionCall = [&](Value *L1, Value *L2, Value *Op, Value *Size, + Value *Op1, Value *Op2) -> Value * { + CallInst *C = IRB.CreateCall(TF.TT.TaintUnionFn, {L1, L2, Op, Size, Op1, Op2}); + C->addRetAttr(Attribute::ZExt); + C->addParamAttr(0, Attribute::ZExt); + C->addParamAttr(1, Attribute::ZExt); + return C; + }; + + // Extract byte i → input bits [i*8+7 : i*8] + SmallVector ByteLabels(NumBytes); + for (unsigned I = 0; I < NumBytes; ++I) + ByteLabels[I] = MakeUnionCall(Shadow, ZeroShadow, ExtractOp, ByteSize, + Zero64, ConstantInt::get(TF.TT.Int64Ty, I * 8)); + + // Concat reversed: result LSB = input byte[NumBytes-1], MSB = input byte[0] + // Build: z3::concat(byte[0], concat(byte[1], ... concat(byte[N-2], byte[N-1])...)) + // Using __taint_union(l1=low, l2=high) → z3::concat(l2, l1) + Value *Result = ByteLabels[NumBytes - 1]; + unsigned AccumBits = 8; + for (int I = (int)NumBytes - 2; I >= 0; --I) { + AccumBits += 8; + Value *CSize = ConstantInt::get(TF.TT.Int16Ty, AccumBits); + Result = MakeUnionCall(Result, ByteLabels[I], ConcatOp, CSize, Zero64, Zero64); + } + + TF.setShadow(&CB, Result); + return; + } + + // Other intrinsics: symbolic propagation not yet implemented — skip. } void TaintVisitor::visitCallBase(CallBase &CB) { @@ -3141,6 +4260,20 @@ void TaintVisitor::visitCallBase(CallBase &CB) { return; } + // handle ucsan_check_pointer / ucsan_uncheck_pointer: both return a pointer + // that aliases their first argument (real<->pseudo translation), so the + // symsan taint label flows straight through from arg 0. + if (F && ClWithUCSan && + (F->getName().equals("ucsan_check_pointer") || + F->getName().equals("ucsan_uncheck_pointer"))) { + // just propagate the label + Value *Shadow = TF.getShadow(CB.getArgOperand(0)); + if (!TF.TT.isZeroShadow(Shadow)) { + TF.setShadow(&CB, Shadow); + } + return; + } + // Calls to this function are synthesized in wrappers, and we shouldn't // instrument them. if (F == TF.TT.TaintVarargWrapperFn.getCallee()->stripPointerCasts()) @@ -3149,10 +4282,18 @@ void TaintVisitor::visitCallBase(CallBase &CB) { IRBuilder<> IRB(&CB); // trace indirect call + bool isUCSanCheckedIndirectCall = false; if (CB.getCalledFunction() == nullptr) { Value *Shadow = TF.getShadow(CB.getCalledOperand()); if (!TF.TT.isZeroShadow(Shadow)) IRB.CreateCall(TF.TT.TaintTraceIndirectCallFn, {Shadow}); + + // Check if the function pointer is from UCSan (ucsan_check_pointer) + Value *FPtr = CB.getCalledOperand()->stripPointerCasts(); + auto *FPtrInst = dyn_cast(FPtr); + if (ClWithUCSan || (FPtrInst && FPtrInst->getMetadata("ucsan.checked"))) { + isUCSanCheckedIndirectCall = true; + } } DenseMap::iterator UnwrappedFnIt = @@ -3189,6 +4330,21 @@ void TaintVisitor::visitCallBase(CallBase &CB) { Instruction *Next = nullptr; if (!CB.getType()->isVoidTy()) { + // For UCSan-checked indirect calls, find the PHINode introduced by UCSanPass. + // The return value may come from either the actual call or ucsan_wrap_retval, + // merged via a PHINode. We need to load the shadow after the PHINode. + PHINode *RetPhiNode = nullptr; + Instruction *ShadowTarget = &CB; + if (isUCSanCheckedIndirectCall) { + for (User *U : CB.users()) { + if (PHINode *PN = dyn_cast(U)) { + RetPhiNode = PN; + ShadowTarget = PN; + break; + } + } + } + if (InvokeInst *II = dyn_cast(&CB)) { if (II->getNormalDest()->getSinglePredecessor()) { Next = &II->getNormalDest()->front(); @@ -3199,7 +4355,12 @@ void TaintVisitor::visitCallBase(CallBase &CB) { } } else { assert(CB.getIterator() != CB.getParent()->end()); - Next = CB.getNextNode(); + if (RetPhiNode) { + // Load shadow after the PHINode + Next = RetPhiNode->getParent()->getFirstNonPHI(); + } else { + Next = CB.getNextNode(); + } } // Don't emit the epilogue for musttail call returns. @@ -3211,13 +4372,13 @@ void TaintVisitor::visitCallBase(CallBase &CB) { unsigned Size = DL.getTypeAllocSize(TF.TT.getShadowTy(&CB)); if (Size > RetvalTLSSize) { // Set overflowed return shadow to be zero. - TF.setShadow(&CB, TF.TT.getZeroShadow(&CB)); + TF.setShadow(ShadowTarget, TF.TT.getZeroShadow(&CB)); } else { LoadInst *LI = NextIRB.CreateAlignedLoad( TF.TT.getShadowTy(&CB), TF.getRetvalTLS(CB.getType(), NextIRB), ShadowTLSAlignment, "_dfsret"); TF.SkipInsts.insert(LI); - TF.setShadow(&CB, LI); + TF.setShadow(ShadowTarget, LI); TF.NonZeroChecks.push_back(LI); } } @@ -3287,7 +4448,6 @@ void TaintFunction::visitCondition(Value *Condition, Instruction *I) { } void TaintVisitor::visitBranchInst(BranchInst &BR) { - if (BR.getMetadata("nosanitize")) return; if (BR.isUnconditional()) return; TF.visitCondition(BR.getCondition(), &BR); } diff --git a/instrumentation/UCSanPass.cpp b/instrumentation/UCSanPass.cpp new file mode 100644 index 00000000..6a16c5fa --- /dev/null +++ b/instrumentation/UCSanPass.cpp @@ -0,0 +1,4277 @@ +//===- UCSanPass.cpp - Under-Constrained Execution Pass ------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +/// \file +/// This file implements the UCSan pass for under-constrained execution. +/// +/// CONCEPTUAL OVERVIEW: +/// ==================== +/// UCSan enables safe execution of functions in isolation, even with +/// uninitialized or invalid pointers. Given a YAML metadata file specifying +/// an entry point and scope, UCSan performs three key transformations: +/// +/// 1. SCOPE-BASED FUNCTION REMOVAL: +/// - All functions NOT in scope are removed from the module +/// - Replaced with "dangle" wrappers that safely handle out-of-scope calls +/// - Enables testing individual functions without complete program context +/// +/// 2. LAZY POINTER INITIALIZATION (checkPointer): +/// - Instruments all pointer dereferences (loads, stores, memops) +/// - Takes a pseudo-pointer as input, translate to valid pointer +/// - Check if the memory object has been allocated; +// if not, allocates memory on-demand at runtime +/// - This is the KEY feature enabling under-constrained execution +/// +/// 3. MEMORY OBJECT SIZE TRACKING: +/// - Tracks GEP offsets and Cast operations +/// - Estimates sizes of underlying memory objects +/// - Enables runtime bounds checking (checkBounds) +/// +/// STANDALONE CAPABILITY: +/// ====================== +/// UCSan-instrumented binaries can run WITHOUT symbolic execution. They provide +/// safe under-constrained execution with lazy allocation. To add symbolic +/// reasoning, the IR is pipelined to TaintPass (SymSan), which runs AFTER this +/// pass and adds constraint collection. +/// +/// INTEGRATION WITH TaintPass: +/// =========================== +/// - This pass marks all instrumented instructions with "ucsan.checked" metadata +/// - TaintPass checks this metadata and skips redundant pointer validation +/// - Enables efficient pipeline: UCSanPass (safety) → TaintPass (symbolics) +/// +//===----------------------------------------------------------------------===// + +#include "UCSanSummary.h" + +#include "llvm/ADT/None.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/Instructions.h" +#include "llvm/IR/IRBuilder.h" +#include "llvm/IR/InstVisitor.h" +#include "llvm/IR/Dominators.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/DataLayout.h" +#include "llvm/IR/IntrinsicInst.h" +#include "llvm/IR/Operator.h" +#include "llvm/IR/Metadata.h" +#include "llvm/IR/MDBuilder.h" +#include "llvm/IR/Attributes.h" +#include "llvm/IR/InlineAsm.h" +#include "llvm/IR/PassManager.h" +#include "llvm/Passes/OptimizationLevel.h" +#include "llvm/Passes/PassBuilder.h" +#include "llvm/Passes/PassPlugin.h" +#include "llvm/Support/YAMLParser.h" +#include "llvm/Support/YAMLTraits.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/SpecialCaseList.h" +#include "llvm/Support/VirtualFileSystem.h" +#include "llvm/Transforms/Utils/BasicBlockUtils.h" +#include "llvm/Transforms/Utils/Local.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/DepthFirstIterator.h" +#include "llvm/Analysis/ValueTracking.h" +#include "llvm/IR/DebugInfo.h" +#include "llvm/IR/DebugInfoMetadata.h" +#include "llvm/BinaryFormat/Dwarf.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include +#include +#include + +using namespace llvm; + +// Demangle a C++ mangled name. If demangling fails (e.g. plain C name), +// return the original name unchanged. +static std::string demangleName(StringRef MangledName) { + int Status = -1; + char *Demangled = abi::__cxa_demangle( + MangledName.str().c_str(), nullptr, nullptr, &Status); + if (Status == 0 && Demangled) { + std::string Result(Demangled); + free(Demangled); + return Result; + } + return MangledName.str(); +} + +namespace { + +// Constants for shadow memory alignment +// Must match sizeof(ucsan_label) = sizeof(i16) = 2 bytes +static const Align ShadowTLSAlignment = Align(2); +static const unsigned ArgTLSSize = 800; +static const unsigned kRetvalTLSSize = 800; + +static const char *BBIDName = "dfsan.bb"; +static const unsigned BBID_STEP = 100000; // Max 100k BBs per function + +// Command line options +static cl::opt ClTraceBound("ucsan-trace-bound", + cl::desc("Enable bounds checking"), + cl::Hidden, cl::init(true)); + +static cl::opt ClTraceBB("ucsan-trace-bb", + cl::desc("Enable basic block tracing"), + cl::Hidden, cl::init(false)); + +static cl::opt ClDumpCfg("ucsan-dump-cfg", + cl::desc("Dump control flow info to file"), + cl::Hidden); + +static cl::opt ClWithTaintPass( + "ucsan-with-taint", + cl::desc("TaintPass will run after UCSanPass, defer taint custom functions"), + cl::Hidden, cl::init(false)); + +static cl::opt ClTypeTable("ucsan-type-table", + cl::desc("Emit type table JSON to file (loaded/appended across modules)"), + cl::Hidden); + +// The ABI list files control how shadow parameters are passed. +static cl::list ClABIListFiles( + "ucsan-abilist", + cl::desc("File listing native ABI functions and how the pass treats them"), + cl::Hidden); + +class UCSanABIList { + std::unique_ptr SCL; + + public: + UCSanABIList() = default; + + void set(std::unique_ptr List) { SCL = std::move(List); } + + bool isValid() const { return SCL != nullptr; } + + /// Returns whether either this function or its source file are listed in the + /// given category. + bool isIn(const Function &F, StringRef Category) const { + if (!SCL) return false; + return isIn(*F.getParent(), Category) || + SCL->inSection("taint", "fun", F.getName(), Category); + } + + /// Returns whether this module is listed in the given category. + bool isIn(const Module &M, StringRef Category) const { + if (!SCL) return false; + return SCL->inSection("taint", "src", M.getModuleIdentifier(), Category); + } + + /// Returns whether the given function name is listed in the given category. + bool isIn(StringRef Name, StringRef Category) const { + if (!SCL) return false; + return SCL->inSection("taint", "fun", Name, Category); + } + + /// Returns the 0-indexed argument that holds the byte count for the named + /// function, or -1 if none is listed. Probes categories size0..size7. + int getSizeArgIdx(StringRef Name) const { + if (!SCL) return -1; + for (int i = 0; i < 8; ++i) { + std::string Cat = "size" + std::to_string(i); + if (SCL->inSection("taint", "fun", Name, Cat)) + return i; + } + return -1; + } + + // Returns the argument index whose pointer the function's return value + // aliases (e.g. strchr/memchr return haystack+index → retptr0), or -1. + // Used to translate the materialized return pointer back into UC space. + int getRetPtrArgIdx(StringRef Name) const { + if (!SCL) return -1; + for (int i = 0; i < 8; ++i) { + std::string Cat = "retptr" + std::to_string(i); + if (SCL->inSection("taint", "fun", Name, Cat)) + return i; + } + return -1; + } +}; + +namespace { + +// Memory map parameters used in application-to-shadow address calculation. +// Offset = (Addr & ~AndMask) ^ XorMask +// Shadow = ShadowBase + Offset * ShadowWidthBytes +struct MemoryMapParams { + uint64_t AndMask; + uint64_t XorMask; + uint64_t ShadowBase; +}; + +} // end anonymous namespace + +// x86_64 Linux +// NOLINTNEXTLINE(readability-identifier-naming) +// UCSan shadow memory layout (x86_64) +// Shadow: 0x480000000000 - 0x680000000000 (2TB) +static const MemoryMapParams Linux_X86_64_MemoryMapParams = { + 0x700000000000, // AndMask (keep old style) + 0, // XorMask (not used) + 0x480000000000, // ShadowBase (not used) +}; + +/// TransformedFunction is used to express the result of transforming one +/// function type into another. This struct is immutable. It holds metadata +/// useful for updating calls of the old function to the new type. +struct TransformedFunction { + TransformedFunction(FunctionType* OriginalType, + FunctionType* TransformedType, + std::vector ArgumentIndexMapping) + : OriginalType(OriginalType), + TransformedType(TransformedType), + ArgumentIndexMapping(ArgumentIndexMapping) {} + + // Disallow copies. + TransformedFunction(const TransformedFunction&) = delete; + TransformedFunction& operator=(const TransformedFunction&) = delete; + + // Allow moves. + TransformedFunction(TransformedFunction&&) = default; + TransformedFunction& operator=(TransformedFunction&&) = default; + + /// Type of the function before the transformation. + FunctionType *OriginalType; + + /// Type of the function after the transformation. + FunctionType *TransformedType; + + /// Transforming a function may change the position of arguments. This + /// member records the mapping from each argument's old position to its new + /// position. Argument positions are zero-indexed. + std::vector ArgumentIndexMapping; +}; + +/// Given function attributes from a call site for the original function, +/// return function attributes appropriate for a call to the transformed +/// function. +AttributeList +TransformFunctionAttributes(const TransformedFunction& TransformedFunction, + LLVMContext& Ctx, AttributeList CallSiteAttrs) { + + // Construct a vector of AttributeSet for each function argument. + std::vector ArgumentAttributes( + TransformedFunction.TransformedType->getNumParams()); + + // Copy attributes from the parameter of the original function to the + // transformed version. 'ArgumentIndexMapping' holds the mapping from + // old argument position to new. + for (unsigned I = 0, IE = TransformedFunction.ArgumentIndexMapping.size(); + I < IE; ++I) { + unsigned TransformedIndex = TransformedFunction.ArgumentIndexMapping[I]; + ArgumentAttributes[TransformedIndex] = CallSiteAttrs.getParamAttrs(I); + } + + // Copy annotations on varargs arguments. + for (unsigned I = TransformedFunction.OriginalType->getNumParams(), + IE = CallSiteAttrs.getNumAttrSets(); + I < IE; ++I) { + ArgumentAttributes.push_back(CallSiteAttrs.getParamAttrs(I)); + } + + return AttributeList::get(Ctx, CallSiteAttrs.getFnAttrs(), + CallSiteAttrs.getRetAttrs(), + llvm::makeArrayRef(ArgumentAttributes)); +} + +// YAML structures for metadata parsing +struct ArgMapEntry { + int idx; + int ucsan_idx; +}; + +struct UCSanScopeCustom { + std::string ref_name; + std::vector arg_maps; + // If true, the wrapper returns the logical NOT of the ref's return value + // (cast to the wrapper's return type). Useful for adapters whose truth + // sense is inverted from the ref's, e.g. bytesEqual→strncmp. + bool invert_ret = false; +}; + +struct UCSanScope { + std::string entry; + std::vector scope; + std::vector shims_yaml; // parsed from YAML + std::map shims; // expanded: orig->true, __shim_orig->false + std::map custom; +}; + +} // namespace + +// YAML traits for parsing +namespace llvm { +namespace yaml { + +template<> +struct MappingTraits { + static void mapping(yaml::IO &io, ArgMapEntry &entry) { + io.mapRequired("idx", entry.idx); + io.mapRequired("ucsan_idx", entry.ucsan_idx); + } +}; + +template<> +struct SequenceTraits> { + static size_t size(yaml::IO &io, std::vector &seq) { + return seq.size(); + } + static ArgMapEntry& element(yaml::IO &io, std::vector &seq, size_t index) { + if (index >= seq.size()) + seq.resize(index + 1); + return seq[index]; + } +}; + +template<> +struct MappingTraits { + static void mapping(yaml::IO &io, UCSanScopeCustom &c) { + io.mapRequired("ref_name", c.ref_name); + io.mapRequired("arg_maps", c.arg_maps); + io.mapOptional("invert_ret", c.invert_ret, false); + } +}; + +template<> +struct MappingTraits { + static void mapping(yaml::IO &io, UCSanScope &sc) { + io.mapRequired("entry", sc.entry); + io.mapOptional("scope", sc.scope); + io.mapOptional("shims", sc.shims_yaml); + io.mapOptional("custom", sc.custom); + } +}; + +template<> +struct CustomMappingTraits> { + static void inputOne(IO &io, StringRef Key, std::map &V) { + io.mapRequired(Key.str().c_str(), V[Key.str()]); + } + + static void output(IO &io, std::map &V) { + for (auto &Entry : V) { + io.mapRequired(Entry.first.c_str(), Entry.second); + } + } +}; + +} // namespace yaml +} // namespace llvm + +namespace { + +class UCSan { + friend struct UCSanFunction; + friend class UCSanVisitor; + + // UCSan uses 16-bit labels (separate from SymSan's 32-bit labels) + enum { + ShadowWidthBits = 16, + ShadowWidthBytes = ShadowWidthBits / 8 + }; + + enum WrapperKind { + WK_None, + WK_Custom, + WK_TaintCustom, // custom in dfsan abilist, defer to TaintPass if available + WK_AutoCustom, + WK_ShimOrig, // original function to be replaced by __shim_ version + WK_ShimTarget, // LLM-generated __shim_ function, instrumented like in-scope + WK_Uninstrumented, + WK_Discard, + WK_Ignore, + }; + + Module *Mod; + LLVMContext *Ctx; + + // Basic types + IntegerType *Int8Ty; + IntegerType *Int16Ty; + IntegerType *Int32Ty; + IntegerType *Int64Ty; + IntegerType *Int1Ty; + IntegerType *IntptrTy; + PointerType *VoidPtrTy; + + // UCSan still needs sanitizer shadow memory + IntegerType *PrimitiveShadowTy; + PointerType *PrimitiveShadowPtrTy; + ConstantInt *ZeroPrimitiveShadow; + ConstantInt *UninitPrimitiveShadow; + ConstantInt *ShadowPtrAndMask; + ConstantInt *ShadowPtrXorMask; + ConstantInt *ShadowPtrBase; + ConstantInt *ShadowPtrMul; + Constant *ArgTLS; + Constant *RetvalTLS; + + // Runtime function types + FunctionType *UCCheckPointerFnTy; + FunctionType *UCUncheckPointerFnTy; + FunctionType *UCCheckPointerArgFnTy; + FunctionType *UCCheckUBIFnTy; + FunctionType *UCCombineLabelFnTy; + FunctionType *UCSetLabelForArgsFnTy; + FunctionType *UCLoadPointerShadowFnTy; + FunctionType *UCStorePointerShadowFnTy; + FunctionType *UCResignShadowFnTy; + FunctionType *UCWrapRetvalFnTy; + FunctionType *UCSetLabelFnTy; + FunctionType *UCTraceBBFnTy; + FunctionType *UCTraceAllocaFnTy; + FunctionType *UCTraceGlobalFnTy; + FunctionType *UCPushStackFrameFnTy; + FunctionType *UCPopStackFrameFnTy; + FunctionType *UCCheckCopyBoundsFnTy; + + // Runtime functions + FunctionCallee UCCheckPointerFn; + FunctionCallee UCUncheckPointerFn; + FunctionCallee UCCheckPointerArgFn; + FunctionCallee UCCheckUBIFn; + FunctionCallee UCCombineLabelFn; + FunctionCallee UCSetLabelForArgsFn; + FunctionCallee UCLoadPointerShadowFn; + FunctionCallee UCStorePointerShadowFn; + FunctionCallee UCResignShadowFn; + FunctionCallee UCWrapRetvalFn; + FunctionCallee UCSetLabelFn; + FunctionCallee UCTraceBBFn; + FunctionCallee UCTraceAllocaFn; + FunctionCallee UCTraceGlobalFn; + FunctionCallee UCPushStackFrameFn; + FunctionCallee UCPopStackFrameFn; + FunctionCallee UCCheckCopyBoundsFn; + // FunctionCallee PopulateDataSegmentFn; + + FunctionCallee LLVMReturnAddrFn; + FunctionCallee ExitFn; + + SmallPtrSet UCRuntimeFunctions; + + /// Memory map parameters used in calculation mapping application addresses + /// to shadow addresses and origin addresses. + const MemoryMapParams *MapParams; + + // Metadata-driven scope + UCSanScope Scope; + std::map CustomFuncs; + std::map CustomFuncTypes; + DenseMap UnwrappedFnMap; + + // ABIList for custom function detection + UCSanABIList ABIList; + AttributeMask ReadOnlyNoneAttrs; + + // Type ID assignment for sidecar type table + std::map TypeIDMap; // type key -> type_id + std::map TypeIDToType; // type_id -> LLVM Type* + std::map LoadedTypes; // type_id -> JSON from previous modules + // Builtin type IDs (fixed across modules): + // 1=i1, 2=i8, 3=i16, 4=i32, 5=i64, 6=float, 7=double + static const uint32_t kFirstDynamicTypeID = 16; // IDs 1-15 reserved for builtins + uint32_t NextTypeID = kFirstDynamicTypeID; + + void initBuiltinTypeIDs(); + + // Entry function arg info for type table + struct ArgInfo { + std::string name; + uint32_t type_id; + }; + std::string EntryFunctionName; + std::vector EntryArgs; + + uint32_t getOrCreateTypeID(Type *T); + std::string getTypeKey(Type *T); + void emitTypeTable(); + void loadTypeTable(); + + void initializeRuntimeFunctions(Module &M); + void initializeCustomFunctionTypes(); + bool loadMetadata(); + bool initializeModule(Module &M); + + Function *buildDangleFunction(Function *F); + Function *buildDiscardFunction(Function *F); + Function *buildDriverWrapperFunction(Function *F); + Function *getCustomFunction(const Function *F); + + // Build the final wrapper return value, applying invert_ret if requested. + // For invert on integer returns: returns `(ret == 0) ? 1 : 0`. Targets are + // comparators (memcmp/strcmp/...) whose `*ret_label` solver-side wants a + // direct `== 0` / `!= 0` constraint anyway, so the extra icmp is desirable. + Value *invertIntRet(Value *RetVal, Type *WrapperRetTy, bool Invert, + IRBuilder<> &IRB); + + WrapperKind getWrapperKind(Function *F); + TransformedFunction getCustomFunctionType(FunctionType *T); + + // Helper functions for shadow management (simplified for standalone) + Value *getShadowAddress(Value *Addr, IRBuilder<> &IRB); + + /// Returns a zero constant with the shadow type of OrigTy. + /// + /// getZeroShadow({T1,T2,...}) = {getZeroShadow(T1),getZeroShadow(T2,...} + /// getZeroShadow([n x T]) = [n x getZeroShadow(T)] + /// getZeroShadow(other type) = i32(0) + /// + /// Note that a zero shadow is always i32(0) when shouldTrackFieldsAndIndices + /// returns false. + Constant *getZeroShadow(Type *OrigTy); + /// Returns a zero constant with the shadow type of V's type. + Constant *getZeroShadow(Value *V); + + /// Checks if V is a zero shadow. + bool isZeroShadow(Value *V); + + /// Returns the shadow type of OrigTy. + /// + /// getShadowTy({T1,T2,...}) = {getShadowTy(T1),getShadowTy(T2),...} + /// getShadowTy([n x T]) = [n x getShadowTy(T)] + /// getShadowTy(other type) = i32 + /// + /// Note that a shadow type is always i32 when shouldTrackFieldsAndIndices + /// returns false. + Type *getShadowTy(Type *OrigTy); + /// Returns the shadow type of of V's type. + Type *getShadowTy(Value *V); + + /// Returns an uninitialized shadow value with the shadow type of OrigTy. + Constant *getUninitShadow(Type *OrigTy); + + /// Marks an instruction as "nosanitize" so TaintPass will skip it + inline void markNosanitize(Value *V) { + Instruction *I = dyn_cast(V); + if (I) I->setMetadata("nosanitize", MDNode::get(*Ctx, None)); + } + + /// Marks a function as "nosanitize" so TaintPass will skip instrumenting it + inline void markFunctionNosanitize(Function *F) { + if (F) F->addFnAttr(Attribute::DisableSanitizerInstrumentation); + } + +public: + UCSan() = default; + + bool runImpl(Module &M); +}; + +struct UCSanFunction { + UCSan &UC; + Function *F; + DominatorTree DT; + + Value *ArgTLSPtr = nullptr; + Value *RetvalTLSPtr = nullptr; + AllocaInst *LabelReturnAlloca = nullptr; + DenseMap ValShadowMap; + DenseMap AllocaShadowMap; + + struct PHIFixupElement { + PHINode *Phi; + PHINode *ShadowPhi; + }; + std::vector PHIFixups; + + DenseSet SkipInsts; + SmallVector RemovalInsts; + + struct CachedShadow { + BasicBlock *Block; // The block where Shadow is defined. + Value *Shadow; + }; + /// Maps a value to its latest shadow value in terms of domination tree. + DenseMap, CachedShadow> CachedShadows; + + // Pointer checking state + DenseMap CheckedPtrMap; + DenseSet CheckedPtrSet; + SmallVector NonZeroChecks; + + struct LoadedPointerSummary { + unsigned ArgNo; + int64_t FieldOffset; + }; + + using MemoryAccessSummary = symsan::ucsan::MemoryAccessSummary; + DenseMap LoadedPointerSummaries; + SmallVector MemoryAccessSummaries; + + /// Computes the shadow address for a given function argument. + /// + /// Shadow = ArgTLS+ArgOffset. + Value *getArgTLS(Type *T, unsigned ArgOffset, IRBuilder<> &IRB); + + /// Computes the shadow address for a retval. + Value *getRetvalTLS(Type *T, IRBuilder<> &IRB); + + Value *getShadow(Value *V); + void setShadow(Instruction *I, Value *Shadow); + + /// XXX: because we never collapse taint labels for aggregate types, + /// we also do not expand taint labels from an aggreated primitive + /// shadow value. Instead, we always load the label for each + /// primitive field. + /// + /// Load all primitive subtypes of T, returning the aggrate shadow value. + /// + /// LS({T1,T2, ...}, Addr) = {LS(T1, SubAdrr),LS(T2, SubAddr),...} + /// LS([n x T], Addr) = [n x LS(T, SubAddr)] + /// LS(other types, Addr) = LS(PS, Addr) + Value *loadShadow(Value *Addr, uint64_t Size, Align Align, Type *Ty, Instruction *Pos); + + /// XXX: we do not union taint labels for aggregate types before store; + /// instead, we store each privimitive field individually. + /// + /// Store all primitive subtypes of T, using the aggrate shadow value. + /// + /// SS(Addr, {T1,T2, ...}) = SS(SubAddr, T1), SS(SubAddr, T2), ... + /// SS(Addr, [T1,T2,...]) = SS(SubAddr, T1), SS(SubAddr, T2), ... + /// SS(Addr, PS) = SS(Addr, PS) + void storeShadow(Value *Addr, uint64_t Size, Align Align, Value *Shadow, Type *Ty, Instruction *Pos); + + Value *checkPointer(Value *Ptr, Value *Size, bool dereference, IRBuilder<> &IRB, + uint32_t TypeID = 0); + void recordPointerLoadSummary(LoadInst &LI); + void recordMemoryAccessSummary(Value *Ptr, uint64_t AccessSize, bool IsWrite, + uint32_t TypeID, Instruction *I); + void emitMemoryAccessSummaries(); + + UCSanFunction(UCSan &UC, Function *F) + : UC(UC), F(F) { + DT.recalculate(*F); + } + +private: + /// Loads a primivite shadow label + Value *loadPrimitiveShadow(Value *Addr, uint64_t Size, Align Align, + Type *Ty, IRBuilder<> &IRB); + /// Loads shadow recursively for aggregate types + Value *loadShadowRecursive(Value *Shadow, SmallVector &Indices, + Type *SubTy, Value *Addr, uint64_t Size, + Align Align, IRBuilder<> &IRB); + /// Stores an aggregate shadow label + void storeShadowRecursive(Value *Shadow, SmallVector &Indices, + Type *SubTy, Value *Addr, uint64_t Size, + Align Align, IRBuilder<> &IRB); + /// Returns the shadow value of an argument A. + Value *getShadowForTLSArgument(Argument *A); +}; + +class UCSanVisitor : public InstVisitor { +public: + UCSanFunction &UF; + + UCSanVisitor(UCSanFunction &UF) : UF(UF) {} + + const DataLayout &getDataLayout() const { + return UF.F->getParent()->getDataLayout(); + } + + void visitCastInst(CastInst &CI); + void visitCallBase(CallBase &CB); + void visitReturnInst(ReturnInst &RI); + void visitAllocaInst(AllocaInst &AI); + void visitBranchInst(BranchInst &BI); + void visitBinaryOperator(BinaryOperator &BO); + void visitCmpInst(CmpInst &CI); + void visitAtomicRMWInst(AtomicRMWInst &I); + void visitLoadInst(LoadInst &LI); + void visitStoreInst(StoreInst &SI); + void visitMemCpyInst(MemCpyInst &I); + void visitMemSetInst(MemSetInst &I); + void visitMemMoveInst(MemMoveInst &I); + void visitGetElementPtrInst(GetElementPtrInst &GEPI); + void visitSelectInst(SelectInst &I); + void visitPHINode(PHINode &PN); + void visitUnreachableInst(UnreachableInst &I); + // void visitIntrinsicInst(IntrinsicInst &I); + +private: + // Returns false when this is an invoke of a custom function. + bool visitWrappedCallBase(Function *F, CallBase &CB); + void visitIndirectCallBase(Value *CV, CallBase &CB); + void visitInlineAsm(InlineAsm *IA, CallBase &CB); +}; + +// Initialize runtime functions +void UCSan::initializeRuntimeFunctions(Module &M) { + Function *F = nullptr; + + // Pointer checking: void* ucsan_check_pointer(void*, i16, i64, i1, i32) + UCCheckPointerFnTy = FunctionType::get( + VoidPtrTy, + {VoidPtrTy, PrimitiveShadowTy, Int64Ty, Int1Ty, Int32Ty}, + false); + UCCheckPointerFn = M.getOrInsertFunction("ucsan_check_pointer", UCCheckPointerFnTy); + F = dyn_cast(UCCheckPointerFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Inverse pointer translation: void* ucsan_uncheck_pointer(void*, i16) + // Converts a real pointer returned by a libc call (e.g. strchr -> haystack + + // index in the materialized buffer) back into the caller's UC pseudo space, + // using the haystack pointer's shadow label to find the backing object. + UCUncheckPointerFnTy = FunctionType::get( + VoidPtrTy, + {VoidPtrTy, PrimitiveShadowTy}, + false); + UCUncheckPointerFn = M.getOrInsertFunction("ucsan_uncheck_pointer", UCUncheckPointerFnTy); + F = dyn_cast(UCUncheckPointerFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Pointer argument checking: void ucsan_check_ptr_arg(i16*, i32, void*) + UCCheckPointerArgFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + {PrimitiveShadowPtrTy, Int32Ty, VoidPtrTy}, + false); + UCCheckPointerArgFn = M.getOrInsertFunction("ucsan_check_ptr_arg", UCCheckPointerArgFnTy); + F = dyn_cast(UCCheckPointerArgFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // UBI checking: void ucsan_check_ubi(i16) + UCCheckUBIFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + {PrimitiveShadowTy}, + false); + UCCheckUBIFn = M.getOrInsertFunction("ucsan_check_ubi", UCCheckUBIFnTy); + F = dyn_cast(UCCheckUBIFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Label combination for binary ops: i16 ucsan_combine_label(i16, i16) + UCCombineLabelFnTy = FunctionType::get( + PrimitiveShadowTy, + {PrimitiveShadowTy, PrimitiveShadowTy}, + false); + UCCombineLabelFn = M.getOrInsertFunction("ucsan_combine_label", UCCombineLabelFnTy); + F = dyn_cast(UCCombineLabelFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Shadow resignation: i16 ucsan_resign_shadow(void*, i16*, i64, void*) + UCResignShadowFnTy = FunctionType::get( + PrimitiveShadowTy, + {VoidPtrTy, PrimitiveShadowPtrTy, Int64Ty, VoidPtrTy}, + false); + UCResignShadowFn = M.getOrInsertFunction("ucsan_resign_shadow", UCResignShadowFnTy); + F = dyn_cast(UCResignShadowFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Return value wrapping: void* ucsan_wrap_retval(i64, i16*, i1, void*) + // Returns pointer to __ucsan_wrapped_return_tls where the actual return value is stored + UCWrapRetvalFnTy = FunctionType::get( + VoidPtrTy, + {Int64Ty, PrimitiveShadowPtrTy, Int1Ty, VoidPtrTy}, + false); + UCWrapRetvalFn = M.getOrInsertFunction("ucsan_wrap_retval", UCWrapRetvalFnTy); + F = dyn_cast(UCWrapRetvalFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Argument initialization: void* ucsan_set_label_for_args(i32, i32, i8, i64) + UCSetLabelForArgsFnTy = FunctionType::get( + VoidPtrTy, + {Int32Ty, Int32Ty, Int8Ty, Int64Ty}, + false); + UCSetLabelForArgsFn = M.getOrInsertFunction("ucsan_set_label_for_args", UCSetLabelForArgsFnTy); + F = dyn_cast(UCSetLabelForArgsFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Load pointer shadow: i16 ucsan_load_pointer_shadow(i16*, i64, i1, void*) + // 4th arg: concrete source address (for make_input addr range lookup) + UCLoadPointerShadowFnTy = FunctionType::get( + PrimitiveShadowTy, + {PrimitiveShadowPtrTy, Int64Ty, Int1Ty, VoidPtrTy}, + false); + UCLoadPointerShadowFn = M.getOrInsertFunction("ucsan_load_pointer_shadow", UCLoadPointerShadowFnTy); + F = dyn_cast(UCLoadPointerShadowFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // Store pointer shadow: void ucsan_store_pointer_shadow(i16, i16*, i64) + UCStorePointerShadowFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + {PrimitiveShadowTy, PrimitiveShadowPtrTy, Int64Ty}, + false); + UCStorePointerShadowFn = M.getOrInsertFunction("ucsan_store_pointer_shadow", UCStorePointerShadowFnTy); + F = dyn_cast(UCStorePointerShadowFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // LLVM return address intrinsic + LLVMReturnAddrFn = M.getOrInsertFunction("llvm.returnaddress", + FunctionType::get(VoidPtrTy, {Int32Ty}, false)); + UCRuntimeFunctions.insert(LLVMReturnAddrFn.getCallee()->stripPointerCasts()); + + // void exit(i32) with noreturn attribute + ExitFn = M.getOrInsertFunction("exit", + FunctionType::get(Type::getVoidTy(*Ctx), {Int32Ty}, false)); + if (Function *F = dyn_cast(ExitFn.getCallee()->stripPointerCasts())) { + F->addFnAttr(Attribute::NoReturn); + UCRuntimeFunctions.insert(F); + } + + // void ucsan_set_label(i16, void*, i64) + UCSetLabelFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + {PrimitiveShadowTy, VoidPtrTy, Int64Ty}, + false); + UCSetLabelFn = M.getOrInsertFunction("ucsan_set_label", UCSetLabelFnTy); + F = dyn_cast(UCSetLabelFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + if (ClTraceBB) { + // void __taint_trace_bb(i32, i32) + UCTraceBBFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + {Int32Ty, Int32Ty}, + false); + UCTraceBBFn = M.getOrInsertFunction("__taint_trace_bb", UCTraceBBFnTy); + F = dyn_cast(UCTraceBBFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + } + + // i16 ucsan_trace_alloca(i64 Size, i64 ElemSize, i64 Address) + // Returns a shadow label representing bounds for this stack allocation + UCTraceAllocaFnTy = FunctionType::get( + PrimitiveShadowTy, + {Int64Ty, Int64Ty, Int64Ty}, + false); + UCTraceAllocaFn = M.getOrInsertFunction("ucsan_trace_alloca", UCTraceAllocaFnTy); + F = dyn_cast(UCTraceAllocaFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // i16 ucsan_trace_global(i64 Address, i64 Size) + // Returns a shadow label representing bounds for a fixed-size global object. + UCTraceGlobalFnTy = FunctionType::get( + PrimitiveShadowTy, + {Int64Ty, Int64Ty}, + false); + UCTraceGlobalFn = M.getOrInsertFunction("ucsan_trace_global", UCTraceGlobalFnTy); + F = dyn_cast(UCTraceGlobalFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // void ucsan_push_stack_frame() + // Called at function entry to save the current stack top + UCPushStackFrameFnTy = FunctionType::get(Type::getVoidTy(*Ctx), {}, false); + UCPushStackFrameFn = M.getOrInsertFunction("ucsan_push_stack_frame", UCPushStackFrameFnTy); + F = dyn_cast(UCPushStackFrameFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // void ucsan_pop_stack_frame() + // Called at function exit to restore the stack top + UCPopStackFrameFnTy = FunctionType::get(Type::getVoidTy(*Ctx), {}, false); + UCPopStackFrameFn = M.getOrInsertFunction("ucsan_pop_stack_frame", UCPopStackFrameFnTy); + F = dyn_cast(UCPopStackFrameFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // void ucsan_check_copy_bounds(void *dst, i16 dst_label, void *src, i16 src_label, i64 dst_bound) + // Check if a copy operation can overflow the destination buffer + UCCheckCopyBoundsFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + {VoidPtrTy, PrimitiveShadowTy, VoidPtrTy, PrimitiveShadowTy, Int64Ty}, + false); + UCCheckCopyBoundsFn = M.getOrInsertFunction("ucsan_check_copy_bounds", UCCheckCopyBoundsFnTy); + F = dyn_cast(UCCheckCopyBoundsFn.getCallee()->stripPointerCasts()); + markFunctionNosanitize(F); + UCRuntimeFunctions.insert(F); + + // __ucsan_symbolize_input: declared in user source code, not emitted by pass. + // Register as runtime function to prevent dangle wrapping. + // Arg shadow is stored to __ucsan_arg_tls automatically by visitCallBase. + if (Function *MakeInputFn = M.getFunction("__ucsan_symbolize_input")) { + markFunctionNosanitize(MakeInputFn); + UCRuntimeFunctions.insert(MakeInputFn); + } +} + +void UCSan::initializeCustomFunctionTypes() { + // Custom function types will be populated based on YAML metadata + // TODO: add more custom function prototypes here + CustomFuncTypes["malloc"] = FunctionType::get(VoidPtrTy, {Int64Ty}, false); + CustomFuncTypes["free"] = FunctionType::get(Type::getVoidTy(*Ctx), {VoidPtrTy}, false); + CustomFuncTypes["realloc"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["calloc"] = FunctionType::get(VoidPtrTy, {Int64Ty, Int64Ty}, false); + CustomFuncTypes["strdup"] = FunctionType::get(VoidPtrTy, {VoidPtrTy}, false); + CustomFuncTypes["strndup"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["exit"] = FunctionType::get(Type::getVoidTy(*Ctx), {Int32Ty}, false); + + // String copy functions + CustomFuncTypes["strcpy"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["stpcpy"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["strncpy"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["strcat"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["strncat"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + + // Comparison functions + CustomFuncTypes["memcmp"] = FunctionType::get(Int32Ty, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["bcmp"] = FunctionType::get(Int32Ty, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["strcmp"] = FunctionType::get(Int32Ty, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["strncmp"] = FunctionType::get(Int32Ty, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["strcasecmp"] = FunctionType::get(Int32Ty, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["strncasecmp"] = FunctionType::get(Int32Ty, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + + // Search functions + CustomFuncTypes["memchr"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int32Ty, Int64Ty}, false); + CustomFuncTypes["memrchr"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int32Ty, Int64Ty}, false); + CustomFuncTypes["strchr"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int32Ty}, false); + CustomFuncTypes["strrchr"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int32Ty}, false); + CustomFuncTypes["strstr"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["strnstr"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy, Int64Ty}, false); + CustomFuncTypes["strpbrk"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, VoidPtrTy}, false); + CustomFuncTypes["memmem"] = FunctionType::get(VoidPtrTy, {VoidPtrTy, Int64Ty, VoidPtrTy, Int64Ty}, false); + + // Length function + CustomFuncTypes["strlen"] = FunctionType::get(Int64Ty, {VoidPtrTy}, false); +} + +bool UCSan::loadMetadata() { + char* filename = getenv("METADATA"); + if (!filename) + return false; + + int fd; + std::vector Buf; + if (sys::fs::openFileForRead(filename, fd)) { + errs() << "Warning: Cannot open metadata file: " << filename << "\n"; + return false; + } + + sys::fs::file_status Status; + if (sys::fs::status(fd, Status)) { + errs() << "Warning: Cannot stat metadata file: " << filename << "\n"; + sys::fs::closeFile(fd); + return false; + } + + Buf.resize(Status.getSize()); + auto ReadResult = sys::fs::readNativeFile(sys::fs::convertFDToNativeFile(fd), + MutableArrayRef(Buf.data(), Buf.size())); + if (!ReadResult) { + errs() << "Warning: Cannot read metadata file: " << filename << "\n"; + sys::fs::closeFile(fd); + return false; + } + + yaml::Input yin(StringRef(Buf.data(), Buf.size())); + yin >> Scope; + + if (yin.error()) { + errs() << "Error parsing YAML metadata: " << filename << "\n"; + sys::fs::closeFile(fd); + return false; + } + + sys::fs::closeFile(fd); + + // Expand shims_yaml list into shims map + for (const auto &name : Scope.shims_yaml) { + Scope.shims[name] = true; // original function + Scope.shims["__shim_" + name] = false; // shim replacement + } + + return true; +} + +//===----------------------------------------------------------------------===// +// Type ID assignment for sidecar type table +//===----------------------------------------------------------------------===// + +void UCSan::initBuiltinTypeIDs() { + // Reserve fixed IDs for primitive types so they're stable across modules + auto assign = [&](uint32_t ID, Type *T) { + std::string Key = getTypeKey(T); + TypeIDMap[Key] = ID; + TypeIDToType[ID] = T; + }; + assign(1, Int1Ty); + assign(2, Int8Ty); + assign(3, Int16Ty); + assign(4, Int32Ty); + assign(5, Int64Ty); + assign(6, Type::getFloatTy(*Ctx)); + assign(7, Type::getDoubleTy(*Ctx)); + assign(8, VoidPtrTy); +} + +std::string UCSan::getTypeKey(Type *T) { + if (auto *ST = dyn_cast(T)) { + if (ST->hasName()) + return ST->getName().str(); + } + // For non-named types, use the LLVM type string + std::string Key; + raw_string_ostream OS(Key); + T->print(OS); + return Key; +} + +uint32_t UCSan::getOrCreateTypeID(Type *T) { + std::string Key = getTypeKey(T); + auto It = TypeIDMap.find(Key); + if (It != TypeIDMap.end()) + return It->second; + uint32_t ID = NextTypeID++; + TypeIDMap[Key] = ID; + TypeIDToType[ID] = T; + // Recursively assign IDs to embedded sub-types (struct fields, array elements) + // Don't chase through pointers — those get their own checkPointer at runtime + if (auto *ST = dyn_cast(T)) { + for (unsigned I = 0, N = ST->getNumElements(); I < N; ++I) + getOrCreateTypeID(ST->getElementType(I)); + } else if (auto *AT = dyn_cast(T)) { + getOrCreateTypeID(AT->getElementType()); + } + return ID; +} + +void UCSan::loadTypeTable() { + if (ClTypeTable.empty()) return; + + auto BufOrErr = llvm::MemoryBuffer::getFile(ClTypeTable); + if (!BufOrErr) return; // file doesn't exist yet, that's ok + + auto Parsed = json::parse(BufOrErr.get()->getBuffer()); + if (!Parsed) { + errs() << "Warning: failed to parse type table: " << ClTypeTable << "\n"; + return; + } + + auto *Root = Parsed->getAsObject(); + if (!Root) return; + + // Load next_type_id + if (auto NID = Root->getInteger("next_type_id")) + NextTypeID = *NID; + + // Load existing type name->id mappings + if (auto *Types = Root->getObject("types")) { + for (auto &KV : *Types) { + auto *TypeObj = KV.second.getAsObject(); + if (!TypeObj) continue; + auto ID = TypeObj->getInteger("type_id"); + auto Name = TypeObj->getString("key"); + if (ID && Name) { + TypeIDMap[Name->str()] = *ID; + // Preserve the full JSON object for types we can't resolve to LLVM Type* + LoadedTypes[*ID] = json::Object(*TypeObj); + } + } + } +} + +void UCSan::emitTypeTable() { + if (ClTypeTable.empty()) return; + + const DataLayout &DL = Mod->getDataLayout(); + + // Build debug info map: LLVM struct name -> DICompositeType + // StructType::getName() returns e.g. "struct.node" + // DICompositeType::getName() returns e.g. "node" + std::map DebugStructMap; + DebugInfoFinder DIF; + DIF.processModule(*Mod); + for (auto *Ty : DIF.types()) { + if (auto *CT = dyn_cast(Ty)) { + if (CT->getTag() == dwarf::DW_TAG_structure_type && !CT->getName().empty()) { + // Map both "struct." and just "" for flexible matching + DebugStructMap["struct." + CT->getName().str()] = CT; + } + } + } + + json::Object Root; + Root["next_type_id"] = NextTypeID; + Root["entry_function"] = EntryFunctionName; + + // Entry args - also try to get arg names from debug info + json::Array ArgsArr; + // Find entry function's debug info for arg names + std::vector EntryArgNames; + if (Function *EntryF = Mod->getFunction(EntryFunctionName)) { + if (auto *SP = EntryF->getSubprogram()) { + for (auto *N : SP->getRetainedNodes()) { + if (auto *DV = dyn_cast(N)) { + if (DV->isParameter()) { + unsigned Idx = DV->getArg() - 1; // DILocalVariable arg is 1-based + if (Idx >= EntryArgNames.size()) + EntryArgNames.resize(Idx + 1); + EntryArgNames[Idx] = DV->getName().str(); + } + } + } + } + } + for (unsigned I = 0; I < EntryArgs.size(); ++I) { + json::Object ArgObj; + std::string Name = EntryArgs[I].name; + if (Name.empty() && I < EntryArgNames.size()) + Name = EntryArgNames[I]; + ArgObj["name"] = Name; + ArgObj["type_id"] = EntryArgs[I].type_id; + ArgsArr.push_back(std::move(ArgObj)); + } + Root["args"] = std::move(ArgsArr); + + // Types + json::Object TypesObj; + for (auto &KV : TypeIDToType) { + uint32_t ID = KV.first; + Type *T = KV.second; + json::Object TypeObj; + TypeObj["type_id"] = ID; + TypeObj["key"] = getTypeKey(T); + + if (auto *ST = dyn_cast(T)) { + TypeObj["kind"] = "struct"; + TypeObj["name"] = ST->hasName() ? ST->getName().str() : "(anonymous)"; + if (ST->isSized()) { + const StructLayout *SL = DL.getStructLayout(ST); + TypeObj["size"] = (int64_t)DL.getTypeAllocSize(ST); + + // Look up field names from debug info + DICompositeType *DCT = nullptr; + if (ST->hasName()) { + auto It = DebugStructMap.find(ST->getName().str()); + if (It != DebugStructMap.end()) + DCT = It->second; + } + + json::Array Fields; + for (unsigned I = 0, N = ST->getNumElements(); I < N; ++I) { + json::Object Field; + Field["index"] = I; + Field["offset"] = (int64_t)SL->getElementOffset(I); + Field["size"] = (int64_t)DL.getTypeAllocSize(ST->getElementType(I)); + Field["type_id"] = getOrCreateTypeID(ST->getElementType(I)); + + // Extract field name from debug info + if (DCT) { + auto Elements = DCT->getElements(); + if (I < Elements.size()) { + if (auto *DT = dyn_cast(Elements[I])) { + Field["name"] = DT->getName().str(); + } + } + } + + Fields.push_back(std::move(Field)); + } + TypeObj["fields"] = std::move(Fields); + } + } else if (auto *AT = dyn_cast(T)) { + TypeObj["kind"] = "array"; + TypeObj["num_elements"] = (int64_t)AT->getNumElements(); + TypeObj["element_type_id"] = getOrCreateTypeID(AT->getElementType()); + if (T->isSized()) + TypeObj["size"] = (int64_t)DL.getTypeAllocSize(T); + } else if (auto *PT = dyn_cast(T)) { + TypeObj["kind"] = "pointer"; + TypeObj["size"] = (int64_t)DL.getTypeAllocSize(T); + Type *PointeeTy = PT->getPointerElementType(); + if (PointeeTy->isSized()) + TypeObj["pointee_type_id"] = getOrCreateTypeID(PointeeTy); + else + TypeObj["pointee_type_id"] = 0; + } else { + // Primitive types (integers, floats, etc.) + TypeObj["kind"] = "primitive"; + std::string Name; + raw_string_ostream OS(Name); + T->print(OS); + TypeObj["name"] = Name; + if (T->isSized()) + TypeObj["size"] = (int64_t)DL.getTypeAllocSize(T); + } + + TypesObj[std::to_string(ID)] = std::move(TypeObj); + } + + // Merge in loaded types from previous modules that weren't re-encountered + for (auto &KV : LoadedTypes) { + std::string Key = std::to_string(KV.first); + if (!TypesObj.get(Key)) + TypesObj[Key] = json::Value(json::Object(KV.second)); + } + + Root["types"] = std::move(TypesObj); + + // Write to file + std::error_code EC; + raw_fd_ostream OS(ClTypeTable, EC, sys::fs::OF_Text); + if (EC) { + errs() << "Warning: failed to write type table: " << ClTypeTable << "\n"; + return; + } + OS << json::Value(std::move(Root)); +} + +// Helper: Get shadow type for a given type (simplified for standalone UCSan) +Type *UCSan::getShadowTy(Value *V) { + return getShadowTy(V->getType()); +} + +Type *UCSan::getShadowTy(Type *OrigTy) { + if (!OrigTy->isSized()) + return PrimitiveShadowTy; + if (isa(OrigTy)) + return PrimitiveShadowTy; + if (isa(OrigTy)) + return PrimitiveShadowTy; + if (ArrayType *AT = dyn_cast(OrigTy)) + return ArrayType::get(getShadowTy(AT->getElementType()), + AT->getNumElements()); + if (StructType *ST = dyn_cast(OrigTy)) { + SmallVector Elements; + for (unsigned I = 0, N = ST->getNumElements(); I < N; ++I) + Elements.push_back(getShadowTy(ST->getElementType(I))); + return StructType::get(*Ctx, Elements); + } + return PrimitiveShadowTy; +} + +/// Returns a zero shadow constant. +Constant *UCSan::getZeroShadow(Type *OrigTy) { + if (!isa(OrigTy) && !isa(OrigTy)) + return ZeroPrimitiveShadow; + Type *ShadowTy = getShadowTy(OrigTy); + return ConstantAggregateZero::get(ShadowTy); +} + +Constant *UCSan::getZeroShadow(Value *V) { + return getZeroShadow(V->getType()); +} + +/// Checks if a value is a zero shadow. +bool UCSan::isZeroShadow(Value *V) { + Type *T = V->getType(); + if (!isa(T) && !isa(T)) { + if (const ConstantInt *CI = dyn_cast(V)) + return CI->isZero(); + return false; + } + + return isa(V); +} + +/// Returns an uninitialized shadow constant. +Constant *UCSan::getUninitShadow(Type *OrigTy) { + if (!isa(OrigTy) && !isa(OrigTy)) + return UninitPrimitiveShadow; + Type *ShadowTy = getShadowTy(OrigTy); + if (ArrayType *AT = dyn_cast(ShadowTy)) { + SmallVector Elements(AT->getNumElements(), + getUninitShadow(AT->getElementType())); + return ConstantArray::get(AT, Elements); + } else if (StructType *ST = dyn_cast(ShadowTy)) { + SmallVector Elements(ST->getNumElements()); + for (unsigned I = 0, N = ST->getNumElements(); I < N; ++I) + Elements[I] = getUninitShadow(ST->getElementType(I)); + return ConstantStruct::get(ST, Elements); + } + llvm_unreachable("Unexpected type for uninitialized shadow"); +} + +/// Get the shadow memory address for a given data address. +/// Shadow memory layout: shadow_addr = ((data_addr & mask) << 1) + ShadowBase +/// For 16-bit labels, each byte gets 2 bytes of shadow memory. +Value *UCSan::getShadowAddress(Value *Addr, IRBuilder<> &IRB) { + assert(Addr != RetvalTLS && "Reinstrumenting?"); + // Formula: ((ptr & ShadowMask) * 2) + ShadowBase + Value *OffsetLong = IRB.CreatePointerCast(Addr, IntptrTy); + markNosanitize(OffsetLong); + if (ShadowPtrAndMask) { + OffsetLong = IRB.CreateAnd(OffsetLong, ShadowPtrAndMask); + markNosanitize(OffsetLong); + } + if (ShadowPtrXorMask) { + OffsetLong = IRB.CreateXor(OffsetLong, ShadowPtrXorMask); + markNosanitize(OffsetLong); + } + if (ShadowPtrMul) { + OffsetLong = IRB.CreateMul(OffsetLong, ShadowPtrMul); + markNosanitize(OffsetLong); + } + if (ShadowPtrBase) { + OffsetLong = IRB.CreateAdd(OffsetLong, ShadowPtrBase); + markNosanitize(OffsetLong); + } + Value *ShadowAddr = IRB.CreateIntToPtr(OffsetLong, PrimitiveShadowPtrTy); + markNosanitize(ShadowAddr); + return ShadowAddr; +} + +/// Computes the shadow address for a given function argument. +/// +/// Shadow = ArgTLS + ArgOffset. +/// UCSan uses shadow memory to track pointer aliasing - which pointers +/// point to the same memory object. This is essential for lazy allocation +/// and pseudo-pointer translation. +Value *UCSanFunction::getArgTLS(Type *T, unsigned ArgOffset, IRBuilder<> &IRB) { + Value *Base = IRB.CreatePointerCast(UC.ArgTLS, UC.IntptrTy); + UC.markNosanitize(Base); + if (ArgOffset) { + Base = IRB.CreateAdd(Base, ConstantInt::get(UC.IntptrTy, ArgOffset)); + UC.markNosanitize(Base); + } + Base = IRB.CreateIntToPtr(Base, PointerType::get(UC.getShadowTy(T), 0), + "_dfsarg"); + UC.markNosanitize(Base); + return Base; +} + +/// Computes the shadow address for a return value. +/// UCSan uses this to track symbolic labels on return values, +/// enabling pointer aliasing tracking across function boundaries. +Value *UCSanFunction::getRetvalTLS(Type *T, IRBuilder<> &IRB) { + Value *Ret = IRB.CreatePointerCast( + UC.RetvalTLS, PointerType::get(UC.getShadowTy(T), 0), "_dfsret"); + UC.markNosanitize(Ret); + return Ret; +} + +/// Get shadow value for a given LLVM value. +/// UCSan uses shadows to track pointer aliasing. +Value *UCSanFunction::getShadow(Value *V) { + if (!isa(V) && !isa(V)) + return UC.getZeroShadow(V); + Value *&Shadow = ValShadowMap[V]; + if (!Shadow) { + if (Argument *A = dyn_cast(V)) { + Shadow = getShadowForTLSArgument(A); + } else { + Shadow = UC.getZeroShadow(V); + } + } + return Shadow; +} + +/// Set shadow value for an instruction. +void UCSanFunction::setShadow(Instruction *I, Value *Shadow) { + assert(!ValShadowMap.count(I)); + ValShadowMap[I] = Shadow; +} + +/// Get shadow for a function argument from TLS. +Value *UCSanFunction::getShadowForTLSArgument(Argument *A) { + unsigned ArgOffset = 0; + const DataLayout &DL = F->getParent()->getDataLayout(); + for (auto &FArg : F->args()) { + if (!FArg.getType()->isSized()) { + if (A == &FArg) + return UC.getZeroShadow(A); + continue; + } + + unsigned Size = DL.getTypeAllocSize(UC.getShadowTy(&FArg)); + if (A != &FArg) { + ArgOffset += alignTo(Size, ShadowTLSAlignment); + if (ArgOffset > ArgTLSSize) + break; // ArgTLS overflows, uses a zero shadow. + continue; + } + + if (ArgOffset + Size > ArgTLSSize) + break; // ArgTLS overflows, uses a zero shadow. + + // Insert ArgTLS load at the start of the entry block. Use the + // (BB, iterator) overload so we handle an empty entry block (e.g. an + // auto-custom wrapper whose first checkPointer fires before any IR has + // been emitted) — getFirstInsertionPt() returns end() and IRBuilder then + // appends to the block. + BasicBlock &EntryBB = F->getEntryBlock(); + IRBuilder<> IRB(*UC.Ctx); + IRB.SetInsertPoint(&EntryBB, EntryBB.getFirstInsertionPt()); + Value *ArgShadowPtr = getArgTLS(FArg.getType(), ArgOffset, IRB); + LoadInst *LI = IRB.CreateAlignedLoad(UC.getShadowTy(&FArg), ArgShadowPtr, + ShadowTLSAlignment); + UC.markNosanitize(LI); + return LI; + } + + return UC.getZeroShadow(A); +} + +/// Load primitive shadow corresponding to bytes [Addr, Addr+Size), where +// Addr has alignment Align, and take the union of each of those shadows. +Value *UCSanFunction::loadPrimitiveShadow(Value *Addr, uint64_t Size, Align Align, + Type *Ty, IRBuilder<> &IRB) { + if (Size == 0) + return UC.ZeroPrimitiveShadow; + + Value *ShadowAddr = UC.getShadowAddress(Addr, IRB); + // TOOD: Optimize for non-pointer types + Value *ConcreteAddr = IRB.CreateBitCast(Addr, UC.VoidPtrTy); + UC.markNosanitize(ConcreteAddr); + CallInst *FallbackCall = IRB.CreateCall( + UC.UCLoadPointerShadowFn, + {ShadowAddr, ConstantInt::get(UC.Int64Ty, Size), + ConstantInt::get(UC.Int1Ty, Ty->isPointerTy()), ConcreteAddr}); + FallbackCall->addRetAttr(Attribute::ZExt); + UC.markNosanitize(FallbackCall); + return FallbackCall; +} + +/// Recursive helper to load shadow for aggregate types. +Value *UCSanFunction::loadShadowRecursive( + Value *Shadow, SmallVector &Indices, Type *SubTy, + Value *Addr, uint64_t Size, Align InstAlign, IRBuilder<> &IRB) { + auto &DL = F->getParent()->getDataLayout(); + + if (!isa(SubTy) && !isa(SubTy)) { + uint64_t SubSize = DL.getTypeStoreSize(SubTy); + assert(Size >= SubSize); + InstAlign = Align(std::min(InstAlign.value(), (uint64_t)DL.getABITypeAlignment(SubTy))); + // load a primitive shadow from address + Value *PrimitiveShadow = loadPrimitiveShadow(Addr, SubSize, InstAlign, SubTy, IRB); + // then insert the primitive shadow into the sub-field + Value *Insert = IRB.CreateInsertValue(Shadow, PrimitiveShadow, Indices); + UC.markNosanitize(Insert); + return Insert; + } + + if (ArrayType *AT = dyn_cast(SubTy)) { + for (unsigned Idx = 0; Idx < AT->getNumElements(); Idx++) { + Indices.push_back(Idx); + // double check the remaining size + Type *ElemTy = AT->getElementType(); + uint64_t ElemSize = DL.getTypeStoreSize(ElemTy); + uint64_t Offset = ElemSize * Idx; + assert(Offset <= Size); + // get the address of the array element + Value *SubAddr = IRB.CreateConstGEP2_32(AT, Addr, 0, Idx); + UC.markNosanitize(SubAddr); + Shadow = loadShadowRecursive(Shadow, Indices, ElemTy, + SubAddr, Size - Offset, InstAlign, IRB); + Indices.pop_back(); + } + return Shadow; + } + + if (StructType *ST = dyn_cast(SubTy)) { + const StructLayout *SL = DL.getStructLayout(ST); + for (unsigned Idx = 0; Idx < ST->getNumElements(); Idx++) { + Indices.push_back(Idx); + // double check the remaining size + uint64_t Offset = SL->getElementOffset(Idx); + assert(Offset <= Size); + Type *ElemTy = ST->getElementType(Idx); + // get the address of the struct field + Value *SubAddr = IRB.CreateConstGEP2_32(ST, Addr, 0, Idx); + UC.markNosanitize(SubAddr); + Shadow = loadShadowRecursive(Shadow, Indices, ElemTy, + SubAddr, Size - Offset, InstAlign, IRB); + Indices.pop_back(); + } + return Shadow; + } + llvm_unreachable("Unexpected shadow type"); +} + +/// Load shadow from shadow memory for a given address. +Value *UCSanFunction::loadShadow(Value *Addr, uint64_t Size, Align Align, + Type *Ty, Instruction *Pos) { + IRBuilder<> IRB(Pos); + // if loading from a local variable, load label from its shadow alloca + if (AllocaInst *AI = dyn_cast(Addr)) { + const auto i = AllocaShadowMap.find(AI); + if (i != AllocaShadowMap.end()) { + LoadInst *LI = IRB.CreateLoad(UC.getShadowTy(Ty), i->second); + UC.markNosanitize(LI); + return LI; + } + } + + SmallVector Objs; + getUnderlyingObjects(Addr, Objs); + bool AllConstants = true; + for (const Value *Obj : Objs) { + if (isa(Obj) || isa(Obj)) + continue; + if (isa(Obj) && cast(Obj)->isConstant()) + continue; + + AllConstants = false; + break; + } + if (AllConstants) + return UC.ZeroPrimitiveShadow; + + const llvm::Align ShadowAlign(Align.value() * UCSan::ShadowWidthBytes); + + // now check if we're loading an aggragate object + if (!isa(Ty) && !isa(Ty)) + return loadPrimitiveShadow(Addr, Size, ShadowAlign, Ty, IRB); + + // if loading an aggregate object, load its shadow recursively + SmallVector Indices; + Type *ShadowTy = UC.getShadowTy(Ty); + Value *Shadow = UndefValue::get(ShadowTy); + Shadow = loadShadowRecursive(Shadow, Indices, Ty, Addr, Size, ShadowAlign, IRB); + return Shadow; +} + +void UCSanFunction::storeShadowRecursive( + Value *Shadow, SmallVector &Indices, Type *SubTy, + Value *Addr, uint64_t Size, Align InstAlign, IRBuilder<> &IRB) { + auto &DL = F->getParent()->getDataLayout(); + + if (!isa(SubTy) && !isa(SubTy)) { + uint64_t SubSize = DL.getTypeStoreSize(SubTy); + assert(Size >= SubSize); + InstAlign = Align(std::min(InstAlign.value(), + (uint64_t)DL.getABITypeAlignment(SubTy))); + // load a primitive shadow from the sub-field + Value *PrimitiveShadow = IRB.CreateExtractValue(Shadow, Indices); + UC.markNosanitize(PrimitiveShadow); + // then store the primitive shadow into the shadow address + Value *ShadowAddr = UC.getShadowAddress(Addr, IRB); + CallInst *CI = IRB.CreateCall(UC.UCStorePointerShadowFn, + {PrimitiveShadow, ShadowAddr, ConstantInt::get(UC.Int64Ty, SubSize)}); + UC.markNosanitize(CI); + return; + } + + if (ArrayType *AT = dyn_cast(SubTy)) { + for (unsigned Idx = 0; Idx < AT->getNumElements(); Idx++) { + Indices.push_back(Idx); + // double check the remaining size + Type *ElemTy = AT->getElementType(); + uint64_t ElemSize = DL.getTypeStoreSize(ElemTy); + uint64_t Offset = ElemSize * Idx; + assert(Offset <= Size); + // get the address of the array element + Value *SubAddr = IRB.CreateConstGEP2_32(AT, Addr, 0, Idx); + UC.markNosanitize(SubAddr); + storeShadowRecursive(Shadow, Indices, ElemTy, + SubAddr, Size - Offset, InstAlign, IRB); + Indices.pop_back(); + } + return; + } + + if (StructType *ST = dyn_cast(SubTy)) { + const StructLayout *SL = DL.getStructLayout(ST); + for (unsigned Idx = 0; Idx < ST->getNumElements(); Idx++) { + Indices.push_back(Idx); + // double check the remaining size + uint64_t Offset = SL->getElementOffset(Idx); + assert(Offset <= Size); + Type *ElemTy = ST->getElementType(Idx); + // get the address of the struct field + Value *SubAddr = IRB.CreateConstGEP2_32(ST, Addr, 0, Idx); + UC.markNosanitize(SubAddr); + storeShadowRecursive(Shadow, Indices, ElemTy, + SubAddr, Size - Offset, InstAlign, IRB); + Indices.pop_back(); + } + return; + } + llvm_unreachable("Unexpected shadow type"); +} + +/// Store shadow to shadow memory for a given address. +void UCSanFunction::storeShadow(Value *Addr, uint64_t Size, Align Alignment, + Value *Shadow, Type *Ty, Instruction *Pos) { + IRBuilder<> IRB(Pos); + if (AllocaInst *AI = dyn_cast(Addr)) { + const auto i = AllocaShadowMap.find(AI); + if (i != AllocaShadowMap.end()) { + StoreInst *SI = IRB.CreateStore(Shadow, i->second); + UC.markNosanitize(SI); + SkipInsts.insert(SI); + return; + } + } + + const Align ShadowAlign(Alignment.value() * UCSan::ShadowWidthBytes); + Value *ShadowAddr = UC.getShadowAddress(Addr, IRB); + // check if the shadow is zero, if so, clear the shadow memory regardless + // of the shadow type + if (UC.isZeroShadow(Shadow)) { + IntegerType *ShadowTy = + IntegerType::get(*UC.Ctx, Size * UC.ShadowWidthBits); + Value *ExtZeroShadow = ConstantInt::get(ShadowTy, 0); + Value *ExtShadowAddr = + IRB.CreateBitCast(ShadowAddr, PointerType::getUnqual(ShadowTy)); + UC.markNosanitize(ExtShadowAddr); + Value *SI = IRB.CreateAlignedStore(ExtZeroShadow, ExtShadowAddr, ShadowAlign); + UC.markNosanitize(SI); + return; + } + + // now check if we're storing an aggragate shadow object + if (!isa(Ty) && !isa(Ty)) { + // TODO: Optimize for non-pointer types + CallInst *CI = IRB.CreateCall( + UC.UCStorePointerShadowFn, + {Shadow, ShadowAddr, ConstantInt::get(UC.Int64Ty, Size)}); + UC.markNosanitize(CI); + return; + } + + // if storing an aggregate shadow object, store its shadow recursively + // we want to do this so union_store may have a chance to simplify some + // constraints + SmallVector Indices; + storeShadowRecursive(Shadow, Indices, Ty, + Addr, Size, ShadowAlign, IRB); +} + +Function *UCSan::buildDangleFunction(Function *F) { + // Creates wrapper for out-of-scope functions + // This allows in-scope code to call out-of-scope functions safely + // The wrapper handles shadow management and pointer checking + + FunctionType *FT = F->getFunctionType(); + std::string FN = "__external$" + F->getName().str(); + Function *NewF = Function::Create(FT, GlobalValue::LinkageTypes::PrivateLinkage, + F->getAddressSpace(), FN, Mod); + + // libc/header byte-swap helpers are often emitted as out-of-scope static + // functions. Returning an arbitrary symbolic value here breaks protocol + // dispatch constraints like htons(ETHERTYPE_IP6). Keep these wrappers + // semantic and let the later TaintPass model llvm.bswap normally. + StringRef OrigName = F->getName(); + if (OrigName == "__bswap_16" || OrigName == "__bswap_32" || + OrigName == "__bswap_64") { + BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", NewF); + IRBuilder<> IRB(BB); + Argument *Arg = &*NewF->arg_begin(); + Type *ArgTy = Arg->getType(); + if (ArgTy->isIntegerTy() && FT->getReturnType() == ArgTy) { + Function *Bswap = Intrinsic::getDeclaration(Mod, Intrinsic::bswap, ArgTy); + Value *Ret = IRB.CreateCall(Bswap, {Arg}); + IRB.CreateRet(Ret); + return NewF; + } + IRB.CreateRet(Constant::getNullValue(FT->getReturnType())); + return NewF; + } + + // Some inputs are compiled with non-standard stack alignment; force realignment + // so calls into UCSan runtime vararg logging are always safe. + NewF->addFnAttr(Attribute::getWithStackAlignment(*Ctx, Align(16))); + NewF->addFnAttr("stackrealign"); + markFunctionNosanitize(NewF); // Mark dangle wrapper so TaintPass skips it + FunctionType *NewFT = NewF->getFunctionType(); + BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", NewF); + IRBuilder<> IRB(BB); + + // Create UCSanFunction context for shadow memory access + UCSanFunction UF(*this, NewF); + + const DataLayout &DL = Mod->getDataLayout(); + bool ignore_resign = getWrapperKind(F) == WK_Ignore; + bool resign_ptrargs = getenv("KO_RESIGN_PTRARGS") != nullptr && !ignore_resign; + bool checker_ubi = getenv("KO_CHECKER_UBI") != nullptr; + + // Get return address for tracking + auto RetAddr = IRB.CreateCall(LLVMReturnAddrFn, {ConstantInt::get(Int32Ty, 0)}, "retaddr"); + + // Process pointer arguments if needed + if (resign_ptrargs || checker_ubi) { + unsigned ArgOffset = 0; + unsigned n = FT->getNumParams(); + + for (auto arg = NewF->arg_begin(); n != 0; ++arg, --n) { + if (arg->getType()->isPointerTy()) { + // Get size of pointed-to type + ConstantInt *CI; + if (arg->getType()->getPointerElementType()->isSized()) { + CI = ConstantInt::get(Int64Ty, DL.getTypeSizeInBits(arg->getType()->getPointerElementType()) / 8); + } else { + CI = ConstantInt::get(Int64Ty, 0); + } + + // Get shadow address for this argument from TLS + // UCSan uses shadow memory to track pointer aliasing + Value *shadow = UF.getArgTLS(arg->getType(), ArgOffset, IRB); + + // Check UBI if enabled + if (checker_ubi) { + Value *loaded_shadow = IRB.CreateAlignedLoad(PrimitiveShadowTy, shadow, ShadowTLSAlignment); + IRB.CreateCall(UCCheckUBIFn, {loaded_shadow}); + } + + // Resign shadow if enabled + if (resign_ptrargs) { + Value *BCI = IRB.CreatePointerCast(&*arg, VoidPtrTy); + IRB.CreateCall(UCResignShadowFn, {BCI, shadow, CI, RetAddr}); + } + } + + // Update offset for next argument + unsigned Size = DL.getTypeAllocSize(getShadowTy(&*arg)); + ArgOffset += alignTo(Size, ShadowTLSAlignment); + } + } + + // Wrap return value + Type *RT = FT->getReturnType(); + if (!RT->isVoidTy()) { + Value *retvaltls = UF.getRetvalTLS(RT, IRB); + + // For struct/array return types, we need to call ucsan_wrap_retval for each + // primitive element so that both ucsan and symsan labels are properly wrapped. + // ucsan_wrap_retval returns a pointer to __ucsan_wrapped_return_tls where + // the actual return value is stored. We must load immediately after each call + // before the next call overwrites it, then assemble the struct. + std::function &)> wrapRetvalRecursive = + [&](Type *SubTy, Value *ShadowAddr, SmallVector &Indices) -> Value * { + if (!isa(SubTy) && !isa(SubTy)) { + // Primitive type: call ucsan_wrap_retval and load from returned pointer + ConstantInt *size = ConstantInt::get(Int64Ty, DL.getTypeSizeInBits(SubTy)); + ConstantInt *is_ptr = ConstantInt::get(Int1Ty, SubTy->isPointerTy()); + Value *ret_ptr = IRB.CreateCall(UCWrapRetvalFn, {size, ShadowAddr, is_ptr, RetAddr}); + // Load the value from the returned pointer (pointing to __ucsan_wrapped_return_tls) + Value *typed_ptr = IRB.CreateBitOrPointerCast(ret_ptr, SubTy->getPointerTo()); + Value *loaded_val = IRB.CreateLoad(SubTy, typed_ptr); + return loaded_val; + } + + // For aggregate types, recursively wrap each element and assemble + Value *Result = UndefValue::get(SubTy); + + if (ArrayType *AT = dyn_cast(SubTy)) { + Type *ElemTy = AT->getElementType(); + for (unsigned Idx = 0; Idx < AT->getNumElements(); Idx++) { + Value *SubShadowAddr = IRB.CreateConstGEP2_32( + getShadowTy(SubTy), ShadowAddr, 0, Idx); + markNosanitize(SubShadowAddr); + Indices.push_back(Idx); + Value *ElemVal = wrapRetvalRecursive(ElemTy, SubShadowAddr, Indices); + Indices.pop_back(); + Result = IRB.CreateInsertValue(Result, ElemVal, Idx); + } + return Result; + } + + if (StructType *ST = dyn_cast(SubTy)) { + for (unsigned Idx = 0; Idx < ST->getNumElements(); Idx++) { + Type *ElemTy = ST->getElementType(Idx); + Value *SubShadowAddr = IRB.CreateConstGEP2_32( + getShadowTy(SubTy), ShadowAddr, 0, Idx); + markNosanitize(SubShadowAddr); + Indices.push_back(Idx); + Value *ElemVal = wrapRetvalRecursive(ElemTy, SubShadowAddr, Indices); + Indices.pop_back(); + Result = IRB.CreateInsertValue(Result, ElemVal, Idx); + } + return Result; + } + + llvm_unreachable("Unexpected type in wrapRetvalRecursive"); + }; + + SmallVector Indices; + Value *RetVal = wrapRetvalRecursive(RT, retvaltls, Indices); + IRB.CreateRet(RetVal); + } else if (F->doesNotReturn()) { + NewF->addFnAttr(Attribute::NoReturn); + auto *ExitCall = IRB.CreateCall(ExitFn, {ConstantInt::get(Int32Ty, 0)}); + markNosanitize(ExitCall); + IRB.CreateUnreachable(); + } else { + IRB.CreateRetVoid(); + } + + return NewF; +} + +Function *UCSan::buildDiscardFunction(Function *F) { + // Creates a trivial stub for discarded functions (e.g., kernel helpers + // like _copy_to_user/printk that have no implementation in UC mode). + // The stub simply returns zero/void and is marked so TaintPass skips it. + + FunctionType *FT = F->getFunctionType(); + std::string FN = "__discard$" + F->getName().str(); + Function *NewF = Function::Create(FT, GlobalValue::LinkageTypes::PrivateLinkage, + F->getAddressSpace(), FN, Mod); + markFunctionNosanitize(NewF); + + BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", NewF); + IRBuilder<> IRB(BB); + + Type *RT = FT->getReturnType(); + if (RT->isVoidTy()) + IRB.CreateRetVoid(); + else + IRB.CreateRet(Constant::getNullValue(RT)); + + return NewF; +} + +Function *UCSan::buildDriverWrapperFunction(Function *F) { + // Creates main() wrapper that calls the entry point function + // This replaces the standard main() and initializes arguments symbolically + + // Check if we should skip wrapper creation + if (getenv("NOT_ENTRY_OBJECT")) { + return F; + } + + // Create new main() with standard signature: int main(int argc, char **argv) + Type *ArgvTy = PointerType::getUnqual(PointerType::getUnqual(Int8Ty)); + FunctionType *FT = FunctionType::get(Int32Ty, {Int32Ty, ArgvTy}, false); + Function *NewF = Function::Create(FT, GlobalValue::LinkageTypes::ExternalLinkage, + F->getAddressSpace(), "main", Mod); + NewF->getArg(0)->setName("argc"); + NewF->getArg(1)->setName("argv"); + + // Ensure 16-byte stack alignment for calls into sanitizer vararg code (Printf). + NewF->addFnAttr(Attribute::getWithStackAlignment(*Ctx, Align(16))); + NewF->addFnAttr("stackrealign"); + markFunctionNosanitize(NewF); // Mark driver wrapper so TaintPass skips it + + BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", NewF); + IRBuilder<> IRB(BB); + + // Handle the entry point function + if (F->isVarArg()) { + // VarArg functions not yet supported for entry points + errs() << "Warning: VarArg entry points not supported\n"; + assert(false && "Not implemented"); + IRB.CreateRet(ConstantInt::get(Int32Ty, 1)); + return NewF; + } + + FunctionType *EntryFT = F->getFunctionType(); + const DataLayout &DL = Mod->getDataLayout(); + + // Record entry function info for type table + EntryFunctionName = F->getName().str(); + EntryArgs.clear(); + + // Initialize arguments for the entry point + std::vector Args; + for (Function::arg_iterator ai = F->arg_begin(), ae = F->arg_end(); ai != ae; ++ai) { + ConstantInt *ArgNo = ConstantInt::get(Int32Ty, ai->getArgNo()); + ConstantInt *ArgSize = ConstantInt::get(Int32Ty, DL.getTypeSizeInBits(ai->getType())); + ConstantInt *IsPtr = ConstantInt::get(Int8Ty, ai->getType()->isPointerTy()); + ConstantInt *InitVal = ConstantInt::get(Int64Ty, 0); + + // Record arg type info + uint32_t TypeID = getOrCreateTypeID(ai->getType()); + EntryArgs.push_back({ai->getName().str(), TypeID}); + + // Call runtime to create symbolic argument + // Returns a pointer that we cast to the appropriate type + Value *ret_val = IRB.CreateCall(UCSetLabelForArgsFn, {ArgNo, ArgSize, IsPtr, InitVal}); + if (ai->getType()->isFloatingPointTy()) { + // Can't bitcast pointer directly to float; go through integer + Type *IntTy = IntegerType::get(*Ctx, DL.getTypeSizeInBits(ai->getType())); + Value *int_val = IRB.CreatePtrToInt(ret_val, IntTy); + Args.push_back(IRB.CreateBitCast(int_val, ai->getType())); + } else { + Args.push_back(IRB.CreateBitOrPointerCast(ret_val, ai->getType())); + } + } + + // Call the actual entry point function + IRB.CreateCall(F, Args); + + // Return 0 (success) + IRB.CreateRet(ConstantInt::get(Int32Ty, 0)); + + // Add basic block tracing to entry point if enabled + if (ClTraceBB) { + unsigned int BBCount = BBID_STEP; + for (Function::iterator BI = F->begin(), BE = F->end(); BI != BE; ++BI, ++BBCount) { + ConstantInt *BBID = ConstantInt::get(Int32Ty, BBCount); + + // Add trace call at beginning of basic block. + // Use getFirstInsertionPt() (not getFirstNonPHI): in C++ EH landing pads + // the landingpad must remain the first non-PHI instruction, and + // getFirstInsertionPt() advances past EH pads so we don't break the block. + CallInst *CI = CallInst::Create(UCTraceBBFn, + {ConstantInt::get(Int32Ty, 0), BBID}, + "", &*BI->getFirstInsertionPt()); + + // Add metadata for BB tracking + MDNode *MD = MDNode::get(Mod->getContext(), + {ConstantAsMetadata::get(BBID)}); + BI->getTerminator()->setMetadata(BBIDName, MD); + } + } + + return NewF; +} + +Value *UCSan::invertIntRet(Value *RetVal, Type *WrapperRetTy, bool Invert, + IRBuilder<> &IRB) { + if (!Invert || !RetVal->getType()->isIntegerTy() || + !WrapperRetTy->isIntegerTy()) { + return IRB.CreateBitOrPointerCast(RetVal, WrapperRetTy); + } + Value *IsZero = + IRB.CreateICmpEQ(RetVal, ConstantInt::get(RetVal->getType(), 0)); + return IRB.CreateZExtOrTrunc(IsZero, WrapperRetTy); +} + +Function *UCSan::getCustomFunction(const Function *F) { + // Handles auto-custom function wrapping based on YAML configuration + // Maps arguments from one function signature to another + + auto Name = F->getName().str(); + + // Check if already built + if (CustomFuncs.find(Name) != CustomFuncs.end()) { + return CustomFuncs[Name]; + } + + // Check if this is a custom function in metadata + if (Scope.custom.find(Name) == Scope.custom.end()) { + return nullptr; // Not a custom function + } + + auto &CustomEntry = Scope.custom[Name]; + + // Verify referenced function type exists + if (CustomFuncTypes.find(CustomEntry.ref_name) == CustomFuncTypes.end()) { + errs() << "Error: Referenced function " << CustomEntry.ref_name << " not found\n"; + return nullptr; + } + + // Check if ref function is a taint type (handled by TaintPass). + // For taint refs, generate a simple forwarding wrapper that calls the ref + // function directly with remapped args. TaintPass will instrument the + // wrapper body and convert the inner call to __dfsw_. + bool IsTaintRef = ABIList.isIn(CustomEntry.ref_name, "taint"); + + // Create wrapper function + auto Linkage = Function::InternalLinkage; + auto WrappedName = "__auto_dfsw_" + Name; + auto RefedName = IsTaintRef ? CustomEntry.ref_name + : ("__dfsw_" + CustomEntry.ref_name); + + FunctionType *RefedFuncType = CustomFuncTypes[CustomEntry.ref_name]; + FunctionType *WrapperType = FunctionType::get(F->getReturnType(), + F->getFunctionType()->params(), + F->isVarArg()); + + Function *WrapperFunc = Function::Create(WrapperType, Linkage, WrappedName, *Mod); + // Keep wrapper/runtime call ABI robust even when module stack alignment is 8. + WrapperFunc->addFnAttr(Attribute::getWithStackAlignment(*Ctx, Align(16))); + WrapperFunc->addFnAttr("stackrealign"); + // For ucsan custom refs, mark wrapper nosanitize so TaintPass skips its body. + // For taint refs, leave the wrapper instrumentable so TaintPass wraps the + // inner ref call with its proper taint-label handling. + if (!IsTaintRef) + markFunctionNosanitize(WrapperFunc); + BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", WrapperFunc); + IRBuilder<> IRB(BB); + + // Create UCSanFunction context for shadow memory access + UCSanFunction UF(*this, WrapperFunc); + + // Build argument mapping + std::map ArgMap; + std::map ArgTLSMap; + + for (auto &ArgMapEntry : CustomEntry.arg_maps) { + ArgMap[ArgMapEntry.ucsan_idx] = ArgMapEntry.idx; + } + + const DataLayout &DL = Mod->getDataLayout(); + + if (IsTaintRef) { + // Simple forwarding wrapper: call the ref function directly with remapped + // args. TaintPass will instrument this body and wrap the call with the + // proper __dfsw_ taint-label handling. + FunctionCallee RefedFunc = Mod->getOrInsertFunction(RefedName, RefedFuncType); + + std::vector Args; + for (unsigned i = 0, e = RefedFuncType->getNumParams(); i != e; ++i) { + if (ArgMap.find(i) == ArgMap.end()) { + errs() << "Error: Argument " << i << " not in arg_maps\n"; + return nullptr; + } + unsigned idx = ArgMap[i]; + Value *Arg = WrapperFunc->getArg(idx); + // Cast wrapper arg to ref function's expected param type if they differ + // (e.g. uint8_t* -> void*). + Type *RefParamTy = RefedFuncType->getParamType(i); + if (Arg->getType() != RefParamTy) + Arg = IRB.CreateBitOrPointerCast(Arg, RefParamTy); + Args.push_back(Arg); + } + + // Lazy-init / enlarge the symbolic object behind each pointer arg with + // the right byte count. The size is taken from the abilist sizeN entry + // on the ref function (e.g. memcmp=size2 → 3rd arg holds the count); + // for ref functions with no entry (e.g. strcmp) pass 0 (strlen-determined). + int SizeArgIdx = ABIList.getSizeArgIdx(CustomEntry.ref_name); + Value *SizeVal; + if (SizeArgIdx >= 0 && (unsigned)SizeArgIdx < Args.size()) { + SizeVal = IRB.CreateZExtOrTrunc(Args[SizeArgIdx], Int64Ty); + markNosanitize(SizeVal); + } else { + SizeVal = ConstantInt::get(Int64Ty, 0); + } + for (unsigned i = 0, e = Args.size(); i != e; ++i) { + if (!Args[i]->getType()->isPointerTy()) continue; + // typeID 0 = typeless bytes (matches WK_TaintCustom convention) + Args[i] = UF.checkPointer(Args[i], SizeVal, true, IRB, 0); + } + + if (!F->getReturnType()->isVoidTy()) { + Value *RetVal = IRB.CreateCall(RefedFunc, Args); + Value *FinalRet = invertIntRet(RetVal, F->getReturnType(), + CustomEntry.invert_ret, IRB); + IRB.CreateRet(FinalRet); + } else { + IRB.CreateCall(RefedFunc, Args); + IRB.CreateRetVoid(); + } + + CustomFuncs[Name] = WrapperFunc; + return WrapperFunc; + } + + // ucsan custom ref: use the existing __dfsw_ wrapper that takes + // extra ucsan_label shadow arguments via __ucsan_arg_tls / __ucsan_retval_tls. + + // Get or insert the referenced function with transformed type (includes shadow args) + TransformedFunction TransformedFn = getCustomFunctionType(RefedFuncType); + FunctionCallee RefedFunc = Mod->getOrInsertFunction(RefedName, TransformedFn.TransformedType); + + unsigned ArgOffset = 0; + + // Calculate shadow offsets for each argument + for (unsigned i = 0, e = F->arg_size(); i != e; ++i) { + ArgTLSMap[i] = ArgOffset; + unsigned Size = DL.getTypeAllocSize(getShadowTy(F->getArg(i)->getType())); + ArgOffset += alignTo(Size, ShadowTLSAlignment); + } + + // Build actual arguments for referenced function + std::vector Args; + + // Add regular arguments + for (unsigned i = 0, e = RefedFuncType->getNumParams(); i != e; ++i) { + if (ArgMap.find(i) == ArgMap.end()) { + errs() << "Error: Argument " << i << " not in arg_maps\n"; + return nullptr; + } + unsigned idx = ArgMap[i]; + Args.push_back(WrapperFunc->getArg(idx)); + } + + // Add shadow arguments from TLS (UCSan uses shadow memory for pointer aliasing) + for (unsigned i = 0, e = RefedFuncType->getNumParams(); i != e; ++i) { + unsigned idx = ArgMap[i]; + Value *argtls = UF.getArgTLS(F->getArg(idx)->getType(), ArgTLSMap[idx], IRB); + Value *ArgShadow = IRB.CreateLoad(PrimitiveShadowTy, argtls); + Args.push_back(ArgShadow); + } + + // Handle return value + if (!F->getReturnType()->isVoidTy()) { + Value *retvaltls = UF.getRetvalTLS(F->getReturnType(), IRB); + Args.push_back(retvaltls); + + Value *RetVal = IRB.CreateCall(RefedFunc, Args); + Value *FinalRet = invertIntRet(RetVal, F->getReturnType(), + CustomEntry.invert_ret, IRB); + IRB.CreateRet(FinalRet); + } else { + IRB.CreateCall(RefedFunc, Args); + IRB.CreateRetVoid(); + } + + CustomFuncs[Name] = WrapperFunc; + return WrapperFunc; +} + +UCSan::WrapperKind UCSan::getWrapperKind(Function *F) { + // Check ABIList for taint custom functions (handled by TaintPass if available) + if (ABIList.isIn(*F, "taint")) + return WK_TaintCustom; + + // Check ABIList for custom functions (priority) + if (ABIList.isIn(*F, "custom")) + return WK_Custom; + + // Check ABIList for discard functions (completely ignored, no wrapper) + if (ABIList.isIn(*F, "discard")) + return WK_Discard; + + // Check ABIList for ignored external functions. These still get an external + // wrapper, but the wrapper must not resign pointer arguments. + if (ABIList.isIn(*F, "ignore")) + return WK_Ignore; + + // Check ABIList for uninstrumented functions + if (ABIList.isIn(*F, "uninstrumented")) + return WK_Uninstrumented; + + // Check if function is in the shims map from YAML metadata + auto shimIt = Scope.shims.find(F->getName().str()); + if (shimIt != Scope.shims.end()) { + return shimIt->second ? WK_ShimOrig : WK_ShimTarget; + } + + // Check if function is in the custom map from YAML metadata + if (Scope.custom.find(F->getName().str()) != Scope.custom.end()) { + return WK_AutoCustom; + } + return WK_None; +} + +TransformedFunction UCSan::getCustomFunctionType(FunctionType *T) { + SmallVector ArgTypes; + + // Some parameters of the custom function being constructed are + // parameters of T. Record the mapping from parameters of T to + // parameters of the custom function, so that parameter attributes + // at call sites can be updated. + std::vector ArgumentIndexMapping; + for (unsigned i = 0, ie = T->getNumParams(); i != ie; ++i) { + Type* param_type = T->getParamType(i); + ArgumentIndexMapping.push_back(ArgTypes.size()); + ArgTypes.push_back(param_type); + } + for (unsigned i = 0, e = T->getNumParams(); i != e; ++i) + ArgTypes.push_back(getShadowTy(T->getParamType(i))); + if (T->isVarArg()) + ArgTypes.push_back(PrimitiveShadowPtrTy); + Type *RetType = T->getReturnType(); + if (!RetType->isVoidTy()) + ArgTypes.push_back(PointerType::getUnqual(getShadowTy(RetType))); + return TransformedFunction( + T, FunctionType::get(T->getReturnType(), ArgTypes, T->isVarArg()), + ArgumentIndexMapping); +} + +Value *UCSanFunction::checkPointer(Value *Ptr, Value *Size, bool dereference, + IRBuilder<> &IRB, uint32_t TypeID) { + Type *Ty = Ptr->getType(); + Value *SPtr = Ptr->stripPointerCasts(); + bool cacheable = isa(Size); + assert(dereference && "Must dereference pointer when calling this function"); + + if (cacheable) { + // cache is only possible if size is a constant + auto itr = CheckedPtrMap.find(Ptr); + if (itr != CheckedPtrMap.end()) { + // finding related instructions created by checkPointer + // worst case, cast(ptr) -> check_ptr -> cast(result) + Instruction *I1 = itr->second, *I2 = nullptr, *I3 = nullptr; + CallBase *CB = nullptr; + if (isa(I1)) { + // I1 = bitcast(result), I2 = call, I3 = bitcast(Ptr) if exists + I2 = dyn_cast(I1->getOperand(0)); + CB = dyn_cast(I2); + auto *AddrOp = dyn_cast(CB->getArgOperand(0)); + // Only track the input cast if it was created by us, not Ptr itself + if (AddrOp && isa(AddrOp) && AddrOp != Ptr) + I3 = AddrOp; + } else { + CB = dyn_cast(I1); + // No output cast; check if there's an input cast we created + auto *AddrOp = dyn_cast(CB->getArgOperand(0)); + if (AddrOp && isa(AddrOp) && AddrOp != Ptr) + I2 = AddrOp; + } + + // update size if needed + assert(CB && "Checked pointer must be a CallBase"); + auto OldSize = dyn_cast(CB->getArgOperand(2)); + auto NewSize = dyn_cast(Size); + assert(OldSize && NewSize && "Size must be a constant"); + if (NewSize->getZExtValue() > OldSize->getZExtValue()) { + // update size + CB->setArgOperand(2, Size); + } + + // cached check pointer must dominate the new use + auto *TBB = IRB.GetInsertBlock(); + auto *SBB = itr->second->getParent(); + if (DT.dominates(SBB, TBB)) { + // The check is already in a dominating block, no need to move it. + return itr->second; + } + + // not dominating, move the checks to a dominating basic block + Instruction *Pos = nullptr; + if (isa(Ptr) || isa(Ptr) || isa(Ptr)) { + Pos = F->getEntryBlock().getTerminator(); + } else if (Instruction *I = dyn_cast(Ptr)) { + // Find a block that dominates both the cached check and the new use + auto *NCD = DT.findNearestCommonDominator(SBB, TBB); + if (I->getParent() == NCD) { + // Ptr is in the common dominator, place after Ptr + Pos = I->getNextNode(); + while (isa(Pos) || isa(Pos)) { + Pos = Pos->getNextNode(); + } + // handle load shadow - skip the shadow if it exists + if (CB->getArgOperand(1) == Pos) { + Pos = Pos->getNextNode(); + } + } else { + // Ptr's block dominates NCD, place at start of NCD + Pos = &*NCD->getFirstInsertionPt(); + } + } else { + errs() << "Unexpected pointer type for checkPointer: " << *Ptr << "\n" + << "\t" << *SPtr << "\n"; + } + if (Pos) { + I1->moveBefore(Pos); + if (I2) I2->moveBefore(I1); + if (I3) I3->moveBefore(I2); + return itr->second; + } else { + cacheable = false; // do not cache if we need to move the instruction + // fall through to insert the check pointer + } + } + } + + if (GetElementPtrInst *GEP = dyn_cast(SPtr)) { + // If this is a GEP, we need to check the base pointer. + SPtr = GEP->getPointerOperand(); + } + + GlobalVariable *GV = + dyn_cast(getUnderlyingObject(Ptr->stripPointerCasts())); + Value *GlobalBoundsShadow = nullptr; + if (GV) { + // underlying obj is a global variable, do two things: + // 1) if size is a constant and smaller than the global obj size, and + // 2) we're not dereferencing the pointe, then we can skip the check + auto &DL = F->getParent()->getDataLayout(); + Type *GTy = GV->getValueType(); + if (GV->hasInitializer()) { + GTy = GV->getInitializer()->getType(); + } + uint64_t TypeSize = DL.getTypeStoreSize(GTy); + bool HasFixedBounds = + GTy->isSized() && (GTy->isArrayTy() || GTy->isStructTy()); + + // trace globals with fixed bounds to allow bounds checks + if (HasFixedBounds) { + Value *GlobalAddr = IRB.CreatePtrToInt(GV, UC.Int64Ty); + UC.markNosanitize(GlobalAddr); + Value *GlobalSize = ConstantInt::get(UC.Int64Ty, TypeSize); + GlobalBoundsShadow = + IRB.CreateCall(UC.UCTraceGlobalFn, {GlobalAddr, GlobalSize}); + UC.markNosanitize(GlobalBoundsShadow); + } + + if (ConstantInt *CI = dyn_cast(Size)) { + if (CI->getZExtValue() < TypeSize && !dereference) { + return Ptr; // size is smaller than the pointed type, no check needed + } + } + } + + if (!isa(SPtr)) { + Value *Addr = Ptr; + if (Ty != UC.VoidPtrTy) { + Addr = IRB.CreateBitCast(Ptr, UC.VoidPtrTy); + UC.markNosanitize(Addr); + } + + // Get shadow for the pointer from TLS (for arguments) or shadow memory + Value* Shadow = GlobalBoundsShadow ? GlobalBoundsShadow : getShadow(Ptr); + + Value *ExtSize = IRB.CreateZExtOrTrunc(Size, UC.Int64Ty); + if (ExtSize != Size) { + UC.markNosanitize(ExtSize); + } + ConstantInt *Deref = ConstantInt::get(UC.Int1Ty, dereference); + ConstantInt *TypeIDVal = ConstantInt::get(UC.Int32Ty, TypeID); + + CallInst *NewAddr = IRB.CreateCall( + UC.UCCheckPointerFn, {Addr, Shadow, ExtSize, Deref, TypeIDVal}); + // UC.markNosanitize(NewAddr); // don't mark as nonsanitize + + Value *Checked = NewAddr; + if (Ty != NewAddr->getType()) { + Checked = IRB.CreateBitCast(NewAddr, Ty); + UC.markNosanitize(Checked); + } + + if (cacheable) { + // only cache if size is a constant + CheckedPtrMap[Ptr] = cast(Checked); + } + + return Checked; + } else { + // AllocaInst doesn't need checking + return Ptr; + } +} + +static Value *stripPointerCastsForSummary(Value *V) { + while (true) { + if (auto *BC = dyn_cast(V)) { + V = BC->getOperand(0); + continue; + } + if (auto *ASC = dyn_cast(V)) { + V = ASC->getOperand(0); + continue; + } + if (auto *CI = dyn_cast(V)) { + V = CI->getOperand(0); + continue; + } + return V; + } +} + +static bool getArgumentBaseAndConstantOffset(Value *Ptr, const DataLayout &DL, + Argument *&Arg, + int64_t &Offset) { + Ptr = stripPointerCastsForSummary(Ptr); + APInt APOffset(DL.getIndexSizeInBits(Ptr->getType()->getPointerAddressSpace()), 0); + + if (auto *GEP = dyn_cast(Ptr)) { + if (!GEP->accumulateConstantOffset(DL, APOffset)) + return false; + Ptr = stripPointerCastsForSummary(GEP->getPointerOperand()); + } + + Arg = dyn_cast(Ptr); + if (!Arg) + return false; + + Offset = APOffset.getSExtValue(); + return true; +} + +static Value *getLoadedPointerBase(Value *Ptr, const DataLayout &DL, + int64_t &AccessOffset) { + Ptr = stripPointerCastsForSummary(Ptr); + AccessOffset = 0; + + if (auto *GEP = dyn_cast(Ptr)) { + APInt APOffset(DL.getIndexSizeInBits(Ptr->getType()->getPointerAddressSpace()), 0); + if (!GEP->accumulateConstantOffset(DL, APOffset)) + return nullptr; + AccessOffset = APOffset.getSExtValue(); + Ptr = stripPointerCastsForSummary(GEP->getPointerOperand()); + } + + return Ptr; +} + +void UCSanFunction::recordPointerLoadSummary(LoadInst &LI) { + if (!LI.getType()->isPointerTy()) + return; + + Argument *Arg = nullptr; + int64_t FieldOffset = 0; + if (!getArgumentBaseAndConstantOffset(LI.getPointerOperand(), + F->getParent()->getDataLayout(), Arg, + FieldOffset)) + return; + + LoadedPointerSummaries[&LI] = {Arg->getArgNo(), FieldOffset}; +} + +void UCSanFunction::recordMemoryAccessSummary(Value *Ptr, uint64_t AccessSize, + bool IsWrite, uint32_t TypeID, + Instruction *I) { + int64_t AccessOffset = 0; + Value *Base = getLoadedPointerBase(Ptr, F->getParent()->getDataLayout(), + AccessOffset); + if (!Base) + return; + + auto It = LoadedPointerSummaries.find(Base); + if (It == LoadedPointerSummaries.end()) + return; + + unsigned Line = 0; + unsigned Col = 0; + if (const DebugLoc &DL = I->getDebugLoc()) { + Line = DL.getLine(); + Col = DL.getCol(); + } + + MemoryAccessSummary Summary{ + It->second.ArgNo, + It->second.FieldOffset + AccessOffset, + AccessSize, + IsWrite, + TypeID, + Line, + Col, + }; + + for (const MemoryAccessSummary &Existing : MemoryAccessSummaries) { + if (Existing.ArgNo == Summary.ArgNo && + Existing.FieldOffset == Summary.FieldOffset && + Existing.AccessSize == Summary.AccessSize && + Existing.IsWrite == Summary.IsWrite && + Existing.TypeID == Summary.TypeID && + Existing.Line == Summary.Line && + Existing.Col == Summary.Col) + return; + } + + MemoryAccessSummaries.push_back(Summary); +} + +void UCSanFunction::emitMemoryAccessSummaries() { + if (MemoryAccessSummaries.empty()) + return; + + LLVMContext &C = F->getContext(); + SmallVector Summaries; + if (MDNode *Existing = symsan::ucsan::getMemoryAccessSummaries(*F)) { + for (const MDOperand &Op : Existing->operands()) + Summaries.push_back(Op.get()); + } + + for (const MemoryAccessSummary &Summary : MemoryAccessSummaries) { + Summaries.push_back(symsan::ucsan::createMemoryAccessSummaryMD(C, Summary)); + } + + symsan::ucsan::setMemoryAccessSummaries(*F, MDNode::get(C, Summaries)); +} + +void UCSanVisitor::visitLoadInst(LoadInst &LI) { + auto &DL = LI.getModule()->getDataLayout(); + Type *Ty = LI.getType(); + uint64_t StoreSize = DL.getTypeStoreSize(Ty); + if (StoreSize == 0) return; + + Value *Ptr = LI.getPointerOperand(); + UF.recordPointerLoadSummary(LI); + UF.recordMemoryAccessSummary(Ptr, StoreSize, false, + UF.UC.getOrCreateTypeID(Ty), &LI); + Value *Base = Ptr->stripPointerCasts(); + + // Check if loading from a constant global variable + if (GlobalVariable *GV = dyn_cast(Base)) { + if (GV->isConstant()) { + UF.setShadow(&LI, UF.UC.getZeroShadow(Ty)); + return; + } + } + + // For pointer loads, check if derived from a non-null initialized global + if (Ty->isPointerTy()) { + Constant *Init = nullptr; + if (GlobalVariable *GV = dyn_cast(Base)) { + // Direct load from GV + if (GV->hasInitializer()) { + Init = GV->getInitializer(); + } + } else if (auto *GEP = dyn_cast(Base)) { + // Load from GEP of GV - try to extract the initialized value + GlobalVariable *GV = dyn_cast( + GEP->getPointerOperand()->stripPointerCasts()); + if (GV && GV->hasInitializer() && GEP->hasAllConstantIndices()) { + SmallVector Indices; + for (auto It = GEP->idx_begin(); It != GEP->idx_end(); ++It) { + if (auto *CI = dyn_cast(*It)) { + Indices.push_back(CI->getZExtValue()); + } + } + // Skip the first index (pointer offset), extract from aggregate + if (Indices.size() > 1) { + Constant *Agg = GV->getInitializer(); + for (unsigned I = 1; I < Indices.size() && Agg; ++I) { + if (auto *AT = dyn_cast(Agg->getType())) { + Agg = Agg->getAggregateElement(Indices[I]); + } else if (auto *ST = dyn_cast(Agg->getType())) { + Agg = Agg->getAggregateElement(Indices[I]); + } else { + Agg = nullptr; + } + } + Init = Agg; + } + } + } + // If the initialized value is a non-null pointer, don't symbolize + if (Init && !Init->isNullValue()) { + UF.setShadow(&LI, UF.UC.getZeroShadow(Ty)); + return; + } + } + ConstantInt *Size = ConstantInt::get(UF.UC.Int64Ty, StoreSize); + uint32_t TypeID = UF.UC.getOrCreateTypeID(Ty); + + // Check and replace pointer + IRBuilder<> IRB(&LI); + Ptr = UF.checkPointer(Ptr, Size, true, IRB, TypeID); + LI.setOperand(0, Ptr); + + Value *Shadow = UF.loadShadow(Ptr, StoreSize, LI.getAlign(), Ty, &LI); + if (!UF.UC.isZeroShadow(Shadow)) + UF.NonZeroChecks.push_back(Shadow); + + UF.setShadow(&LI, Shadow); + + // Mark as checked for TaintPass + LI.setMetadata("ucsan.checked", + MDNode::get(*UF.UC.Ctx, None)); +} + +void UCSanVisitor::visitStoreInst(StoreInst &SI) { + if (SI.getMetadata("nosanitize")) return; + + auto &DL = SI.getModule()->getDataLayout(); + uint64_t StoreSize = DL.getTypeStoreSize(SI.getValueOperand()->getType()); + if (StoreSize == 0) return; + + Value *Ptr = SI.getPointerOperand(); + ConstantInt *Size = ConstantInt::get(UF.UC.Int64Ty, StoreSize); + + Value *Val = SI.getValueOperand(); + Type *Ty = Val->getType(); + uint32_t TypeID = UF.UC.getOrCreateTypeID(Ty); + + // Check and replace pointer + IRBuilder<> IRB(&SI); + Ptr = UF.checkPointer(Ptr, Size, true, IRB, TypeID); + SI.setOperand(SI.getPointerOperandIndex(), Ptr); + Value *Shadow = UF.getShadow(Val); + UF.storeShadow(Ptr, StoreSize, SI.getAlign(), Shadow, Ty, &SI); + + // Mark as checked + SI.setMetadata("ucsan.checked", + MDNode::get(*UF.UC.Ctx, None)); +} + +void UCSanVisitor::visitMemCpyInst(MemCpyInst &I) { + IRBuilder<> IRB(&I); + Value *Length = IRB.CreateZExtOrTrunc(I.getLength(), UF.UC.Int64Ty); + if (Length != I.getLength()) { + UF.UC.markNosanitize(Length); + } + + // Check source and destination pointers + Value *src = UF.checkPointer(I.getRawSource(), Length, true, IRB); + // check the destination pointer, if it's a global variable, + // no need to call check pointer to symbolize, as it's the dest. + Value *dest = I.getRawDest(); + if (!isa(getUnderlyingObject(dest->stripPointerCasts()))) { + dest = UF.checkPointer(dest, Length, true, IRB); + } + + I.setSource(src); + I.setDest(dest); + + // create a memcpy call to copy the shadow + Value *ShadowLength = IRB.CreateMul( + Length, + ConstantInt::get(UF.UC.Int64Ty, UCSan::ShadowWidthBytes)); + UF.UC.markNosanitize(ShadowLength); + auto MemCpyInst = llvm::Intrinsic::getDeclaration(I.getModule(), + llvm::Intrinsic::memcpy, + {UF.UC.PrimitiveShadowPtrTy, UF.UC.PrimitiveShadowPtrTy, UF.UC.Int64Ty}); + Value *CI = IRB.CreateCall(MemCpyInst, + {UF.UC.getShadowAddress(dest, IRB), + UF.UC.getShadowAddress(src, IRB), + ShadowLength, + I.getVolatileCst()}); + UF.UC.markNosanitize(CI); + + I.setMetadata("ucsan.checked", MDNode::get(*UF.UC.Ctx, None)); +} + +void UCSanVisitor::visitMemSetInst(MemSetInst &I) { + IRBuilder<> IRB(&I); + Value *Length = IRB.CreateZExtOrTrunc(I.getLength(), UF.UC.Int64Ty); + if (Length != I.getLength()) { + UF.UC.markNosanitize(Length); + } + + // check the destination pointer, if it's a global variable, + // no need to call check pointer. stack still needs to be checked, + // not for symbolizing, but for bounds check. + Value *dest = I.getRawDest(); + if (!isa(getUnderlyingObject(dest->stripPointerCasts()))) { + // Check destination pointer + dest = UF.checkPointer(dest, Length, true, IRB); + I.setDest(dest); + } + + // update shadow + Value *ValShadow = UF.getShadow(I.getValue()); + Value *CI = IRB.CreateCall(UF.UC.UCSetLabelFn, {ValShadow, dest, Length}); + UF.UC.markNosanitize(CI); + + I.setMetadata("ucsan.checked", MDNode::get(*UF.UC.Ctx, None)); +} + +void UCSanVisitor::visitMemMoveInst(MemMoveInst &I) { + IRBuilder<> IRB(&I); + Value *Length = IRB.CreateZExtOrTrunc(I.getLength(), UF.UC.Int64Ty); + if (Length != I.getLength()) { + UF.UC.markNosanitize(Length); + } + + // Check source and destination pointers + Value *src = UF.checkPointer(I.getRawSource(), Length, true, IRB); + // check the destination pointer, if it's a global variable, + // no need to call check pointer to symbolize, as it's the dest. + Value *dest = I.getRawDest(); + if (!isa(getUnderlyingObject(dest->stripPointerCasts()))) { + dest = UF.checkPointer(dest, Length, true, IRB); + } + + I.setDest(dest); + I.setSource(src); + + // memmove shadow + Value *DestShadowAddr = UF.UC.getShadowAddress(dest, IRB); + Value *SrcShadowAddr = UF.UC.getShadowAddress(src, IRB); + Value *ShadowLength = IRB.CreateMul( + Length, + ConstantInt::get(UF.UC.Int64Ty, UCSan::ShadowWidthBytes)); + UF.UC.markNosanitize(ShadowLength); + auto MemMoveDecl = llvm::Intrinsic::getDeclaration(I.getModule(), + llvm::Intrinsic::memmove, + {UF.UC.PrimitiveShadowPtrTy, UF.UC.PrimitiveShadowPtrTy, UF.UC.Int64Ty}); + auto *MTI = cast( + IRB.CreateCall(MemMoveDecl, + {DestShadowAddr, SrcShadowAddr, ShadowLength, I.getVolatileCst()})); + MTI->setDestAlignment(Align(UF.UC.ShadowWidthBytes)); + MTI->setSourceAlignment(Align(UF.UC.ShadowWidthBytes)); + UF.UC.markNosanitize(MTI); + + I.setMetadata("ucsan.checked", MDNode::get(*UF.UC.Ctx, None)); +} + +void UCSanVisitor::visitGetElementPtrInst(GetElementPtrInst &GEPI) { + // use the type of the pointer operand to estimate object size + Value *Ptr = GEPI.getPointerOperand(); + if (UF.CheckedPtrSet.count(Ptr) == 0 && + !isa(Ptr->stripPointerCasts())) { + IRBuilder<> IRB(&GEPI); + const DataLayout &DL = getDataLayout(); + Type *SrcElemTy = GEPI.getSourceElementType(); + Value *Size = ConstantInt::get(UF.UC.Int64Ty, + DL.getTypeAllocSize(SrcElemTy)); + uint32_t TypeID = UF.UC.getOrCreateTypeID(SrcElemTy); + Value *Addr = IRB.CreateBitCast(Ptr, UF.UC.VoidPtrTy); + if (Addr != Ptr) UF.UC.markNosanitize(Addr); + Value *Checked = IRB.CreateCall(UF.UC.UCCheckPointerFn, + { Addr, UF.getShadow(Ptr), Size, ConstantInt::get(UF.UC.Int1Ty, false), + ConstantInt::get(UF.UC.Int32Ty, TypeID) }); + UF.UC.markNosanitize(Checked); + UF.CheckedPtrSet.insert(Ptr); + } + // Propagate shadow through GEP + Value *Shadow = UF.getShadow(Ptr); + UF.setShadow(&GEPI, Shadow); +} + +void UCSanVisitor::visitInlineAsm(InlineAsm *IA, CallBase &CB) { + IRBuilder<> IRB(&CB); + auto DL = CB.getModule()->getDataLayout(); + + // Use ParseConstraints to identify memory operands and check their pointers + auto Constraints = IA->ParseConstraints(); + unsigned ArgIdx = 0; + for (auto &CI : Constraints) { + // clobbers don't have corresponding arguments + if (CI.Type == InlineAsm::isClobber) + continue; + // non-indirect outputs don't have a pointer argument + if (CI.Type == InlineAsm::isOutput && !CI.isIndirect) { + continue; + } + // bounds check + if (ArgIdx >= CB.arg_size()) + break; + + Value *Arg = CB.getArgOperand(ArgIdx); + // only check indirect (memory) operands that are pointers + if (CI.isIndirect && Arg->getType()->isPointerTy()) { + // skip compile time constants and allocas + if (!isa(Arg) && + !isa(Arg->stripPointerCasts())) { + unsigned ObjSize = 0; + Type *PointeeTy = Arg->getType()->getPointerElementType(); + uint32_t TypeID = 0; + if (PointeeTy->isSized()) { + ObjSize = DL.getTypeAllocSize(PointeeTy); + TypeID = UF.UC.getOrCreateTypeID(PointeeTy); + } + Value *Size = ConstantInt::get(UF.UC.Int64Ty, ObjSize); + Value *Ptr = UF.checkPointer(Arg, Size, true, IRB, TypeID); // dereference pointer + CB.setArgOperand(ArgIdx, Ptr); + } + } + ++ArgIdx; + } + + // Next, handle inline assembly patterns using parsed constraints + auto AsmStr = IA->getAsmString(); + + // Check if all constraints are clobbers (no inputs/outputs) + bool allClobbers = true; + for (auto &CI : Constraints) { + if (CI.Type != InlineAsm::isClobber) { + allClobbers = false; + break; + } + } + + // Handle trap/crash instructions: ud2, int3, .byte 0x0f,0x0b, etc. + // These have only clobber constraints and should be replaced with exit() + if (allClobbers && + (AsmStr.find("ud2") != StringRef::npos || + AsmStr.find(".byte 0x0f, 0x0b") != StringRef::npos || + AsmStr.find("int3") != StringRef::npos || + AsmStr.find("int $3") != StringRef::npos || + AsmStr.find("hlt") != StringRef::npos)) { + Value *Result = IRB.CreateCall(UF.UC.ExitFn, + {ConstantInt::get(UF.UC.Int32Ty, 180)}); + UF.UC.markNosanitize(Result); + CB.replaceAllUsesWith(Result); + CB.eraseFromParent(); + return; + } + + // Handle call/callq instructions in inline asm + // Extract the callee symbol and route through normal call handling + StringRef AsmStrRef(AsmStr); + auto CallPos = AsmStrRef.find("callq "); + if (CallPos == StringRef::npos) + CallPos = AsmStrRef.find("call "); + if (CallPos != StringRef::npos) { + // Extract symbol name after call/callq + StringRef After = AsmStrRef.substr( + CallPos + (AsmStrRef[CallPos + 4] == 'q' ? 6 : 5)); + // Trim leading whitespace + After = After.ltrim(); + // Symbol name ends at whitespace, newline, or end of string + auto EndPos = After.find_first_of(" \t\n\r;"); + StringRef Symbol = (EndPos != StringRef::npos) ? + After.substr(0, EndPos) : After; + // Strip operand modifiers like ${0:P} - if it starts with $, it's a register operand + if (!Symbol.empty() && Symbol[0] != '$' && Symbol[0] != '%' && + Symbol[0] != '*') { + // Look up the function in the module + Function *Callee = CB.getModule()->getFunction(Symbol); + + // If not found, the symbol is only referenced in inline asm + if (!Callee) { + // If the inline asm has side effects (volatile), insert a compiler + // barrier to preserve ordering before removing it + if (IA->hasSideEffects()) { + auto *Barrier = InlineAsm::get( + FunctionType::get(Type::getVoidTy(*UF.UC.Ctx), false), + "", "~{memory}", true); + auto *BarrierCall = IRB.CreateCall(Barrier); + UF.UC.markNosanitize(BarrierCall); + } + if (CB.getType()->isVoidTy() || CB.use_empty()) { + // No return value or no uses — safe to just delete + CB.eraseFromParent(); + return; + } + // Has uses — try to find a type-matching input as passthrough + // e.g., "={rsp},{rsp}" means rsp is passed through unchanged + Value *Passthrough = nullptr; + for (unsigned I = 0; I < CB.arg_size(); ++I) { + if (CB.getArgOperand(I)->getType() == CB.getType()) { + Passthrough = CB.getArgOperand(I); + break; + } + } + if (Passthrough) { + CB.replaceAllUsesWith(Passthrough); + } else { + CB.replaceAllUsesWith(Constant::getNullValue(CB.getType())); + } + CB.eraseFromParent(); + return; + } + { + // Build input args from parsed constraints + SmallVector CallArgs; + unsigned InArgIdx = 0; + for (auto &CI : Constraints) { + if (CI.Type == InlineAsm::isClobber) + continue; + if (CI.Type == InlineAsm::isOutput && !CI.isIndirect) + continue; + if (InArgIdx >= CB.arg_size()) + break; + if (CI.Type == InlineAsm::isInput) { + CallArgs.push_back(CB.getArgOperand(InArgIdx)); + } + ++InArgIdx; + } + + // Check if the callee has a wrapper (custom or auto-custom) + DenseMap::iterator UnwrappedFnIt = + UF.UC.UnwrappedFnMap.find(Callee); + if (UnwrappedFnIt != UF.UC.UnwrappedFnMap.end()) { + // Replace inline asm with a direct call to the callee + // visitWrappedCallBase will redirect to the wrapper + FunctionType *FT = Callee->getFunctionType(); + + // Adjust args to match function signature + SmallVector AdjustedArgs; + for (unsigned I = 0; I < FT->getNumParams() && I < CallArgs.size(); ++I) { + Value *Arg = CallArgs[I]; + if (Arg->getType() != FT->getParamType(I)) { + Arg = IRB.CreateBitOrPointerCast(Arg, FT->getParamType(I)); + UF.UC.markNosanitize(Arg); + } + AdjustedArgs.push_back(Arg); + } + + CallInst *NewCall = IRB.CreateCall(Callee, AdjustedArgs); + UF.UC.markNosanitize(NewCall); + + if (!CB.getType()->isVoidTy()) { + // Handle return value: replace all uses of the inline asm result + if (CB.getType() == NewCall->getType()) { + CB.replaceAllUsesWith(NewCall); + } else if (StructType *ST = dyn_cast(CB.getType())) { + // Inline asm may return a struct; replace ExtractValue uses + for (auto *U : CB.users()) { + if (auto *EI = dyn_cast(U)) { + if (EI->getIndices()[0] == 0) { + // First element is typically the return value + Value *Cast = IRB.CreateBitOrPointerCast(NewCall, EI->getType()); + UF.UC.markNosanitize(Cast); + EI->replaceAllUsesWith(Cast); + } else { + // Other elements get zero/null + EI->replaceAllUsesWith(Constant::getNullValue(EI->getType())); + } + UF.SkipInsts.insert(EI); + UF.RemovalInsts.push_back(EI); + } + } + } + } + CB.eraseFromParent(); + + // Now handle the new call through the normal wrapped call path + visitWrappedCallBase(UnwrappedFnIt->second, *NewCall); + return; + } + + // For in-scope or normal out-of-scope functions, replace with direct call + FunctionType *FT = Callee->getFunctionType(); + SmallVector AdjustedArgs; + for (unsigned I = 0; I < FT->getNumParams() && I < CallArgs.size(); ++I) { + Value *Arg = CallArgs[I]; + if (Arg->getType() != FT->getParamType(I)) { + Arg = IRB.CreateBitOrPointerCast(Arg, FT->getParamType(I)); + UF.UC.markNosanitize(Arg); + } + AdjustedArgs.push_back(Arg); + } + + CallInst *NewCall = IRB.CreateCall(Callee, AdjustedArgs); + UF.UC.markNosanitize(NewCall); + + if (!CB.getType()->isVoidTy()) { + if (CB.getType() == NewCall->getType()) { + CB.replaceAllUsesWith(NewCall); + } else if (isa(CB.getType())) { + for (auto *U : CB.users()) { + if (auto *EI = dyn_cast(U)) { + if (EI->getIndices()[0] == 0) { + Value *Cast = IRB.CreateBitOrPointerCast(NewCall, EI->getType()); + UF.UC.markNosanitize(Cast); + EI->replaceAllUsesWith(Cast); + } else { + EI->replaceAllUsesWith(Constant::getNullValue(EI->getType())); + } + UF.SkipInsts.insert(EI); + UF.RemovalInsts.push_back(EI); + } + } + } + } + CB.eraseFromParent(); + + // Handle shadow propagation for the new call through visitCallBase + visitCallBase(*NewCall); + return; + } + } + } + + return; +} + +void UCSanVisitor::visitIndirectCallBase(Value *FPtr, CallBase &CB) { + Value *Shadow = UF.getShadow(FPtr); + Type *RT = CB.getFunctionType()->getReturnType(); + const DataLayout &DL = getDataLayout(); + + // inline the under-constrained handling procedure + // get the current basic block and its ID + auto *curBB = CB.getParent(); + auto *BBID = curBB->getTerminator()->getMetadata(BBIDName); + PHINode* RetPhiNode = nullptr; + + // check if the indirect called function is a valid one + IRBuilder<> IRB(&CB); + Instruction *TB, *EB; + PointerType *FPtrTy = cast(FPtr->getType()); + // first, we check if the function pointer is null + Value *Cond1 = IRB.CreateICmpNE(FPtr, ConstantPointerNull::get(FPtrTy)); + UF.UC.markNosanitize(Cond1); + // second, we check if the function ptr is symbolic + Value *Cond2 = IRB.CreateICmpEQ(Shadow, UF.UC.ZeroPrimitiveShadow); + UF.UC.markNosanitize(Cond2); + // Cond1 && Cond2 + Value *And = IRB.CreateAnd(Cond1, Cond2); + UF.UC.markNosanitize(And); + // if not null and not symbolic, we can call it directly + SplitBlockAndInsertIfThenElse(And, &CB, &TB, &EB); + // Update dominator tree after block split + UF.DT.recalculate(*UF.F); + // get the correct merge bb + curBB = TB->getParent()->getSingleSuccessor(); + assert(curBB != nullptr && "Expected single successor after if-then-else"); + IRB.SetInsertPoint(curBB->getFirstNonPHI()); + // remove bbid from then else blocks, and reannotate current block + if (BBID) { + TB->getParent()->getTerminator()->setMetadata(BBIDName, nullptr); + EB->getParent()->getTerminator()->setMetadata(BBIDName, nullptr); + curBB->getTerminator()->setMetadata(BBIDName, BBID); + } + Value *retTB = nullptr, *retEB = nullptr; + if (!RT->isVoidTy()) { + RetPhiNode = IRB.CreatePHI(RT, 2); + UF.UC.markNosanitize(RetPhiNode); + } + auto *FT = CB.getFunctionType(); + unsigned ArgOffset = 0; + { // then block - *valid*: call the underlying function + IRBuilder<> IRB_TB(TB); + std::vector Args; + for (unsigned I = 0, N = FT->getNumParams(); I != N; ++I) { + Value *Arg = CB.getArgOperand(I); + unsigned Size = DL.getTypeAllocSize(UF.UC.getShadowTy(Arg)); + // Stop storing if arguments' size overflows. Inside a function, arguments + // after overflow have zero shadow values. + if (ArgOffset + Size > ArgTLSSize) + break; + StoreInst *SI = IRB_TB.CreateAlignedStore( + UF.getShadow(CB.getArgOperand(I)), + UF.getArgTLS(FT->getParamType(I), ArgOffset, IRB_TB), + ShadowTLSAlignment); + UF.UC.markNosanitize(SI); + ArgOffset += alignTo(Size, ShadowTLSAlignment); + Args.push_back(Arg); + } + retTB = IRB_TB.CreateCall( + CB.getFunctionType(), + FPtr, + Args, ""); + if (RetPhiNode) RetPhiNode->addIncoming(retTB, IRB_TB.GetInsertBlock()); + } + { // else block + IRBuilder<> IRB_EB(EB); + // resign shadow for args + for (unsigned I = 0, N = FT->getNumParams(); I != N; ++I) { + Value *Arg = CB.getArgOperand(I); + unsigned Size = DL.getTypeAllocSize(UF.UC.getShadowTy(Arg)); + // Stop storing if arguments' size overflows. Inside a function, arguments + // after overflow have zero shadow values. + if (ArgOffset + Size > ArgTLSSize) + break; + if (getenv("KO_RESIGN_PTRARGS") && Arg->getType()->isPointerTy()) { + // only resign ptr args + std::vector Args; + ConstantInt *CI; + if (Arg->getType()->getPointerElementType()->isSized()) { + CI = ConstantInt::get(UF.UC.Int64Ty, + DL.getTypeSizeInBits(Arg->getType()->getPointerElementType()) / 8); + } else { + CI = ConstantInt::get(UF.UC.Int64Ty, 0); + } + auto BCI = IRB_EB.CreateBitCast(Arg, UF.UC.VoidPtrTy); // FIXME: cast to i32* (defined as void *) + auto ArgTLS = UF.getArgTLS(Arg->getType(), ArgOffset, IRB_EB); + Args.push_back(BCI); + Args.push_back(ArgTLS); + Args.push_back(CI); + Args.push_back(ConstantPointerNull::get(UF.UC.VoidPtrTy)); + StoreInst *SI = IRB_EB.CreateAlignedStore(UF.getShadow(Arg), ArgTLS, ShadowTLSAlignment); + UF.UC.markNosanitize(SI); + CallInst *Call = IRB_EB.CreateCall(UF.UC.UCResignShadowFn, Args, ""); + UF.UC.markNosanitize(Call); + } + ArgOffset += alignTo(Size, ShadowTLSAlignment); + } + // wrap the returned value + if (!RT->isVoidTy()) { + std::vector Args; + ConstantInt *Size = ConstantInt::get(UF.UC.Int64Ty, DL.getTypeSizeInBits(RT)); + ConstantInt *isPtr = ConstantInt::get(UF.UC.Int1Ty, RT->isPointerTy()); + Args.push_back(Size); + Args.push_back(UF.getRetvalTLS(RT, IRB_EB)); + Args.push_back(isPtr); + Args.push_back(ConstantPointerNull::get(UF.UC.VoidPtrTy)); + Value *Ret = IRB_EB.CreateCall(UF.UC.UCWrapRetvalFn, Args, ""); + UF.UC.markNosanitize(Ret); + if (!RT->isPointerTy()) { // if not a pointer return + Type *PRT = RT->getPointerTo(); + Ret = IRB_EB.CreatePointerCast(Ret, PRT); + UF.UC.markNosanitize(Ret); + Ret = IRB_EB.CreateLoad(RT, Ret); + UF.UC.markNosanitize(Ret); + } else { + Ret = IRB_EB.CreatePointerCast(Ret, RT); + UF.UC.markNosanitize(Ret); + } + retEB = Ret; + if (RetPhiNode) RetPhiNode->addIncoming(retEB, IRB_EB.GetInsertBlock()); + } + } + + if (RetPhiNode) { + // Load return shadow from RetvalTLS at the merge point. + // Both branches write their return shadow to RetvalTLS: + // then-block: callee writes it normally + // else-block: ucsan_wrap_retval writes it + IRB.SetInsertPoint(RetPhiNode->getNextNonDebugInstruction()); + unsigned Size = DL.getTypeAllocSize(UF.UC.getShadowTy(RT)); + if (Size > kRetvalTLSSize) { + UF.ValShadowMap[RetPhiNode] = UF.UC.getZeroShadow(RT); + } else { + LoadInst *LI = IRB.CreateAlignedLoad( + UF.UC.getShadowTy(RT), UF.getRetvalTLS(RT, IRB), + ShadowTLSAlignment, "_dfsret"); + UF.UC.markNosanitize(LI); + UF.SkipInsts.insert(LI); + UF.ValShadowMap[RetPhiNode] = LI; + UF.NonZeroChecks.push_back(LI); + } + CB.replaceAllUsesWith(RetPhiNode); + } + CB.eraseFromParent(); + +} + +bool UCSanVisitor::visitWrappedCallBase(Function *F, CallBase &CB) { + IRBuilder<> IRB(&CB); + Value *Shadow = nullptr; + const DataLayout &DL = getDataLayout(); + FunctionType *FT = F->getFunctionType(); + switch (UF.UC.getWrapperKind(F)) { + case UCSan::WK_None: + // No wrapper needed, fall through to default behavior + llvm_unreachable("WK_None should not be handled here"); + return false; + case UCSan::WK_ShimTarget: + llvm_unreachable("WK_ShimTarget should not be in UnwrappedFnMap"); + return false; + case UCSan::WK_ShimOrig: + { + // Redirect call to __shim_ function, let normal TLS path handle shadows + std::string ShimName = "__shim_" + F->getName().str(); + FunctionCallee ShimF = UF.UC.Mod->getOrInsertFunction(ShimName, F->getFunctionType()); + CB.setCalledFunction(ShimF); + return false; // let caller handle arg/retval TLS + } + case UCSan::WK_AutoCustom: + // invoke the custom function + { + // Only store shadows for fixed parameters, varargs are not tracked in TLS + // FIXME: add vararg shadow tracking support + unsigned NumFixedParams = FT->getNumParams(); + unsigned ArgOffset = 0; + + for (unsigned I = 0; I < NumFixedParams; ++I) { + unsigned Size = + DL.getTypeAllocSize(UF.UC.getShadowTy(FT->getParamType(I))); + // Stop storing if arguments' size overflows. Inside a function, + // arguments after overflow have zero shadow values. + if (ArgOffset + Size > ArgTLSSize) + report_fatal_error("Argument size overflow in custom function"); + StoreInst *SI = IRB.CreateAlignedStore( + UF.getShadow(CB.getArgOperand(I)), + UF.getArgTLS(FT->getParamType(I), ArgOffset, IRB), + ShadowTLSAlignment); + UF.UC.markNosanitize(SI); + ArgOffset += alignTo(Size, ShadowTLSAlignment); + } + + // For taint ref_names, the wrapper just forwards to the ref function + // and TaintPass will handle the __dfsw_ wrapping via its own dfsan TLS. + // Skip reading retval TLS in that case + auto It = UF.UC.Scope.custom.find(F->getName().str()); + bool IsTaintRef = (It != UF.UC.Scope.custom.end()) && + UF.UC.ABIList.isIn(It->second.ref_name, "taint"); + + CB.setCalledFunction(UF.UC.getCustomFunction(F)); + if (!FT->getReturnType()->isVoidTy()) { + if (IsTaintRef) { + // No ucsan return shadow from the wrapper; set zero. + UF.setShadow(&CB, UF.UC.getZeroShadow(&CB)); + } else { + IRB.SetInsertPoint(CB.getNextNode()); + LoadInst *LI = IRB.CreateAlignedLoad(UF.UC.getShadowTy(&CB), + UF.getRetvalTLS(CB.getType(), IRB), + ShadowTLSAlignment, "_autoret"); + UF.UC.markNosanitize(LI); + Shadow = LI; + UF.setShadow(&CB, Shadow); + } + } + } + return true; + + case UCSan::WK_Discard: + // Completely discarded, nothing to do + return true; + + case UCSan::WK_Ignore: + llvm_unreachable("WK_Ignore should be handled by external wrapper"); + return false; + + case UCSan::WK_TaintCustom: + case UCSan::WK_Uninstrumented: + { + // TaintPass will handle the __dfsw_ wrapping with proper taint labels. + // Just check pointer arguments here, skip ucsan arg/retval TLS. + // + // For memory/string functions, use type_id=0 (typeless bytes) and pass + // the actual byte count to checkPointer so lazy init allocates enough. + StringRef FName = F->getName(); + // Determine the length argument index from the abilist (sizeN category). + // -1 means no explicit length (string functions: use size=0). + int LenArgIdx = UF.UC.ABIList.getSizeArgIdx(FName); + if (LenArgIdx == -1 && FName == "memmem") { + LenArgIdx = -2; // special: arg1 is len for arg0, arg3 is len for arg2 + } + // All taint-listed functions are byte-typed mem/str fns; non-mem taint + // fns like assert_cond/assume_cond have no pointer args so the predicate + // is only consulted on pointer-arg paths anyway. + bool IsMemOrStrFn = UF.UC.ABIList.isIn(*F, "taint"); + + // Functions whose return value is a pointer into a pointer argument + // (e.g. strchr/memchr return base+index). Capture the base arg's UC shadow + // so we can translate the materialized result pointer back into UC pseudo + // space after the call (see below). + int RetPtrIdx = UF.UC.ABIList.getRetPtrArgIdx(FName); + Value *RetPtrBaseShadow = nullptr; + + Value *LenVal = nullptr; + if (LenArgIdx >= 0 && (unsigned)LenArgIdx < CB.arg_size()) { + LenVal = IRB.CreateZExtOrTrunc(CB.getArgOperand(LenArgIdx), UF.UC.Int64Ty); + UF.UC.markNosanitize(LenVal); + } + + // Check copy semantics from abilist: copystr is unbounded (size determined + // by strlen at runtime), so we need check_copy_bounds to enlarge the dst + // symbolic obj. Bounded copies (memcpy/memmove/strncpy/strncat) have their + // size in a sizeN entry; check_pointer handles enlargement directly. + bool IsCopyStr = UF.UC.ABIList.isIn(*F, "copystr"); + + Value *ResolvedDst = nullptr, *ResolvedSrc = nullptr; + Value *DstShadow = nullptr, *SrcShadow = nullptr; + Value *DstBound = ConstantInt::get(UF.UC.Int64Ty, 0); + + auto *I = CB.arg_begin(); + for (unsigned N = FT->getNumParams(); N != 0; ++I, --N) { + unsigned ArgIdx = FT->getNumParams() - N; + // skip nullptr + if ((*I)->getType()->isPointerTy() && !isa(*I)) { + Value *sizeArg; + uint32_t TypeID; + if (IsMemOrStrFn) { + TypeID = 0; // typeless bytes + if (LenArgIdx == -2) { + // memmem: arg0 uses arg1 as len, arg2 uses arg3 as len + unsigned MyLenIdx = (ArgIdx == 0) ? 1 : 3; + if (MyLenIdx < CB.arg_size()) { + sizeArg = IRB.CreateZExtOrTrunc(CB.getArgOperand(MyLenIdx), UF.UC.Int64Ty); + UF.UC.markNosanitize(sizeArg); + } else { + sizeArg = ConstantInt::get(UF.UC.Int64Ty, 0); + } + } else if (LenVal) { + sizeArg = LenVal; + } else { + sizeArg = ConstantInt::get(UF.UC.Int64Ty, 0); // string fn, unknown length + } + } else { + Type *PointeeTy = (*I)->getType()->getPointerElementType(); + sizeArg = ConstantInt::get(UF.UC.Int64Ty, DL.getTypeAllocSize(PointeeTy)); + TypeID = UF.UC.getOrCreateTypeID(PointeeTy); + } + + // Capture shadow before checkPointer resolves the pointer + bool skipCheckPointer = false; + if ((IsCopyStr || LenVal) && ArgIdx <= 1) { + Value *Shadow = UF.getShadow(*I); + if (ArgIdx == 0) { + DstShadow = Shadow; + // For GV destinations, shadow is zero but we know the size at compile time + if (auto *GV = dyn_cast((*I)->stripPointerCasts())) { + Type *T = GV->getValueType(); + if (T && (T->isArrayTy() || T->isStructTy()) && T->isSized()) { + DstBound = ConstantInt::get(UF.UC.Int64Ty, DL.getTypeAllocSize(T)); + } + } + if (isa(getUnderlyingObject((*I)->stripPointerCasts()))) { + // destination is part of aglobal variable, skip + skipCheckPointer = true; + } + } else if (ArgIdx == 1) { + SrcShadow = Shadow; + } + } + + // Capture the UC shadow of the base arg (before checkPointer resolves + // the pointer) for return-pointer translation. + if ((int)ArgIdx == RetPtrIdx) + RetPtrBaseShadow = UF.getShadow(*I); + + Value *rptr = *I; + if (!skipCheckPointer) { + rptr = UF.checkPointer(*I, sizeArg, true, IRB, TypeID); + } + CB.setArgOperand(ArgIdx, rptr); + + // Capture resolved pointers for copy bounds check + if (IsCopyStr && ArgIdx <= 1) { + if (ArgIdx == 0) ResolvedDst = rptr; + else if (ArgIdx == 1) ResolvedSrc = rptr; + } + } + } + + // Emit copy bounds check for unbounded copies (size=strlen(src)). + if (IsCopyStr && ResolvedDst && ResolvedSrc && + DstShadow && SrcShadow) { + Value *DstAddr = IRB.CreateBitCast(ResolvedDst, UF.UC.VoidPtrTy); + Value *SrcAddr = IRB.CreateBitCast(ResolvedSrc, UF.UC.VoidPtrTy); + UF.UC.markNosanitize(DstAddr); + UF.UC.markNosanitize(SrcAddr); + IRB.CreateCall(UF.UC.UCCheckCopyBoundsFn, + {DstAddr, DstShadow, SrcAddr, SrcShadow, DstBound}); + } + + if (!FT->getReturnType()->isVoidTy()) { + if (RetPtrIdx >= 0 && FT->getReturnType()->isPointerTy() && + RetPtrBaseShadow) { + // The result points into the materialized buffer (base+index in np). + // Translate it back into the caller's UC pseudo space using the base + // arg's UC shadow, so pointer arithmetic against the original arg + // (e.g. p - c == index) is preserved. The symsan taint label is + // carried separately through the __dfsw_ retval TLS (different shadow + // memory), so rewriting the pointer value here does not affect it. + IRBuilder<> AfterIRB(CB.getNextNode()); + // Only emit (and mark nosanitize) a cast when one is actually needed. + // CreateBitCast returns &CB unchanged when the types already match, and + // marking that nosanitize would suppress TaintPass on the real call. + Value *RetArg = &CB; + if (CB.getType() != UF.UC.VoidPtrTy) { + RetArg = AfterIRB.CreateBitCast(&CB, UF.UC.VoidPtrTy); + UF.UC.markNosanitize(RetArg); + } + CallInst *Unchecked = AfterIRB.CreateCall( + UF.UC.UCUncheckPointerFn, {RetArg, RetPtrBaseShadow}); + // Do NOT mark the uncheck call nosanitize: TaintPass must visit it to + // propagate the symsan taint label from arg 0 (mirrors how it treats + // ucsan_check_pointer). nosanitize would suppress that propagation. + Value *RetFixed = Unchecked; + if (CB.getType() != Unchecked->getType()) { + RetFixed = AfterIRB.CreateBitCast(Unchecked, CB.getType()); + UF.UC.markNosanitize(RetFixed); + } + // Redirect downstream uses to the UC pointer, but not the + // bitcast/uncheck chain we just created. + CB.replaceUsesWithIf(RetFixed, [&](Use &U) { + User *user = U.getUser(); + return user != RetArg && user != Unchecked && user != RetFixed; + }); + // The returned pointer aliases the base object and carries its UC shadow. + if (Instruction *RetFixedI = dyn_cast(RetFixed)) + if (RetFixedI != &CB) + UF.setShadow(RetFixedI, RetPtrBaseShadow); + UF.setShadow(&CB, RetPtrBaseShadow); + } else { + // Set zero ucsan shadow for the return value; TaintPass sets the taint label. + UF.setShadow(&CB, UF.UC.getZeroShadow(&CB)); + } + } + return true; // fully handled, skip normal arg/retval TLS stores + } + case UCSan::WK_Custom: + { + // Call the __dfsw_ wrapper with shadow arguments + CallInst *CI = dyn_cast(&CB); + if (!CI) + return false; + + auto FName = F->getName(); + bool IsContractPrim = FName.startswith("assume_") || FName.startswith("assert_"); + + TransformedFunction CustomFn = UF.UC.getCustomFunctionType(FT); + std::string CustomFName = "__dfsw_" + FName.str(); + FunctionCallee CustomF = + UF.UC.Mod->getOrInsertFunction(CustomFName, CustomFn.TransformedType); + if (Function *CustomFnPtr = dyn_cast(CustomF.getCallee())) { + CustomFnPtr->copyAttributesFrom(F); + + // Custom functions returning non-void will write to the return label. + if (!FT->getReturnType()->isVoidTy()) { + CustomFnPtr->removeFnAttrs(UF.UC.ReadOnlyNoneAttrs); + } + // mark as nosanitize + UF.UC.markFunctionNosanitize(CustomFnPtr); + } + + std::vector Args; + + // Adds non-variable arguments. + auto *I = CB.arg_begin(); + for (unsigned N = FT->getNumParams(); N != 0; ++I, --N) { + Type *T = (*I)->getType(); + if (isa(T) && !IsContractPrim) { + // Check pointer arguments before passing to custom function + auto DL = getDataLayout(); + Type *PointeeTy = T->getPointerElementType(); + Value *sizeArg = + ConstantInt::get(UF.UC.Int64Ty, + DL.getTypeAllocSize(PointeeTy)); + uint32_t TypeID = UF.UC.getOrCreateTypeID(PointeeTy); + Value *rptr = UF.checkPointer(*I, sizeArg, true, IRB, TypeID); + Args.push_back(rptr); + } else { + Args.push_back(*I); + } + } + + // Then push shadow labels for each argument + I = CB.arg_begin(); + const unsigned ShadowArgStart = Args.size(); + for (unsigned N = FT->getNumParams(); N != 0; ++I, --N) { + Args.push_back(UF.getShadow(*I)); + } + + // For vararg functions, push shadow for varargs + if (FT->isVarArg()) { + auto *LabelVATy = ArrayType::get(UF.UC.PrimitiveShadowTy, + CB.arg_size() - FT->getNumParams()); + auto *LabelVAAlloca = new AllocaInst( + LabelVATy, getDataLayout().getAllocaAddrSpace(), + "labelva", &UF.F->getEntryBlock().front()); + + for (unsigned N = 0; I != CB.arg_end(); ++I, ++N) { + auto *LabelVAPtr = IRB.CreateStructGEP(LabelVATy, LabelVAAlloca, N); + UF.UC.markNosanitize(LabelVAPtr); + auto *SI = IRB.CreateStore(UF.getShadow(*I), LabelVAPtr); + UF.UC.markNosanitize(SI); + } + + auto *VAA = IRB.CreateStructGEP(LabelVATy, LabelVAAlloca, 0); + UF.UC.markNosanitize(VAA); + Args.push_back(VAA); + } + + // Add pointer to return label if function returns non-void + Type *RetTy = FT->getReturnType(); + if (!RetTy->isVoidTy()) { + if (!UF.LabelReturnAlloca) { + UF.LabelReturnAlloca = + new AllocaInst(UF.UC.getShadowTy(RetTy), + getDataLayout().getAllocaAddrSpace(), + "labelreturn", &UF.F->getEntryBlock().front()); + } + Args.push_back(UF.LabelReturnAlloca); + } + + // Add any remaining vararg arguments + append_range(Args, drop_begin(CB.args(), FT->getNumParams())); + + CallInst *CustomCI = IRB.CreateCall(CustomF, Args); + CustomCI->setCallingConv(CI->getCallingConv()); + CustomCI->setAttributes(TransformFunctionAttributes( + CustomFn, *UF.UC.Ctx, CI->getAttributes())); + UF.UC.markNosanitize(CustomCI); + + // Update the parameter attributes of the custom call instruction to + // zero extend the shadow parameters. This is required for targets + // which consider ShadowTy an illegal type. + for (unsigned N = 0; N < FT->getNumParams(); N++) { + const unsigned ArgNo = ShadowArgStart + N; + if (CustomCI->getArgOperand(ArgNo)->getType() == + UF.UC.PrimitiveShadowTy) { + CustomCI->addParamAttr(ArgNo, Attribute::ZExt); + } + } + + // Load the return value shadow + if (!RetTy->isVoidTy()) { + LoadInst *LabelLoad = + IRB.CreateLoad(UF.UC.getShadowTy(RetTy), UF.LabelReturnAlloca); + UF.UC.markNosanitize(LabelLoad); + UF.setShadow(CustomCI, LabelLoad); + } + + CI->replaceAllUsesWith(CustomCI); + CI->eraseFromParent(); + return true; + } + } + + return false; +} + +void UCSanVisitor::visitCallBase(CallBase &CB) { + Function *F = CB.getCalledFunction(); + PHINode* RetPhiNode = nullptr; + + if (auto *IA = dyn_cast(CB.getCalledOperand())) { + // handle inline assembly calls + visitInlineAsm(IA, CB); + return; + } + + // intrinsics are handled elsewhere + if (F && F->isIntrinsic()) { + return; + } + + // Callee may call ucsan_resign_shadow which changes pointer state; + // invalidate all cached check_pointer results so subsequent accesses + // go through ucsan_check_pointer again for lazy re-initialization. + UF.CheckedPtrMap.clear(); + + if (!F) { + // indirect call + visitIndirectCallBase(CB.getCalledOperand(), CB); + return; + } + + DenseMap::iterator UnwrappedFnIt = + UF.UC.UnwrappedFnMap.find(F); + if (UnwrappedFnIt != UF.UC.UnwrappedFnMap.end()) { + if (visitWrappedCallBase(UnwrappedFnIt->second, CB)) + return; + } + + IRBuilder<> IRB(&CB); + FunctionType *FT = CB.getFunctionType(); + const DataLayout &DL = getDataLayout(); + + // Stores argument shadows. + if (F && F->hasName() && !F->getName().startswith("__dfsan") && + !F->getName().startswith("__taint")) { + unsigned ArgOffset = 0; + for (unsigned I = 0, N = FT->getNumParams(); I != N; ++I) { + unsigned Size = + DL.getTypeAllocSize(UF.UC.getShadowTy(FT->getParamType(I))); + // Stop storing if arguments' size overflows. Inside a function, arguments + // after overflow have zero shadow values. + if (ArgOffset + Size > ArgTLSSize) + break; + StoreInst *SI = IRB.CreateAlignedStore( + UF.getShadow(CB.getArgOperand(I)), + UF.getArgTLS(FT->getParamType(I), ArgOffset, IRB), + ShadowTLSAlignment); + UF.UC.markNosanitize(SI); + ArgOffset += alignTo(Size, ShadowTLSAlignment); + } + } + + Instruction *Next = nullptr; + if (!CB.getType()->isVoidTy()) { + if (InvokeInst *II = dyn_cast(&CB)) { + if (II->getNormalDest()->getSinglePredecessor()) { + Next = &II->getNormalDest()->front(); + } else { + BasicBlock *NewBB = + SplitEdge(II->getParent(), II->getNormalDest(), &UF.DT); + Next = &NewBB->front(); + } + } else { + assert(CB.getIterator() != CB.getParent()->end()); + Next = CB.getNextNode(); + } + + // Don't emit the epilogue for musttail call returns. + if (isa(CB) && cast(CB).isMustTailCall()) + return; + + IRBuilder<> NextIRB(Next); + unsigned Size = DL.getTypeAllocSize(UF.UC.getShadowTy(&CB)); + if (Size > kRetvalTLSSize) { + // Set overflowed return shadow to be zero. + UF.setShadow(&CB, UF.UC.getZeroShadow(&CB)); + } else { + LoadInst *LI = NextIRB.CreateAlignedLoad( + UF.UC.getShadowTy(&CB), UF.getRetvalTLS(CB.getType(), NextIRB), + ShadowTLSAlignment, "_dfsret"); + UF.UC.markNosanitize(LI); + UF.SkipInsts.insert(LI); + UF.setShadow(&CB, LI); + UF.NonZeroChecks.push_back(LI); + } + } +} + +void UCSanVisitor::visitCastInst(CastInst &CI) { + // Propagate shadow through cast + Value *Shadow = UF.getShadow(CI.getOperand(0)); + UF.setShadow(&CI, Shadow); +} + +void UCSanVisitor::visitReturnInst(ReturnInst &RI) { + IRBuilder<> IRB(&RI); + + // Pop stack frame before returning to free stack allocation labels + CallInst *PopCall = IRB.CreateCall(UF.UC.UCPopStackFrameFn, {}); + UF.UC.markNosanitize(PopCall); + + if (RI.getReturnValue()) { + Value *S = UF.getShadow(RI.getReturnValue()); + Type *RT = UF.F->getFunctionType()->getReturnType(); + unsigned Size = getDataLayout().getTypeAllocSize(UF.UC.getShadowTy(RT)); + if (Size <= kRetvalTLSSize) { + StoreInst *SI = IRB.CreateAlignedStore(S, UF.getRetvalTLS(RT, IRB), ShadowTLSAlignment); + UF.UC.markNosanitize(SI); + } + } else { + // for void return, clean the tls return value shadow to zero + // store void* enough? + Type *RT = UF.UC.VoidPtrTy; + unsigned Size = getDataLayout().getTypeAllocSize(UF.UC.getShadowTy(RT)); + if (Size <= kRetvalTLSSize) { + StoreInst *SI = IRB.CreateAlignedStore(UF.UC.getZeroShadow(RT), + UF.getRetvalTLS(RT, IRB), ShadowTLSAlignment); + UF.UC.markNosanitize(SI); + } + } + + if (ClTraceBB) { + CallInst::Create(UF.UC.UCTraceBBFn, + { ConstantInt::get(UF.UC.Int32Ty, -1), ConstantInt::get(UF.UC.Int32Ty, 0)}, + "", &RI); + } +} + +void UCSanVisitor::visitAtomicRMWInst(AtomicRMWInst &I) { + auto &DL = I.getModule()->getDataLayout(); + Value *Ptr = I.getPointerOperand(); + Type *Ty = I.getType(); + unsigned StoreSize = DL.getTypeStoreSize(Ty); + ConstantInt *Size = ConstantInt::get(UF.UC.Int64Ty, StoreSize); + uint32_t TypeID = UF.UC.getOrCreateTypeID(Ty); + + IRBuilder<> IRB(&I); + Ptr = UF.checkPointer(Ptr, Size, true, IRB, TypeID); + I.setOperand(0, Ptr); + + // FIXME: AtomicRMWInst should not operate on ptrs + UF.setShadow(&I, UF.UC.ZeroPrimitiveShadow); + + I.setMetadata("ucsan.checked", MDNode::get(*UF.UC.Ctx, None)); +} + +void UCSanVisitor::visitAllocaInst(AllocaInst &I) { + Type *T = I.getAllocatedType(); + bool isArray = I.isArrayAllocation() || T->isArrayTy() || T->isStructTy(); + bool AllLoadsStores = true; + for (User *U : I.users()) { + if (isa(U)) { + continue; + } + if (StoreInst *SI = dyn_cast(U)) { + if (SI->getPointerOperand() == &I) { + continue; + } + } + + AllLoadsStores = false; + break; + } + if (AllLoadsStores) { + IRBuilder<> IRB(&I); + AllocaInst *AI = IRB.CreateAlloca(UF.UC.getShadowTy(T), I.getArraySize(), + I.getName() + ".ucsan"); + UF.UC.markNosanitize(AI); + UF.AllocaShadowMap[&I] = AI; + if (getenv("KO_CHECKER_UBI")) { + // Set shadow to kUninitializedLabel for UBI detection + StoreInst *SI = IRB.CreateStore(UF.UC.UninitPrimitiveShadow, AI); + UF.UC.markNosanitize(SI); + } + } else { + // For complex allocas that aren't arrays/structs, set shadow memory to kUninitializedLabel + if (getenv("KO_CHECKER_UBI") && !isArray) { + IRBuilder<> IRB(I.getNextNode()); + auto DL = I.getModule()->getDataLayout(); + auto allocaSizeInBits = I.getAllocationSizeInBits(DL); + if (allocaSizeInBits.hasValue()) { + int allocaSizeInBytes = (allocaSizeInBits->getFixedValue() + 7) >> 3; + Value* Size = ConstantInt::get(UF.UC.Int64Ty, allocaSizeInBytes); + Value* Ptr = IRB.CreateBitOrPointerCast(&I, UF.UC.VoidPtrTy); + if (Ptr != &I) { UF.UC.markNosanitize(Ptr); } + CallInst *CI = IRB.CreateCall(UF.UC.UCSetLabelFn, {UF.UC.UninitPrimitiveShadow, Ptr, Size}); + UF.UC.markNosanitize(CI); + } + } + } + + // Track bounds for stack allocations (arrays/structs) + if (isArray) { + // Insert after the alloca instruction to get the address + BasicBlock::iterator ip(&I); + IRBuilder<> IRB(I.getParent(), ++ip); + + // Get array size + Value *Size = IRB.CreateZExtOrTrunc(I.getArraySize(), UF.UC.Int64Ty); + if (Size != I.getArraySize()) { + UF.UC.markNosanitize(Size); + } + + // Get element size + const DataLayout &DL = getDataLayout(); + uint64_t es = DL.getTypeAllocSize(I.getAllocatedType()); + ConstantInt *ElemSize = ConstantInt::get(UF.UC.Int64Ty, es); + + // Get address + Value *Address = IRB.CreatePtrToInt(&I, UF.UC.Int64Ty); + UF.UC.markNosanitize(Address); + + // Call runtime to track stack bounds: ucsan_trace_alloca(Size, ElemSize, Address) + // (ucsan_trace_alloca sets shadow to kUninitializedLabel for UBI detection) + CallInst *Bounds = IRB.CreateCall(UF.UC.UCTraceAllocaFn, {Size, ElemSize, Address}); + UF.UC.markNosanitize(Bounds); + UF.setShadow(&I, Bounds); + } else { + UF.setShadow(&I, UF.UC.ZeroPrimitiveShadow); + } +} + +void UCSanVisitor::visitBranchInst(BranchInst &BI) { + // Only check conditional branches + if (BI.isUnconditional()) return; + + // Check if UBI checker is enabled + if (!getenv("KO_CHECKER_UBI")) return; + + // Get shadow of branch condition + Value *Condition = BI.getCondition(); + Value *Shadow = UF.getShadow(Condition); + + // Skip if shadow is statically known to be zero + if (UF.UC.isZeroShadow(Shadow)) return; + + // Insert UBI check before the branch + IRBuilder<> IRB(&BI); + CallInst *CI = IRB.CreateCall(UF.UC.UCCheckUBIFn, {Shadow}); + UF.UC.markNosanitize(CI); +} + +void UCSanVisitor::visitBinaryOperator(BinaryOperator &BO) { + Value *Op1Shadow = UF.getShadow(BO.getOperand(0)); + Value *Op2Shadow = UF.getShadow(BO.getOperand(1)); + + // If both shadows are zero, result is zero + if (UF.UC.isZeroShadow(Op1Shadow) && UF.UC.isZeroShadow(Op2Shadow)) { + UF.setShadow(&BO, UF.UC.getZeroShadow(&BO)); + return; + } + + // Call runtime to combine labels + IRBuilder<> IRB(&BO); + CallInst *CombinedShadow = IRB.CreateCall(UF.UC.UCCombineLabelFn, {Op1Shadow, Op2Shadow}); + UF.UC.markNosanitize(CombinedShadow); + UF.setShadow(&BO, CombinedShadow); +} + +void UCSanVisitor::visitCmpInst(CmpInst &CI) { + Value *Op1 = CI.getOperand(0); + Value *Op2 = CI.getOperand(1); + Value *Op1Shadow = UF.getShadow(Op1); + Value *Op2Shadow = UF.getShadow(Op2); + + // If both shadows are zero, result is zero + if (UF.UC.isZeroShadow(Op1Shadow) && UF.UC.isZeroShadow(Op2Shadow)) { + UF.setShadow(&CI, UF.UC.getZeroShadow(&CI)); + return; + } + + // If comparing with null pointer constant, just use the other operand's shadow + if (isa(Op1)) { + UF.setShadow(&CI, Op2Shadow); + return; + } + if (isa(Op2)) { + UF.setShadow(&CI, Op1Shadow); + return; + } + + // Call runtime to combine labels (comparing pointers is fine) + IRBuilder<> IRB(&CI); + CallInst *CombinedShadow = IRB.CreateCall(UF.UC.UCCombineLabelFn, {Op1Shadow, Op2Shadow}); + UF.UC.markNosanitize(CombinedShadow); + UF.setShadow(&CI, CombinedShadow); +} + +void UCSanVisitor::visitSelectInst(SelectInst &I) { + Value *TrueShadow = UF.getShadow(I.getTrueValue()); + Value *FalseShadow = UF.getShadow(I.getFalseValue()); + + Value *ShadowSel; + if (TrueShadow == FalseShadow) { + ShadowSel = TrueShadow; + } else { + ShadowSel = SelectInst::Create(I.getCondition(), TrueShadow, FalseShadow, "", &I); + } + UF.setShadow(&I, ShadowSel); +} + +void UCSanVisitor::visitPHINode(PHINode &PN) { + Type *ShadowTy = UF.UC.getShadowTy(PN.getType()); + PHINode *ShadowPN = + PHINode::Create(ShadowTy, PN.getNumIncomingValues(), "", &PN); + + // Give the shadow phi node valid predecessors to fool SplitEdge into working. + Value *UndefShadow = UndefValue::get(ShadowTy); + for (PHINode::block_iterator i = PN.block_begin(), e = PN.block_end(); i != e; + ++i) { + ShadowPN->addIncoming(UndefShadow, *i); + } + + UF.PHIFixups.push_back({&PN, ShadowPN}); + UF.setShadow(&PN, ShadowPN); +} + +void UCSanVisitor::visitUnreachableInst(UnreachableInst &I) { + IRBuilder<> IRB(&I); + Value *ExitCall = IRB.CreateCall(UF.UC.ExitFn, + {ConstantInt::get(UF.UC.Int32Ty, 0)}); + UF.UC.markNosanitize(ExitCall); +} + +bool UCSan::initializeModule(Module &M) { + Triple TargetTriple(M.getTargetTriple()); + if (TargetTriple.getOS() != Triple::Linux) + report_fatal_error("unsupported operating system"); + switch (TargetTriple.getArch()) { + case Triple::x86_64: + MapParams = &Linux_X86_64_MemoryMapParams; + break; + default: + report_fatal_error("unsupported architecture"); + } + + Mod = &M; + Ctx = &M.getContext(); + const DataLayout &DL = M.getDataLayout(); + + // Initialize basic types + Int1Ty = IntegerType::getInt1Ty(*Ctx); + Int8Ty = IntegerType::getInt8Ty(*Ctx); + Int16Ty = IntegerType::getInt16Ty(*Ctx); + Int32Ty = IntegerType::getInt32Ty(*Ctx); + Int64Ty = IntegerType::getInt64Ty(*Ctx); + IntptrTy = M.getDataLayout().getIntPtrType(*Ctx); + VoidPtrTy = PointerType::getUnqual(Int8Ty); + + PrimitiveShadowTy = IntegerType::get(*Ctx, ShadowWidthBits); // 16-bit labels + PrimitiveShadowPtrTy = PointerType::getUnqual(PrimitiveShadowTy); // pointer to 16-bit shadow + ZeroPrimitiveShadow = ConstantInt::getSigned(PrimitiveShadowTy, 0); + UninitPrimitiveShadow = ConstantInt::getSigned(PrimitiveShadowTy, -1); + ShadowPtrMul = ConstantInt::getSigned(IntptrTy, ShadowWidthBytes); // 2 for 16-bit + ShadowPtrAndMask = ShadowPtrXorMask = ShadowPtrBase = nullptr; + if (MapParams->AndMask != 0) + ShadowPtrAndMask = ConstantInt::get(IntptrTy, ~MapParams->AndMask); + if (MapParams->XorMask != 0) + ShadowPtrXorMask = ConstantInt::get(IntptrTy, MapParams->XorMask); + if (MapParams->ShadowBase != 0) + ShadowPtrBase = ConstantInt::get(IntptrTy, MapParams->ShadowBase); + + // Initialize runtime functions + initializeRuntimeFunctions(M); + + // Load metadata + if (!loadMetadata()) { + report_fatal_error("Failed to load metadata"); + } + initializeCustomFunctionTypes(); + + // Initialize builtin type IDs (fixed across modules) + initBuiltinTypeIDs(); + + // Load existing type table (for multi-file compilation) + loadTypeTable(); + + // Initialize ABIList from command-line abilist files + if (!ClABIListFiles.empty()) { + std::vector AllABIListFiles(ClABIListFiles.begin(), ClABIListFiles.end()); + ABIList.set( + SpecialCaseList::createOrDie(AllABIListFiles, *vfs::getRealFileSystem())); + } + + // Initialize ReadOnlyNoneAttrs for custom functions + ReadOnlyNoneAttrs.addAttribute(Attribute::ReadOnly) + .addAttribute(Attribute::ReadNone); + + return true; +} + +bool UCSan::runImpl(Module &M) { + initializeModule(M); + bool Changed = false; + + Type *ArgTLSTy = ArrayType::get(Int64Ty, ArgTLSSize / 8); + ArgTLS = Mod->getOrInsertGlobal("__ucsan_arg_tls", ArgTLSTy); + if (GlobalVariable *G = dyn_cast(ArgTLS)) { + Changed |= G->getThreadLocalMode() != GlobalVariable::InitialExecTLSModel; + G->setThreadLocalMode(GlobalVariable::InitialExecTLSModel); + } + + Type *RetvalTLSTy = ArrayType::get(Int64Ty, kRetvalTLSSize / 8); + RetvalTLS = Mod->getOrInsertGlobal("__ucsan_retval_tls", RetvalTLSTy); + if (GlobalVariable *G = dyn_cast(RetvalTLS)) { + Changed |= G->getThreadLocalMode() != GlobalVariable::InitialExecTLSModel; + G->setThreadLocalMode(GlobalVariable::InitialExecTLSModel); + } + + std::vector FnsToInstrument; + std::vector FnsOutOfScope; + + // make sure we have the entry function in metadata + if (Scope.entry.empty()) { + report_fatal_error("No entry function specified in metadata"); + } + + // Filter functions based on scope + for (Function &F : M) { + if (F.isIntrinsic()) continue; + + // Skip runtime functions (basic check) + if (UCRuntimeFunctions.find(&F) != UCRuntimeFunctions.end()) { + continue; + } + + // Demangle the LLVM mangled name before matching against the YAML scope, + // which lists demangled (human) names. For plain C names demangleName() + // returns the name unchanged. + std::string FName = demangleName(F.getName()); + + // Check if function is in scope + bool inScope = false; + if (Scope.entry == FName) { + inScope = true; + } else { + for (const auto &scopeFn : Scope.scope) { + if (scopeFn == FName) { + inScope = true; + break; + } + } + } + + if (inScope) { + FnsToInstrument.push_back(&F); + } else { + auto WK = getWrapperKind(&F); + if (WK == WK_TaintCustom && ClWithTaintPass) { + // TaintPass will call the custom wrapper and pass taint labels. + // UCSan will only do check_pointer on pointer args at call sites. + UnwrappedFnMap[&F] = &F; + // Remove the body of there is one + if (!F.isDeclaration()) F.deleteBody(); + } else if (WK == WK_Discard) { + // Replace with a trivial stub so both UCSan and TaintPass skip it + Function *Stub = buildDiscardFunction(&F); + F.replaceAllUsesWith(Stub); + F.deleteBody(); + } else if (WK == WK_Uninstrumented) { + // Leave the function as is, but add to UnwrappedFnMap + UnwrappedFnMap[&F] = &F; + } else if (WK == WK_Custom || WK == WK_AutoCustom) { + if (!F.isDeclaration()) F.deleteBody(); + UnwrappedFnMap[&F] = &F; + } else if (WK == WK_ShimOrig) { + // Original function: delete body, redirect calls via visitWrappedCallBase + if (!F.isDeclaration()) F.deleteBody(); + UnwrappedFnMap[&F] = &F; + } else if (WK == WK_ShimTarget) { + // LLM-generated __shim_ function, instrument like in-scope + FnsToInstrument.push_back(&F); + } else { + FnsOutOfScope.push_back(&F); + } + } + + if (Scope.entry != "main" && F.getName() == "main") { + F.setName("__original$main"); + } + } + + // Build dangle wrappers for out-of-scope functions + for (Function *F : FnsOutOfScope) { + if (Function *dangle = buildDangleFunction(F)) { + F->replaceAllUsesWith(dangle); + F->eraseFromParent(); + } + } + + // check if we need to dump cov information + std::unique_ptr COVF; + if (!ClDumpCfg.empty()) { + std::error_code EC; + COVF = std::make_unique(ClDumpCfg, EC, llvm::sys::fs::OF_Append | llvm::sys::fs::OF_Text); + if (EC) { + errs() << "Failed to open control flow file: " << ClDumpCfg << "\n"; + COVF.reset(); + } + } + + // Instrument in-scope functions + for (Function *F : FnsToInstrument) { + if (!F || F->isDeclaration()) continue; + + // Make in-scope functions externally visible for debugging/linking, + // but preserve internal/private linkage to avoid multiple definitions + // when linking (e.g. static inline functions like percpu_counter_add) + if (!F->hasLocalLinkage()) + F->setLinkage(GlobalValue::ExternalLinkage); + + // Kernel-style inputs may use 8-byte stack alignment; force realignment so + // inserted UCSan runtime calls follow userspace ABI requirements. + F->addFnAttr(Attribute::getWithStackAlignment(*Ctx, Align(16))); + F->addFnAttr("stackrealign"); + + // Add driver wrapper for entry point + bool isEntry = false; + if (demangleName(F->getName()) == Scope.entry || + (Scope.entry == "main" && F->getName() == "__original$main")) { + buildDriverWrapperFunction(F); + isEntry = true; + } + + // Annotate BBs for all functions (excluding entry) + if (!isEntry) { + unsigned int Idx = find(Scope.scope, demangleName(F->getName())) - Scope.scope.begin() + 2; + unsigned int BBCount = 0; + for (Function::iterator BB = F->begin(); BB != F->end(); BB++, BBCount++) { + auto *BBID = ConstantInt::get(Int32Ty, Idx * BBID_STEP + BBCount); + if (ClTraceBB) { + // getFirstInsertionPt() (not getFirstNonPHI) so the trace call lands + // after any landingpad in C++ EH blocks, keeping the module valid. + CallInst::Create(UCTraceBBFn, {ConstantInt::get(Int32Ty, Idx), BBID}, + "", &*BB->getFirstInsertionPt()); + } + MDNode *MD = MDNode::get(Mod->getContext(), + {ConstantAsMetadata::get(BBID)}); + BB->getTerminator()->setMetadata(BBIDName, MD); + } + } + + if (F->getLinkage() == GlobalValue::AvailableExternallyLinkage) + F->setLinkage(GlobalValue::WeakAnyLinkage); + + // Instrument ALL functions (including entry point) + removeUnreachableBlocks(*F); + UCSanFunction UF(*this, F); + + // Insert push_stack_frame call at function entry for stack bounds tracking + BasicBlock &EntryBB = F->getEntryBlock(); + IRBuilder<> EntryIRB(&*(EntryBB.getFirstInsertionPt())); + CallInst *PushCall = EntryIRB.CreateCall(UCPushStackFrameFn, {}); + markNosanitize(PushCall); + + // UCSanVisitor may create new basic blocks, which confuses df_iterator. + // Build a copy of the list before iterating over it. + SmallVector BBList(depth_first(&F->getEntryBlock())); + for (BasicBlock *BB : BBList) { + + // Dump cov info + if (COVF) { + auto getBBID = [&](BasicBlock *B) -> int64_t { + auto *MD = B->getTerminator()->getMetadata(BBIDName); + if (!MD) return -1; + auto *C = dyn_cast(MD->getOperand(0)); + return dyn_cast(C->getValue())->getZExtValue(); + }; + + int64_t CurID = getBBID(BB); + if (CurID >= 0) { + Instruction *Term = BB->getTerminator(); + // COV := debug_info:current_bbid:termination_types + // debug_info := file,line,column + // types := C (conditional branch) + // := D (unconditional branch) + // := S (switch cases) + // := R (return) + // := U (unreachable) + if (const auto &DL = Term->getDebugLoc()) { + *COVF << DL->getFilename() << "," << DL.getLine() << "," << DL.getCol(); + } else { + *COVF << F->getName() << ",0,0"; + } + *COVF << ":" << CurID << ":"; + if (auto *BR = dyn_cast(Term)) { + if (BR->isConditional()) { + // condition branch: C:T:true_target_id:F:false_target_id + *COVF << "C:T:" << getBBID(BR->getSuccessor(0)) + << ":F:" << getBBID(BR->getSuccessor(1)) << "\n"; + } else { + // unconditional branch: D:next_bbid + *COVF << "D:" << getBBID(BR->getSuccessor(0)) << "\n"; + } + } else if (auto *SI = dyn_cast(Term)) { + // switch cases: S:default_bbid:case1_bbid:case2_bbid:... + *COVF << "S"; + for (unsigned i = 0, n = SI->getNumSuccessors(); i < n; ++i) { + *COVF << ":" << getBBID(SI->getSuccessor(i)); + } + *COVF << "\n"; + } else if (isa(Term)) { + // return: "R" + *COVF << "R\n"; + } else if (isa(Term)) { + // unreachable: "U" + *COVF << "U\n"; + } else { + errs() << "Warning: unhandled termination type: " << *Term << "\n"; + } + } + } + + Instruction *Inst = &BB->front(); + while (true) { + // UCSanVisitor may split the current basic block, changing the current + // instruction's next pointer and moving the next instruction to the + // tail block from which we should continue. + Instruction *Next = Inst->getNextNode(); + // UCSanVisitor may delete Inst, so keep track of whether it was a + // terminator. + bool IsTerminator = Inst->isTerminator(); + if (!UF.SkipInsts.count(Inst)) + UCSanVisitor(UF).visit(Inst); + + if (IsTerminator) break; + Inst = Next; + } + } + + // We will not necessarily be able to compute the shadow for every phi node + // until we have visited every block. Therefore, the code that handles phi + // nodes adds them to the PHIFixups list so that they can be properly + // handled here. + for (auto &P : UF.PHIFixups) { + for (unsigned Val = 0, N = P.Phi->getNumIncomingValues(); Val != N; + ++Val) { + P.ShadowPhi->setIncomingValue( + Val, UF.getShadow(P.Phi->getIncomingValue(Val))); + } + } + + UF.emitMemoryAccessSummaries(); + + // Removal instructions + for (auto *RI : UF.RemovalInsts) + RI->eraseFromParent(); + } + + // Fix initializer for declared global variables with external linkage + // Also strip dso_local so llc generates PIC-compatible relocations + // (needed for code compiled without -fPIC, e.g. kernel) + for (GlobalVariable &GV : M.globals()) { + if (GV.isDSOLocal() && !GV.hasLocalLinkage() && GV.hasDefaultVisibility()) { + // Keep dso_local on globals used in inline asm with immediate ("i") + // constraint, as their address must be a compile-time constant. + // Walk through ConstantExpr users (e.g. GEP, bitcast) to find + // indirect uses in inline asm. + bool usedAsImmInAsm = false; + SmallVector Worklist(GV.users().begin(), GV.users().end()); + SmallPtrSet Visited; + while (!Worklist.empty() && !usedAsImmInAsm) { + User *U = Worklist.pop_back_val(); + if (!Visited.insert(U).second) + continue; + if (isa(U)) { + Worklist.append(U->user_begin(), U->user_end()); + continue; + } + auto *CB = dyn_cast(U); + if (!CB || !CB->isInlineAsm()) + continue; + auto *IA = cast(CB->getCalledOperand()); + auto Constraints = IA->ParseConstraints(); + unsigned argIdx = 0; + for (auto &CI : Constraints) { + if (CI.Type != InlineAsm::isInput) + continue; + if (argIdx < CB->arg_size() && + CB->getArgOperand(argIdx)->stripPointerCasts() == &GV) { + for (auto &Code : CI.Codes) { + if (Code == "i") { + usedAsImmInAsm = true; + break; + } + } + } + if (usedAsImmInAsm) break; + argIdx++; + } + } + if (!usedAsImmInAsm) + GV.setDSOLocal(false); + } + if (GV.isDeclaration() && GV.hasExternalLinkage()) { + GV.setLinkage(GlobalValue::WeakAnyLinkage); + GV.setInitializer(Constant::getNullValue(GV.getValueType())); + } + } + for (Function &F : M) { + if (F.isDSOLocal() && !F.hasLocalLinkage() && F.hasDefaultVisibility()) { + F.setDSOLocal(false); + } + } + + // === C++ exception-handling passthrough === + // The dangle loop replaced C++ EH runtime functions with __external$ stubs + // that return normally, and erased the originals (F->eraseFromParent()). + // __cxa_throw is noreturn, so when its stub returns the caller hits + // `unreachable` (UB -> segfault / wrong exit code). Fix: rebuild the stubs as + // passthroughs to the real libc++ implementations so throw/catch works inside + // instrumented code. Because the originals were erased, we re-declare them via + // getOrInsertFunction using the stub's (preserved) signature. + { + static const char *CxxEHNames[] = { + "__cxa_throw", "__cxa_rethrow", + "__cxa_allocate_exception", "__cxa_free_exception", + "__cxa_begin_catch", "__cxa_end_catch", + nullptr}; + for (int idx = 0; CxxEHNames[idx]; ++idx) { + const char *Name = CxxEHNames[idx]; + std::string ExtName = std::string("__external$") + Name; + Function *ExtF = M.getFunction(ExtName); + if (!ExtF) continue; // EH function not used in this module + + // Re-declare the real libc++ implementation with the stub's signature. + FunctionCallee RealCallee = + M.getOrInsertFunction(Name, ExtF->getFunctionType()); + Function *OrigF = dyn_cast(RealCallee.getCallee()); + if (!OrigF) continue; + + // Rebuild the stub body as a passthrough to the real function. + ExtF->deleteBody(); + BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", ExtF); + IRBuilder<> IRB(BB); + std::vector Args; + for (auto &Arg : ExtF->args()) Args.push_back(&Arg); + + bool IsNoReturn = StringRef(Name) == "__cxa_throw" || + StringRef(Name) == "__cxa_rethrow"; + CallInst *CI = IRB.CreateCall(OrigF, Args); + if (IsNoReturn) { + CI->addFnAttr(Attribute::NoReturn); + IRB.CreateUnreachable(); + } else if (ExtF->getReturnType()->isVoidTy()) { + IRB.CreateRetVoid(); + } else { + IRB.CreateRet(CI); + } + } + + // Restore the real __gxx_personality_v0 on all instrumented functions. + // The dangle loop's replaceAllUsesWith rewired the personality references to + // the stub; revert them so libunwind can correctly walk instrumented frames + // during EH unwinding. + Function *StubPersonality = M.getFunction("__external$__gxx_personality_v0"); + if (StubPersonality) { + FunctionCallee RealCallee = M.getOrInsertFunction( + "__gxx_personality_v0", StubPersonality->getFunctionType()); + if (Function *RealPersonality = dyn_cast(RealCallee.getCallee())) { + for (Function &F : M) { + if (F.hasPersonalityFn() && + F.getPersonalityFn()->stripPointerCasts() == StubPersonality) { + F.setPersonalityFn(RealPersonality); + } + } + } + } + } + + // Emit type table JSON + emitTypeTable(); + + return true; +} + +} // anonymous namespace + +namespace { +class UCSanPass : public PassInfoMixin { +public: + UCSanPass() = default; + + PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM) { + if (UCSan().runImpl(M)) { + return PreservedAnalyses::none(); + } + return PreservedAnalyses::all(); + } + + static bool isRequired() { return true; } +}; +} // anonymous namespace + +extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK +llvmGetPassPluginInfo() { + return {LLVM_PLUGIN_API_VERSION, "UCSanPass", "v1.0", + [](PassBuilder &PB) { + PB.registerOptimizerLastEPCallback( + [](ModulePassManager &MPM, OptimizationLevel OL) { + MPM.addPass(UCSanPass()); + }); + PB.registerPipelineParsingCallback( + [](StringRef Name, ModulePassManager &MPM, + ArrayRef) { + if (Name == "ucsan") { + MPM.addPass(UCSanPass()); + return true; + } + return false; + }); + }}; +} diff --git a/instrumentation/UCSanSummary.h b/instrumentation/UCSanSummary.h new file mode 100644 index 00000000..1ee60397 --- /dev/null +++ b/instrumentation/UCSanSummary.h @@ -0,0 +1,122 @@ +//===- UCSanSummary.h - Shared UCSan/Taint metadata helpers -----*- C++ -*-===// +// +// Shared metadata schema for summaries emitted by UCSanPass and consumed by +// TaintPass. Keep this header small: it is included by LLVM pass plugins only. +// +//===----------------------------------------------------------------------===// + +#ifndef SYMSAN_INSTRUMENTATION_UCSAN_SUMMARY_H +#define SYMSAN_INSTRUMENTATION_UCSAN_SUMMARY_H + +#include "llvm/ADT/StringRef.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/Metadata.h" + +#include + +namespace symsan { +namespace ucsan { + +static constexpr const char *MemSummaryMetadataName = "ucsan.mem_summary"; +static constexpr const char *PtrFieldAccessKind = "ptr_field_access"; + +struct MemoryAccessSummary { + unsigned ArgNo = 0; + int64_t FieldOffset = 0; + uint64_t AccessSize = 0; + bool IsWrite = false; + uint32_t TypeID = 0; + unsigned Line = 0; + unsigned Col = 0; +}; + +static inline bool getMDString(llvm::MDNode *N, unsigned Idx, + llvm::StringRef &Out) { + if (!N || Idx >= N->getNumOperands()) + return false; + auto *S = llvm::dyn_cast_or_null(N->getOperand(Idx)); + if (!S) + return false; + Out = S->getString(); + return true; +} + +static inline bool getMDConstantInt(llvm::MDNode *N, unsigned Idx, + llvm::ConstantInt *&Out) { + if (!N || Idx >= N->getNumOperands()) + return false; + Out = llvm::mdconst::dyn_extract(N->getOperand(Idx)); + return Out != nullptr; +} + +static inline bool parseMemoryAccessSummary(llvm::MDNode *N, + MemoryAccessSummary &Out) { + llvm::StringRef Kind; + if (!getMDString(N, 0, Kind) || Kind != PtrFieldAccessKind) + return false; + + llvm::ConstantInt *ArgNo = nullptr; + llvm::ConstantInt *FieldOffset = nullptr; + llvm::ConstantInt *AccessSize = nullptr; + llvm::ConstantInt *IsWrite = nullptr; + llvm::ConstantInt *TypeID = nullptr; + llvm::ConstantInt *Line = nullptr; + llvm::ConstantInt *Col = nullptr; + if (!getMDConstantInt(N, 1, ArgNo) || + !getMDConstantInt(N, 2, FieldOffset) || + !getMDConstantInt(N, 3, AccessSize) || + !getMDConstantInt(N, 4, IsWrite) || + !getMDConstantInt(N, 5, TypeID) || + !getMDConstantInt(N, 6, Line) || + !getMDConstantInt(N, 7, Col)) + return false; + + Out.ArgNo = ArgNo->getZExtValue(); + Out.FieldOffset = FieldOffset->getSExtValue(); + Out.AccessSize = AccessSize->getZExtValue(); + Out.IsWrite = IsWrite->isOne(); + Out.TypeID = TypeID->getZExtValue(); + Out.Line = Line->getZExtValue(); + Out.Col = Col->getZExtValue(); + return true; +} + +static inline llvm::MDNode *createMemoryAccessSummaryMD( + llvm::LLVMContext &C, const MemoryAccessSummary &Summary) { + auto *Int1Ty = llvm::Type::getInt1Ty(C); + auto *Int32Ty = llvm::Type::getInt32Ty(C); + auto *Int64Ty = llvm::Type::getInt64Ty(C); + llvm::Metadata *Ops[] = { + llvm::MDString::get(C, PtrFieldAccessKind), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int32Ty, Summary.ArgNo)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int64Ty, Summary.FieldOffset, true)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int64Ty, Summary.AccessSize)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int1Ty, Summary.IsWrite)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int32Ty, Summary.TypeID)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int32Ty, Summary.Line)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int32Ty, Summary.Col)), + }; + return llvm::MDNode::get(C, Ops); +} + +static inline llvm::MDNode *getMemoryAccessSummaries(const llvm::Function &F) { + return F.getMetadata(MemSummaryMetadataName); +} + +static inline void setMemoryAccessSummaries(llvm::Function &F, + llvm::MDNode *Summaries) { + F.setMetadata(MemSummaryMetadataName, Summaries); +} + +} // namespace ucsan +} // namespace symsan + +#endif // SYMSAN_INSTRUMENTATION_UCSAN_SUMMARY_H diff --git a/libcxx/CMakeLists.txt b/libcxx/CMakeLists.txt index c3c53ffc..361d2a5a 100644 --- a/libcxx/CMakeLists.txt +++ b/libcxx/CMakeLists.txt @@ -4,3 +4,12 @@ install (FILES "build_taint/lib/libc++.a" DESTINATION "${SYMSAN_LIB_DIR}") install (FILES "build_taint/lib/libc++abi.a" DESTINATION "${SYMSAN_LIB_DIR}") install (FILES "build_taint/lib/libunwind.a" DESTINATION "${SYMSAN_LIB_DIR}") +# Plain (uninstrumented) EH runtime for UCSan-only C++ targets — run +# rebuild_native.sh to produce these. Installed with a -native suffix so they +# coexist with the taint-instrumented archives above. See rebuild_native.sh +# for why UCSan needs a plain libc++abi/libunwind for exception handling. +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/build_native/lib/libc++abi.a") + install (FILES "build_native/lib/libc++abi.a" DESTINATION "${SYMSAN_LIB_DIR}" RENAME "libc++abi-native.a") + install (FILES "build_native/lib/libunwind.a" DESTINATION "${SYMSAN_LIB_DIR}" RENAME "libunwind-native.a") +endif() + diff --git a/libcxx/build_native/lib/libc++abi.a b/libcxx/build_native/lib/libc++abi.a new file mode 100644 index 00000000..55f18733 Binary files /dev/null and b/libcxx/build_native/lib/libc++abi.a differ diff --git a/libcxx/build_native/lib/libunwind.a b/libcxx/build_native/lib/libunwind.a new file mode 100644 index 00000000..e5cd2922 Binary files /dev/null and b/libcxx/build_native/lib/libunwind.a differ diff --git a/libcxx/build_taint/lib/libc++.a b/libcxx/build_taint/lib/libc++.a index 0f23b3fa..d1b39c8f 100644 Binary files a/libcxx/build_taint/lib/libc++.a and b/libcxx/build_taint/lib/libc++.a differ diff --git a/libcxx/build_taint/lib/libc++abi.a b/libcxx/build_taint/lib/libc++abi.a index 4079695a..1831ffa6 100644 Binary files a/libcxx/build_taint/lib/libc++abi.a and b/libcxx/build_taint/lib/libc++abi.a differ diff --git a/libcxx/build_taint/lib/libunwind.a b/libcxx/build_taint/lib/libunwind.a index bc6c3b2f..a71f6f63 100644 Binary files a/libcxx/build_taint/lib/libunwind.a and b/libcxx/build_taint/lib/libunwind.a differ diff --git a/libcxx/rebuild_native.sh b/libcxx/rebuild_native.sh new file mode 100755 index 00000000..ee88b406 --- /dev/null +++ b/libcxx/rebuild_native.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Build a *plain* (uninstrumented) C++ exception-handling runtime — +# libc++abi + libunwind — for under-constrained (UCSan-only) binaries. +# +# Why a separate build: +# UCSan replaces out-of-scope libc++ with "dangle" stubs that return +# arbitrary symbolic values. C++ exception handling cannot be modeled that +# way: __cxa_throw is noreturn and must transfer control to a landing pad. +# The EH passthrough in UCSanPass therefore re-points the dangle stubs at the +# *plain* __cxa_* / __gxx_personality_v0 symbols, which must be resolved by a +# real, concretely-running EH runtime. +# +# The taint-instrumented libc++abi (built by rebuild.sh with ko-clang) renames +# every instrumented function with a ".taint" suffix, so it exports +# __cxa_throw.taint, not the plain __cxa_throw the passthrough calls. This +# script builds libc++abi + libunwind with plain clang-14, giving the plain EH +# symbols. Only the EH runtime is needed; libc++ (the STL) is left +# out-of-scope and dangled by UCSan. +# +# usage: rebuild_native.sh +# Override the compiler with KO_NATIVE_CC / KO_NATIVE_CXX (default clang-14). + +LLVM_VERSION=14.0.6 + +CC=${KO_NATIVE_CC:-clang-14} +CXX=${KO_NATIVE_CXX:-clang++-14} + +NINJA_B=`which ninja 2>/dev/null` + +if [ "$NINJA_B" = "" ]; then + echo "[-] Error: can't find 'ninja' in your \$PATH. please install ninja-build" 1>&2 + echo "[-] Debian&Ubuntu: sudo apt-get install ninja-build" 1>&2 + exit 1 +fi + +set -euxo pipefail + +CUR_DIR=`pwd` +LLVM_SRC="llvm_project" + +if [ ! -d $LLVM_SRC ]; then + git clone --depth 1 --branch llvmorg-${LLVM_VERSION} https://github.com/llvm/llvm-project.git $LLVM_SRC +fi + +mkdir -p build_native +rm -rf build_native/* + +# Plain clang-14 — no ko-clang, no taint/ucsan instrumentation, so the EH +# entry points keep their plain names (__cxa_throw, __gxx_personality_v0, ...). +cmake -G Ninja -S $LLVM_SRC/runtimes -B build_native \ + -DLLVM_TARGETS_TO_BUILD=X86 -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=${CC} -DCMAKE_CXX_COMPILER=${CXX} \ + -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" \ + -DLIBCXXABI_ENABLE_SHARED=OFF -DLIBCXX_ENABLE_SHARED=OFF \ + -DLIBUNWIND_ENABLE_SHARED=OFF \ + -DLIBCXX_CXX_ABI="libcxxabi" \ + -DLIBCXXABI_USE_LLVM_UNWINDER=ON \ + -DLLVM_DISTRIBUTION_COMPONENTS="cxxabi;unwind" + +# Only build the EH runtime: libc++abi (the __cxa_*/personality layer) and +# libunwind (the unwinder). libcxx is enabled only so libc++abi can find its +# headers at build time. +ninja -C build_native cxxabi unwind diff --git a/parsers/rgd-parser.cpp b/parsers/rgd-parser.cpp index 2a6f43cd..f7606070 100644 --- a/parsers/rgd-parser.cpp +++ b/parsers/rgd-parser.cpp @@ -108,7 +108,7 @@ static void printAst(FILE* f, const rgd::AstNode *node, int indent) { fprintf(f, ")\n"); } -int RGDAstParser::restart(std::vector &inputs) { +int RGDAstParser::restart(std::vector &inputs, bool copy_input) { // save a copy of the inputs inputs_cache = inputs; // clear caches diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index d19e0238..d5f1967c 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -20,7 +20,7 @@ target_include_directories(pysymsan PRIVATE target_link_libraries(pysymsan PRIVATE launcher z3parser - z3 + ${Z3_LIBRARY} ${Python3_LIBRARIES} rt ) diff --git a/python/README.md b/python/README.md index 3b79f831..00d16406 100644 --- a/python/README.md +++ b/python/README.md @@ -1,21 +1,194 @@ -Provide a python binding to launch symsan-instrumented binary, receive events, parse constraints, and solve constraints. +# SymSan Python Binding +Python binding to launch SymSan-instrumented binaries, receive events, parse constraints, and solve constraints. + +## Usage + +```python +import sys +sys.path.insert(0, 'symsan_python_path') +import symsan + +# Or set PYTHONPATH +# PYTHONPATH=symsan_python_path python3 your_script.py ``` -static PyMethodDef SymSanMethods[] = { - {"init", SymSanInit, METH_VARARGS, "initialize symsan target"}, - {"config", (PyCFunction)SymSanConfig, METH_VARARGS | METH_KEYWORDS, "config symsan"}, - {"run", (PyCFunction)SymSanRun, METH_VARARGS | METH_KEYWORDS, "run symsan target, optional stdin=file"}, - {"read_event", SymSanReadEvent, METH_VARARGS, "read a symsan event"}, - {"terminate", (PyCFunction)SymSanTerminate, METH_NOARGS, "terminate current symsan instance"}, - {"destroy", (PyCFunction)SymSanDestroy, METH_NOARGS, "destroy symsan target"}, - {"reset_input", InitParser, METH_VARARGS, "reset the symbolic expression parser with a new input"}, - {"parse_cond", ParseCond, METH_VARARGS, "parse trace_cond event into solving tasks"}, - {"parse_gep", ParseGEP, METH_VARARGS, "parse trace_gep event into solving tasks"}, - {"add_constraint", AddConstraint, METH_VARARGS, "add a constraint"}, - {"record_memcmp", RecordMemcmp, METH_VARARGS, "record a memcmp event"}, - {"solve_task", SolveTask, METH_VARARGS, "solve a task"}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; + +## API Reference + +### Initialization & Configuration + +#### `symsan.init(program_path, [uniontable_size])` +Initialize SymSan target with the instrumented binary. + +**Parameters:** +- `program_path` (str): Path to the instrumented binary +- `uniontable_size` (int, optional): Size of union table (default: from defs.h) + +**Returns:** Capsule containing dfsan_label_info pointer + +#### `symsan.config(input_file, args=[], debug=0, bounds=0, undefined=0)` +Configure SymSan runtime options. + +**Parameters:** +- `input_file` (str): Path to input file or "stdin" +- `args` (list[str], optional): Command-line arguments for target +- `debug` (int, optional): Enable debug output (0 or 1) +- `bounds` (int, optional): Enable bounds checking (0 or 1) +- `undefined` (int, optional): Enable undefined behavior solving (0 or 1) + +### Execution + +#### `symsan.run([stdin_file])` +Run the instrumented target. + +**Parameters:** +- `stdin_file` (str, optional): File to use as stdin + +#### `symsan.read_event(buffer_size, [timeout])` +Read a symbolic execution event from the target. + +**Parameters:** +- `buffer_size` (int): Size of buffer to read +- `timeout` (int, optional): Timeout in milliseconds (default: 0 = no timeout) + +**Returns:** bytes containing the event data + +#### `symsan.terminate()` +Terminate the target process. + +**Returns:** Tuple of (exit_status, is_killed) + +#### `symsan.destroy()` +Clean up and destroy SymSan instance. + +### Constraint Solving + +#### `symsan.reset_input(input_list)` +Reset the symbolic expression parser with new input(s). + +**Parameters:** +- `input_list` (list[bytes]): List of input byte arrays + +#### `symsan.parse_cond(label, result, flags)` +Parse a conditional branch event into solving tasks. + +**Parameters:** +- `label` (int): DFSan label ID +- `result` (int): Branch result value +- `flags` (int): Event flags + +**Returns:** List of task IDs (int) + +#### `symsan.parse_gep(ptr_label, ptr, index_label, index, num_elems, elem_size, offset, enum_index)` +Parse a GEP (GetElementPtr) event into solving tasks. + +**Parameters:** +- `ptr_label` (int): DFSan label for pointer +- `ptr` (int): Pointer value +- `index_label` (int): DFSan label for index +- `index` (int): Index value +- `num_elems` (int): Number of elements in array +- `elem_size` (int): Size of each element +- `offset` (int): Current offset +- `enum_index` (bool): Whether to enumerate index values + +**Returns:** List of task IDs (int) + +#### `symsan.record_memcmp(label, content)` +Record a memcmp operation for constraint solving. + +**Parameters:** +- `label` (int): DFSan label ID +- `content` (bytes): Concrete content being compared + +#### `symsan.add_constraint(label, val)` +Add an explicit constraint. + +**Parameters:** +- `label` (int): DFSan label ID +- `val` (int): Constraint value + +#### `symsan.solve_task(task_id, [timeout])` +Solve a constraint task and return solutions. + +**Parameters:** +- `task_id` (int): Task ID from parse_cond or parse_gep +- `timeout` (int, optional): Timeout in milliseconds (default: 5000) + +**Returns:** Tuple of (status, solutions) +- `status` (int): Solving status code +- `solutions` (list[dict]): List of solution dictionaries + +**Status Codes:** +- `1` - invalid_task +- `2` - opt_sat (optimized satisfiable) +- `3` - opt_unsat (optimized unsatisfiable) +- `4` - opt_timeout (optimized timeout) +- `5` - nested_sat (nested satisfiable) +- `6` - opt_sat_nested_unsat +- `7` - opt_sat_nested_timeout + +## Solution Format + +Solutions are returned as dictionaries with different fields depending on the operation type. + +### OpType Enum + +The `symsan.OpType` enum defines three operation types: + +```python +symsan.OpType.SET # 0 - Set a byte value +symsan.OpType.INSERT # 1 - Insert bytes +symsan.OpType.DELETE # 2 - Delete bytes ``` -Currently only z3 solver is supported, will merge jigsaw and i2s later. +### Solution Dictionary Fields + +**Common fields (all operations):** +- `op` (int): Operation type (OpType.SET, OpType.INSERT, or OpType.DELETE) +- `id` (int): Input ID (which input file this applies to) +- `offset` (int): Byte offset in the input + +**Operation-specific fields:** + +**SET operation** - Set a single byte: +```python +{ + 'op': symsan.OpType.SET, + 'id': 0, + 'offset': 10, + 'val': 0x41 # Byte value to set +} +``` + +**INSERT operation** - Insert bytes at position: +```python +{ + 'op': symsan.OpType.INSERT, + 'id': 0, + 'offset': 5, + 'data': b'hello' # Bytes to insert +} +``` + +**DELETE operation** - Delete bytes: +```python +{ + 'op': symsan.OpType.DELETE, + 'id': 0, + 'offset': 20, + 'len': 3 # Number of bytes to delete +} +``` + +## Example Usage + +See `test.py` + +## Notes + +- Currently only Z3 solver is supported +- The binding uses Python 3.7+ (uses f-strings in examples, but core module works with 3.6+) +- Solutions are automatically sorted by offset when applying multiple operations +- INSERT and DELETE operations are designed for string constraint solving (strchr, strstr, etc.) +- SET operations are most common and used for integer/byte-level constraints diff --git a/python/symsan-py.cpp b/python/symsan-py.cpp index ed5bcef2..645e8cc5 100644 --- a/python/symsan-py.cpp +++ b/python/symsan-py.cpp @@ -22,20 +22,29 @@ extern "C" { #include #include +#include +#include #define PY_SSIZE_T_CLEAN #include +// For attach mode - track mapped shm for cleanup +static void *__attached_shm = nullptr; +static size_t __attached_shm_size = 0; + // z3parser static z3::context __z3_context; symsan::Z3ParserSolver *__z3_parser = nullptr; -static PyObject* SymSanInit(PyObject *self, PyObject *args) { +static PyObject* SymSanInit(PyObject *self, PyObject *args, PyObject *keywds) { + static const char *kwlist[] = {"program", "shm_size", "init_solver", NULL}; const char *program; unsigned long long ut_size = uniontable_size; + int init_solver = 1; // default True - if (!PyArg_ParseTuple(args, "s|K", &program, &ut_size)) { + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|Kp", + const_cast(kwlist), &program, &ut_size, &init_solver)) { return NULL; } @@ -46,11 +55,13 @@ static PyObject* SymSanInit(PyObject *self, PyObject *args) { return PyErr_SetFromErrno(PyExc_OSError); } - // setup parser - __z3_parser = new symsan::Z3ParserSolver(shm_base, ut_size, __z3_context); - if (__z3_parser == nullptr) { - fprintf(stderr, "Failed to initialize parser\n"); - return PyErr_NoMemory(); + // setup parser (optional) + if (init_solver) { + __z3_parser = new symsan::Z3ParserSolver(shm_base, ut_size, __z3_context); + if (__z3_parser == nullptr) { + fprintf(stderr, "Failed to initialize parser\n"); + return PyErr_NoMemory(); + } } return PyCapsule_New(shm_base, "dfsan_label_info", NULL); @@ -200,13 +211,104 @@ static PyObject* SymSanTerminate(PyObject *self) { static PyObject* SymSanDestroy(PyObject *self) { if (__z3_parser != nullptr) { delete __z3_parser; - symsan_destroy(); __z3_parser = nullptr; } + + // Clean up attached shm (from init_parser with shm name) + if (__attached_shm != nullptr) { + munmap(__attached_shm, __attached_shm_size); + __attached_shm = nullptr; + __attached_shm_size = 0; + } else { + // Only call symsan_destroy if we used init (launcher mode) + symsan_destroy(); + } + Py_RETURN_NONE; } +// Initialize parser from shared memory (by name or address) +// Usage: init_parser(shm_name, size) or init_parser(shm_capsule, size) static PyObject* InitParser(PyObject *self, PyObject *args) { + PyObject *shm_arg = NULL; + unsigned long long shm_size = 0; + + if (!PyArg_ParseTuple(args, "OK", &shm_arg, &shm_size)) { + return NULL; + } + + // Clean up any previous parser + if (__z3_parser != nullptr) { + delete __z3_parser; + __z3_parser = nullptr; + } + if (__attached_shm != nullptr) { + munmap(__attached_shm, __attached_shm_size); + __attached_shm = nullptr; + __attached_shm_size = 0; + } + + void *shm_base = nullptr; + + if (PyUnicode_Check(shm_arg)) { + // shm_arg is a string (shared memory name) + const char *shm_name = PyUnicode_AsUTF8(shm_arg); + if (shm_name == NULL) { + return NULL; + } + + int shm_fd = shm_open(shm_name, O_RDWR, S_IRUSR | S_IWUSR); + if (shm_fd == -1) { + fprintf(stderr, "Failed to open shm '%s': %s\n", shm_name, strerror(errno)); + return PyErr_SetFromErrno(PyExc_OSError); + } + + shm_base = mmap(0, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + close(shm_fd); + + if (shm_base == MAP_FAILED) { + fprintf(stderr, "Failed to mmap shm: %s\n", strerror(errno)); + return PyErr_SetFromErrno(PyExc_OSError); + } + + __attached_shm = shm_base; + __attached_shm_size = shm_size; + } else if (PyCapsule_CheckExact(shm_arg)) { + // shm_arg is a capsule (already mapped address) + shm_base = PyCapsule_GetPointer(shm_arg, "dfsan_label_info"); + if (shm_base == NULL) { + return NULL; + } + // Don't track for munmap - caller owns this memory + } else if (PyLong_Check(shm_arg)) { + // shm_arg is an integer address + shm_base = (void *)PyLong_AsUnsignedLongLong(shm_arg); + if (PyErr_Occurred()) { + return NULL; + } + // Don't track for munmap - caller owns this memory + } else { + PyErr_SetString(PyExc_TypeError, "first argument must be shm name (str), capsule, or address (int)"); + return NULL; + } + + // Create the parser + __z3_parser = new symsan::Z3ParserSolver(shm_base, shm_size, __z3_context); + if (__z3_parser == nullptr) { + if (__attached_shm != nullptr) { + munmap(__attached_shm, __attached_shm_size); + __attached_shm = nullptr; + __attached_shm_size = 0; + } + fprintf(stderr, "Failed to initialize parser\n"); + return PyErr_NoMemory(); + } + + Py_RETURN_NONE; +} + +// Reset parser with new input data +static PyObject* ResetParser(PyObject *self, PyObject *args) { if (__z3_parser == nullptr) { PyErr_SetString(PyExc_RuntimeError, "parser not initialized"); return NULL; @@ -239,7 +341,8 @@ static PyObject* InitParser(PyObject *self, PyObject *args) { inputs.push_back({(uint8_t*)data, size}); } - if (__z3_parser->restart(inputs) != 0) { + // Always copy input data in Python to avoid dangling pointers + if (__z3_parser->restart(inputs, true) != 0) { PyErr_SetString(PyExc_RuntimeError, "failed to restart parser"); return NULL; } @@ -247,6 +350,47 @@ static PyObject* InitParser(PyObject *self, PyObject *args) { Py_RETURN_NONE; } +// Update input cache without clearing deps (for late-arriving GV data) +static PyObject* UpdateInput(PyObject *self, PyObject *args) { + if (__z3_parser == nullptr) { + PyErr_SetString(PyExc_RuntimeError, "parser not initialized"); + return NULL; + } + + std::vector inputs; + PyObject *iargs = NULL; + + if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &iargs)) { + return NULL; + } + + Py_ssize_t argc = PyList_Size(iargs); + for (Py_ssize_t i = 0; i < argc; i++) { + PyObject *item = PyList_GetItem(iargs, i); + if (item == NULL) { + PyErr_SetString(PyExc_RuntimeError, "failed to retrieve args list"); + return NULL; + } + if (!PyBytes_Check(item)) { + PyErr_SetString(PyExc_TypeError, "args must be a list of bytes"); + return NULL; + } + Py_ssize_t size; + char *data; + if (PyBytes_AsStringAndSize(item, &data, &size) != 0) { + return NULL; + } + inputs.push_back({(uint8_t*)data, size}); + } + + if (__z3_parser->update_input(inputs, true) != 0) { + PyErr_SetString(PyExc_RuntimeError, "failed to update input"); + return NULL; + } + + Py_RETURN_NONE; +} + static PyObject* ParseCond(PyObject *self, PyObject *args) { if (__z3_parser == nullptr) { PyErr_SetString(PyExc_RuntimeError, "parser not initialized"); @@ -291,12 +435,13 @@ static PyObject* ParseGEP(PyObject *self, PyObject *args) { uint64_t num_elems = 0; uint64_t elem_size = 0; int64_t current_offset = 0; - bool enum_index = false; // XXX: default to false? + int enum_index_i = 0; // PyArg 'p' expects int* if (!PyArg_ParseTuple(args, "IKILKKLp", &ptr_label, &ptr, &index_label, &index, - &num_elems, &elem_size, ¤t_offset, &enum_index)) { + &num_elems, &elem_size, ¤t_offset, &enum_index_i)) { return NULL; } + bool enum_index = (enum_index_i != 0); std::vector tasks; if (__z3_parser->parse_gep(ptr_label, ptr, index_label, index, num_elems, @@ -363,6 +508,27 @@ static PyObject* RecordMemcmp(PyObject *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject* RecordMinimize(PyObject *self, PyObject *args) { + if (__z3_parser == nullptr) { + PyErr_SetString(PyExc_RuntimeError, "parser not initialized"); + return NULL; + } + + dfsan_label label = 0; + bool allow_zero = true; + + if (!PyArg_ParseTuple(args, "I|b", &label, &allow_zero)) { + return NULL; + } + + if (__z3_parser->record_minimize(label, allow_zero) != 0) { + PyErr_SetString(PyExc_RuntimeError, "failed to record minimize hint"); + return NULL; + } + + Py_RETURN_NONE; +} + static PyObject* SolveTask(PyObject *self, PyObject *args) { if (__z3_parser == nullptr) { PyErr_SetString(PyExc_RuntimeError, "parser not initialized"); @@ -380,11 +546,29 @@ static PyObject* SolveTask(PyObject *self, PyObject *args) { PyObject *sols = PyList_New(solutions.size()); for (size_t i = 0; i < solutions.size(); i++) { - PyObject *sol = PyTuple_New(3); - auto val = solutions[i]; - PyTuple_SetItem(sol, 0, PyLong_FromUnsignedLong(val.id)); - PyTuple_SetItem(sol, 1, PyLong_FromUnsignedLong(val.offset)); - PyTuple_SetItem(sol, 2, PyLong_FromUnsignedLong(val.val)); + auto &val = solutions[i]; + PyObject *sol = PyDict_New(); + + // Common fields for all operations + PyDict_SetItemString(sol, "op", PyLong_FromLong((int)val.op)); + PyDict_SetItemString(sol, "id", PyLong_FromUnsignedLong(val.id)); + PyDict_SetItemString(sol, "offset", PyLong_FromLong(val.offset)); + + // Operation-specific fields + using op_t = symsan::Z3ParserSolver::solution_op_t; + switch (val.op) { + case op_t::SET: + PyDict_SetItemString(sol, "val", PyLong_FromUnsignedLong(val.val)); + break; + case op_t::INSERT: + PyDict_SetItemString(sol, "data", + PyBytes_FromStringAndSize((char*)val.data.data(), val.data.size())); + break; + case op_t::DELETE: + PyDict_SetItemString(sol, "len", PyLong_FromUnsignedLong(val.len)); + break; + } + PyList_SetItem(sols, i, sol); } @@ -395,19 +579,52 @@ static PyObject* SolveTask(PyObject *self, PyObject *args) { return ret; } +static PyObject* ExportTaskSMT2(PyObject *self, PyObject *args) { + if (__z3_parser == nullptr) { + PyErr_SetString(PyExc_RuntimeError, "parser not initialized"); + return NULL; + } + + uint64_t id = 0; + const char *filename = NULL; + if (!PyArg_ParseTuple(args, "Ks", &id, &filename)) { + return NULL; + } + + int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); + return NULL; + } + + int ret = __z3_parser->export_task_smt2(id, fd); + close(fd); + + if (ret != 0) { + PyErr_SetString(PyExc_RuntimeError, "failed to export task as SMT2"); + return NULL; + } + + Py_RETURN_NONE; +} + static PyMethodDef SymSanMethods[] = { - {"init", SymSanInit, METH_VARARGS, "initialize symsan target"}, + {"init", (PyCFunction)SymSanInit, METH_VARARGS | METH_KEYWORDS, "initialize symsan target"}, {"config", (PyCFunction)SymSanConfig, METH_VARARGS | METH_KEYWORDS, "config symsan"}, {"run", (PyCFunction)SymSanRun, METH_VARARGS | METH_KEYWORDS, "run symsan target, optional stdin=file"}, {"read_event", SymSanReadEvent, METH_VARARGS, "read a symsan event"}, {"terminate", (PyCFunction)SymSanTerminate, METH_NOARGS, "terminate current symsan instance"}, {"destroy", (PyCFunction)SymSanDestroy, METH_NOARGS, "destroy symsan target"}, - {"reset_input", InitParser, METH_VARARGS, "reset the symbolic expression parser with a new input"}, + {"init_parser", InitParser, METH_VARARGS, "initialize parser from shared memory (name or address)"}, + {"reset_input", ResetParser, METH_VARARGS, "reset the symbolic expression parser with a new input"}, + {"update_input", UpdateInput, METH_VARARGS, "update input cache without clearing deps"}, {"parse_cond", ParseCond, METH_VARARGS, "parse trace_cond event into solving tasks"}, {"parse_gep", ParseGEP, METH_VARARGS, "parse trace_gep event into solving tasks"}, {"add_constraint", AddConstraint, METH_VARARGS, "add a constraint"}, {"record_memcmp", RecordMemcmp, METH_VARARGS, "record a memcmp event"}, + {"record_minimize", RecordMinimize, METH_VARARGS, "record a label to minimize during solving (e.g., malloc size)"}, {"solve_task", SolveTask, METH_VARARGS, "solve a task"}, + {"export_task_smt2", ExportTaskSMT2, METH_VARARGS, "export a task as SMT-LIB v2 to a file"}, {NULL, NULL, 0, NULL} /* Sentinel */ }; @@ -429,5 +646,46 @@ PyInit_symsan(void) { delete __z3_parser; symsan_destroy(); } - return PyModule_Create(&SymSanModule); + + PyObject *module = PyModule_Create(&SymSanModule); + if (module == NULL) { + return NULL; + } + + // Create OpType enum class + PyObject *enum_module = PyImport_ImportModule("enum"); + if (enum_module == NULL) { + Py_DECREF(module); + return NULL; + } + + PyObject *int_enum = PyObject_GetAttrString(enum_module, "IntEnum"); + Py_DECREF(enum_module); + if (int_enum == NULL) { + Py_DECREF(module); + return NULL; + } + + // Create OpType enum with SET=0, INSERT=1, DELETE=2 + PyObject *enum_dict = PyDict_New(); + PyDict_SetItemString(enum_dict, "SET", PyLong_FromLong(0)); + PyDict_SetItemString(enum_dict, "INSERT", PyLong_FromLong(1)); + PyDict_SetItemString(enum_dict, "DELETE", PyLong_FromLong(2)); + + PyObject *enum_args = PyTuple_Pack(2, PyUnicode_FromString("OpType"), enum_dict); + Py_DECREF(enum_dict); + + PyObject *op_type_enum = PyObject_CallObject(int_enum, enum_args); + Py_DECREF(int_enum); + Py_DECREF(enum_args); + + if (op_type_enum == NULL) { + Py_DECREF(module); + return NULL; + } + + // Add OpType to module + PyModule_AddObject(module, "OpType", op_type_enum); + + return module; } diff --git a/python/test.py b/python/test.py index 3a0023b5..28c6e1d3 100644 --- a/python/test.py +++ b/python/test.py @@ -58,8 +58,15 @@ class memcmp_msg(ctypes.Structure): symsan.record_memcmp(label, buf.content) for task in tasks: - r, sol = symsan.solve_task(task) - print(sol) + r, sols = symsan.solve_task(task) + print(f"Status: {r}") + for sol in sols: + if sol['op'] == symsan.OpType.SET: + print(f" SET id={sol['id']} offset={sol['offset']} val={sol['val']:#x}") + elif sol['op'] == symsan.OpType.INSERT: + print(f" INSERT id={sol['id']} offset={sol['offset']} data={sol['data'].hex()}") + elif sol['op'] == symsan.OpType.DELETE: + print(f" DELETE id={sol['id']} offset={sol['offset']} len={sol['len']}") status, is_killed = symsan.terminate() print(f"exit status {status}, killed? {is_killed}") diff --git a/runtime/dfsan/CMakeLists.txt b/runtime/dfsan/CMakeLists.txt index 03eec817..9cb7acf4 100644 --- a/runtime/dfsan/CMakeLists.txt +++ b/runtime/dfsan/CMakeLists.txt @@ -1,7 +1,7 @@ # include_directories(..) -# Runtime library sources and build flags. -set(DFSAN_RTL_SOURCES +# SymSan sources (symbolic execution, taint tracking) +set(SYMSAN_RTL_SOURCES dfsan.cpp dfsan_custom.cpp dfsan_interceptors.cpp @@ -9,7 +9,17 @@ set(DFSAN_RTL_SOURCES union_util.cpp union_hashtable.cpp) -set(DFSAN_RTL_HEADERS +# UCSan sources (under-constrained execution, pointer/object tracking) +set(UCSAN_RTL_SOURCES + ucsan.cpp + ucsan_custom.cpp) + +# Combined sources (both SymSan and UCSan) +set(DFSAN_RTL_SOURCES + ${SYMSAN_RTL_SOURCES} + ${UCSAN_RTL_SOURCES}) + +set(SYMSAN_RTL_HEADERS dfsan.h dfsan_flags.inc dfsan_platform.h @@ -17,6 +27,19 @@ set(DFSAN_RTL_HEADERS union_util.h union_hashtable.h) +# UCSan headers +set(UCSAN_RTL_HEADERS + ucsan.h + ucsan_platform.h + ucsan_exit_reason.h + ucsan_flags.inc) + +# Combined headers +set(DFSAN_RTL_HEADERS + ${SYMSAN_RTL_HEADERS} + ${UCSAN_RTL_HEADERS}) + +list(APPEND ${SANITIZER_COMMON_CFLAGS} "-O3 -Wno-format") set(DFSAN_COMMON_CFLAGS ${SANITIZER_COMMON_CFLAGS}) if(OS_NAME MATCHES "Linux" AND @@ -31,12 +54,46 @@ append_rtti_flag(OFF DFSAN_COMMON_CFLAGS) # Prevent clang from generating libc calls. append_list_if(COMPILER_RT_HAS_FFREESTANDING_FLAG -ffreestanding DFSAN_COMMON_CFLAGS) -# Static runtime library. +# Static runtime libraries. +# We build three variants: +# 1. symsan_rt - standalone SymSan (symbolic execution) +# 2. ucsan_rt - standalone UCSan (under-constrained execution) +# 3. dfsan_rt - combined library (both SymSan and UCSan) + +add_compiler_rt_component(symsan) +add_compiler_rt_component(ucsan) add_compiler_rt_component(dfsan) foreach(arch ${DFSAN_SUPPORTED_ARCH}) set(DFSAN_CFLAGS ${DFSAN_COMMON_CFLAGS}) append_list_if(COMPILER_RT_HAS_FPIE_FLAG -fPIE DFSAN_CFLAGS) + + # Standalone SymSan library + add_compiler_rt_runtime(symsan_rt + STATIC + ARCHS ${arch} + SOURCES ${SYMSAN_RTL_SOURCES} + $ + $ + $ + CFLAGS ${DFSAN_CFLAGS} + PARENT_TARGET symsan) + + # Standalone UCSan library + add_compiler_rt_runtime(ucsan_rt + STATIC + ARCHS ${arch} + SOURCES ${UCSAN_RTL_SOURCES} + $ + $ + $ + CFLAGS ${DFSAN_CFLAGS} + PARENT_TARGET ucsan) + + # Combined library (SymSan + UCSan) + # USE_UCSAN_CUSTOM tells dfsan_custom.cpp to skip definitions + # that are provided by ucsan_custom.cpp (e.g., __dfsw_memcpy) + set(DFSAN_COMBINED_CFLAGS ${DFSAN_CFLAGS} -DUSE_UCSAN_CUSTOM) add_compiler_rt_runtime(dfsan_rt STATIC ARCHS ${arch} @@ -46,8 +103,9 @@ foreach(arch ${DFSAN_SUPPORTED_ARCH}) $ $ ADDITIONAL_HEADERS ${DFSAN_RTL_HEADERS} - CFLAGS ${DFSAN_CFLAGS} + CFLAGS ${DFSAN_COMBINED_CFLAGS} PARENT_TARGET dfsan) + add_sanitizer_rt_symbols(dfsan_rt ARCHS ${arch} EXTRA dfsan.syms.extra) @@ -108,6 +166,21 @@ install(FILES ${dfsan_abilist_filename} install(FILES "libc++_abilist.txt" DESTINATION ${COMPILER_RT_LIBRARY_INSTALL_DIR}) +set(ucsan_abilist_filename ${dfsan_abilist_dir}/ucsan_abilist.txt) +add_custom_target(ucsan_abilist ALL + DEPENDS ${ucsan_abilist_filename}) +add_custom_command(OUTPUT ${ucsan_abilist_filename} + VERBATIM + COMMAND + ${CMAKE_COMMAND} -E make_directory ${dfsan_abilist_dir} + COMMAND + cat ${CMAKE_CURRENT_SOURCE_DIR}/ucsan_abilist.txt + > ${ucsan_abilist_filename} + DEPENDS ucsan_abilist.txt) +add_dependencies(dfsan ucsan_abilist) +install(FILES ${ucsan_abilist_filename} + DESTINATION ${COMPILER_RT_LIBRARY_INSTALL_DIR}) + set(taint_linker_script_filename ${COMPILER_RT_OUTPUT_DIR}/taint.ld) add_custom_target(taint_linker_script ALL DEPENDS ${taint_linker_script_filename}) diff --git a/runtime/dfsan/dfsan.cpp b/runtime/dfsan/dfsan.cpp index ddd3d31f..8974aa67 100644 --- a/runtime/dfsan/dfsan.cpp +++ b/runtime/dfsan/dfsan.cpp @@ -32,6 +32,7 @@ #include "taint_allocator.h" #include "union_util.h" #include "union_hashtable.h" +#include "ucsan_exit_reason.h" #include #include @@ -79,10 +80,10 @@ bool print_debug; static const int kArgTlsSize = 800; static const int kRetvalTlsSize = 800; -SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint64_t - __dfsan_retval_tls[kRetvalTlsSize / sizeof(uint64_t)]; -SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint64_t - __dfsan_arg_tls[kArgTlsSize / sizeof(uint64_t)]; +SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL dfsan_label + __dfsan_retval_tls[kRetvalTlsSize / sizeof(dfsan_label)]; +SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL dfsan_label + __dfsan_arg_tls[kArgTlsSize / sizeof(dfsan_label)]; SANITIZER_INTERFACE_ATTRIBUTE uptr __dfsan_shadow_ptr_mask; @@ -185,9 +186,54 @@ static inline bool is_valid_op(uint16_t op) { return op >= __dfsan::Add && op < __dfsan::LastOp || op == __dfsan::Not; } +static inline dfsan_label add_taint_info(dfsan_label_info *info) { + dfsan_label label = + atomic_fetch_add(&__dfsan_last_label, 1, memory_order_relaxed) + 1; + dfsan_check_label(label); + + AOUT("%u = (%u, %u, %u, %u, %lu, %lu)\n", label, info->l1, info->l2, + info->op, info->size, info->op1.i, info->op2.i); + + internal_memcpy(&__dfsan_label_info[label], info, sizeof(dfsan_label_info)); + return label; +} + +// for internal use only, skip optimization and ubsan checks +// caller must ensure op is valid, handle commutative conventions +static dfsan_label do_taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, + uint16_t size, uint64_t op1, uint64_t op2) { + // dedup + uint32_t h1 = l1 ? __dfsan_label_info[l1].hash : 0; + uint32_t h2 = l2 ? __dfsan_label_info[l2].hash : 0; + uint32_t h3 = op; + h3 = (h3 << 16) | size; + uint32_t hash = xxhash(h1, h2, h3); + + struct dfsan_label_info label_info = { + .l1 = l1, .l2 = l2, .op1 = {op1}, .op2 = {op2}, .op = op, .size = size, + .hash = hash}; + + __taint::option res = __union_table.lookup(label_info); + if (res != __taint::none()) { + dfsan_label label = *res; + AOUT("%u found\n", label); + return label; + } + + dfsan_label label = add_taint_info(&label_info); + __union_table.insert(&__dfsan_label_info[label], label); + + return label; +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_cond(dfsan_label label, bool r, uint8_t flag, uint32_t cid); +// Forward declarations for trace callbacks (implemented in solvers) +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_trace_event_addr(uint32_t label, uint32_t event_id, + uint64_t info, void* addr, uint32_t info2); + extern "C" SANITIZER_INTERFACE_ATTRIBUTE dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, uint16_t size, uint64_t op1, uint64_t op2) { @@ -212,7 +258,7 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // propagate if it's casting op if (op == __dfsan::BitCast) return l1; if (op == __dfsan::PtrToInt) {AOUT("WARNING: ptrtoint %d\n", l1); return 0;} - if ((op & 0xff) == __dfsan::ICmp) { return 0;} // ptr1 op ptr2 + if ((op & 0xff) == __dfsan::ICmp) { return 0; } // ptr1 op ptr2 if (op != __dfsan::Extract) { AOUT("WARNING: unsupported op %d over ptr1 %d ptr2 %d\n", op, l1, l2); return 0; @@ -267,14 +313,31 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, else if (op == __dfsan::LShr) return l1; // x >> 0 = x else if (op == __dfsan::AShr) return l1; // x >> 0 = x } + // Simplify PtrToInt(string_op) - base_addr to just PtrToInt (the index) + // This is the ptr2int+sub equivalent of what __taint_gep_offset does for GEP: + // ptr = base + index, so PtrToInt(ptr) - base = index, + // and PtrToInt already represents the index as a bitvector + if (op == __dfsan::Sub && l2 == 0 && l1 >= CONST_OFFSET) { + dfsan_label_info *l1_info = get_label_info(l1); + if (l1_info->op == __dfsan::PtrToInt && l1_info->l1 >= CONST_OFFSET) { + dfsan_label_info *src_info = get_label_info(l1_info->l1); + if (src_info->op >= __dfsan::fstr_op_start && + src_info->op < __dfsan::fstr_op_end && + src_info->op1.i == op2) { + AOUT("simplify ptr2int(string_op) - base: %d\n", l1); + return l1; + } + } + } if (op == __dfsan::Trunc) { if (__dfsan_label_info[l1].op == __dfsan::ZExt || __dfsan_label_info[l1].op == __dfsan::SExt) { dfsan_label base = __dfsan_label_info[l1].l1; if (size == __dfsan_label_info[base].size) return base; } - } else if (op == __dfsan::Xor && l1 == l2) { + } else if ((op == __dfsan::Xor || op == __dfsan::Sub) && l1 == l2) { // x ^ x = 0 + // x - x = 0 return 0; } @@ -313,9 +376,12 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // check for division by zero // -fsanitize=integer-divide-by-zero if (orig_op2 != 0) { - cond = __taint_union(l2, 0, (bveq << 8) | __dfsan::ICmp, size, - orig_op2, 0); + cond = do_taint_union(l2, 0, (bveq << 8) | __dfsan::ICmp, size, + orig_op2, 0); __taint_trace_cond(cond, 0, UndefinedCheck, ub_division_by_zero); + } else { + AOUT("WARNING: division by zero\n"); + __taint_trace_event_addr(l2, EVENT_DIV_BY_ZERO, 0, __builtin_return_address(0), 0); } break; case __dfsan::Shl: @@ -324,30 +390,30 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // -fsanitize=shift-exponent // check for too large value: exponent > size if (orig_op2 < size) { - cond = __taint_union(l2, 0, (bvuge << 8) | __dfsan::ICmp, - op_size, orig_op2, size), + cond = do_taint_union(l2, 0, (bvuge << 8) | __dfsan::ICmp, + op_size, orig_op2, size); __taint_trace_cond(cond, 0, UndefinedCheck, ub_shift_exponent); } if ((int64_t)orig_op2 >= 0) { // check for negative value - cond = __taint_union(l2, 0, (bvslt << 8) | __dfsan::ICmp, - op_size, orig_op2, 0), + cond = do_taint_union(l2, 0, (bvslt << 8) | __dfsan::ICmp, + op_size, orig_op2, 0); __taint_trace_cond(cond, 0, UndefinedCheck, ub_shift_exponent); } if (op == __dfsan::Shl && orig_op1 != 0 && orig_op2 <= __builtin_clzl(orig_op1) - (64 - size)) { // check for shift overflow // op2 > leading zero bits in op1 - cond = __taint_union(l2, 0, (bvugt << 8) | __dfsan::ICmp, op_size, - orig_op2, __builtin_clzl(orig_op1) - (64 - size)); + cond = do_taint_union(l2, 0, (bvugt << 8) | __dfsan::ICmp, op_size, + orig_op2, __builtin_clzl(orig_op1) - (64 - size)); __taint_trace_cond(cond, 0, UndefinedCheck, ub_shift_overflow); } if (l1 && (int64_t)orig_op1 >= 0) { // check for negative base // -fsanitize=shift-base // op1 < 0 - cond = __taint_union(l1, 0, (bvslt << 8) | __dfsan::ICmp, - get_label_info(l1)->size, orig_op1, 0); + cond = do_taint_union(l1, 0, (bvslt << 8) | __dfsan::ICmp, + get_label_info(l1)->size, orig_op1, 0); __taint_trace_cond(cond, 0, UndefinedCheck, ub_shift_base); } break; @@ -356,14 +422,7 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, } } - dfsan_label label = - atomic_fetch_add(&__dfsan_last_label, 1, memory_order_relaxed) + 1; - dfsan_check_label(label); - assert(label > l1 && label > l2); - - AOUT("%u = (%u, %u, %u, %u, %lu, %lu)\n", label, l1, l2, op, size, op1, op2); - - internal_memcpy(&__dfsan_label_info[label], &label_info, sizeof(dfsan_label_info)); + dfsan_label label = add_taint_info(&label_info); __union_table.insert(&__dfsan_label_info[label], label); if (flags().solve_ub) { @@ -373,9 +432,9 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // old_vale >= (1 << new_size) if (orig_op1 < (1UL << size)) { // if current value does not have loss - dfsan_label loss = __taint_union(l1, 0, (bvuge << 8) | __dfsan::ICmp, - get_label_info(l1)->size, orig_op1, - 1UL << size); + dfsan_label loss = do_taint_union(l1, 0, (bvuge << 8) | __dfsan::ICmp, + get_label_info(l1)->size, orig_op1, + 1UL << size); __taint_trace_cond(loss, 0, UndefinedCheck, ub_unsigned_integer_truncation); } // -fsanitize=implicit-signed-integer-truncation @@ -384,8 +443,8 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, if ((int64_t)orig_op1 >= target) { uint16_t old_size = get_label_info(l1)->size; if (old_size < 64) target &= ~(1UL << old_size); - dfsan_label loss = __taint_union(l1, 0, (bvslt << 8) | __dfsan::ICmp, - old_size, orig_op1, target); + dfsan_label loss = do_taint_union(l1, 0, (bvslt << 8) | __dfsan::ICmp, + old_size, orig_op1, target); __taint_trace_cond(loss, 0, UndefinedCheck, ub_signed_integer_truncation); } @@ -402,12 +461,12 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // Currently no sign change, check if it can happen // Sign changes when: sign_bit(l1) != sign_bit(label) // We check: (l1 < 0) XOR (label < 0) - dfsan_label src_neg = __taint_union(l1, 0, (bvslt << 8) | __dfsan::ICmp, - src_size, orig_op1, 0); - dfsan_label dst_neg = __taint_union(label, 0, (bvslt << 8) | __dfsan::ICmp, - size, orig_op1 & new_mask, 0); - dfsan_label sign_diff = __taint_union(src_neg, dst_neg, __dfsan::Xor, 1, - src_sign ? 1 : 0, dst_sign ? 1 : 0); + dfsan_label src_neg = do_taint_union(l1, 0, (bvslt << 8) | __dfsan::ICmp, + src_size, orig_op1, 0); + dfsan_label dst_neg = do_taint_union(label, 0, (bvslt << 8) | __dfsan::ICmp, + size, orig_op1 & new_mask, 0); + dfsan_label sign_diff = do_taint_union(src_neg, dst_neg, __dfsan::Xor, 1, + src_sign ? 1 : 0, dst_sign ? 1 : 0); __taint_trace_cond(sign_diff, 0, UndefinedCheck, ub_integer_sign_change); } } @@ -430,19 +489,22 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, if (!has_signed_overflow) { // Build symbolic expression: ((l1 ^ label) & (l2 ^ label)) < 0 - dfsan_label xor_l1 = __taint_union(l1, label, __dfsan::Xor, size, orig_op1, result); - dfsan_label xor_l2 = __taint_union(l2, label, __dfsan::Xor, size, orig_op2, result); - dfsan_label and_xors = __taint_union(xor_l1, xor_l2, __dfsan::And, size, xor1, xor2); - dfsan_label cond = __taint_union(and_xors, 0, (bvslt << 8) | __dfsan::ICmp, - size, overflow_check, 0); + dfsan_label xor_l1 = do_taint_union(l1, label, __dfsan::Xor, size, orig_op1, result); + dfsan_label xor_l2 = do_taint_union(l2, label, __dfsan::Xor, size, orig_op2, result); + dfsan_label and_xors = do_taint_union(xor_l1, xor_l2, __dfsan::And, size, xor1, xor2); + dfsan_label cond = do_taint_union(and_xors, 0, (bvslt << 8) | __dfsan::ICmp, + size, overflow_check, 0); __taint_trace_cond(cond, 0, UndefinedCheck, ub_integer_overflow); + } else { + AOUT("WARNING: signed integer overflow\n"); + __taint_trace_event_addr(label, EVENT_INT_OVERFLOW, 0, __builtin_return_address(0), 0); } // Unsigned overflow: result < op1 (for any non-zero op2) // When adding two unsigned numbers, overflow means result wrapped around - if (result >= orig_op1 && orig_op2 != 0) { - dfsan_label cond = __taint_union(label, l1, (bvult << 8) | __dfsan::ICmp, - size, result, orig_op1); + if (result >= orig_op1 && (orig_op2 != 0 || l2 != 0)) { + dfsan_label cond = do_taint_union(label, l1, (bvult << 8) | __dfsan::ICmp, + size, result, orig_op1); __taint_trace_cond(cond, 0, UndefinedCheck, ub_integer_overflow); } } else if (op == __dfsan::Mul) { @@ -464,24 +526,25 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // This is an approximation - full check would need wider multiplication bool has_signed_overflow = (overflow_check & sign_bit) != 0; - if (!has_signed_overflow && orig_op1 != 0 && orig_op2 != 0) { - dfsan_label xor_l1 = __taint_union(l1, label, __dfsan::Xor, size, orig_op1, result); - dfsan_label xor_l2 = __taint_union(l2, label, __dfsan::Xor, size, orig_op2, result); - dfsan_label and_xors = __taint_union(xor_l1, xor_l2, __dfsan::And, size, xor1, xor2); - dfsan_label cond = __taint_union(and_xors, 0, (bvslt << 8) | __dfsan::ICmp, - size, overflow_check, 0); + if (!has_signed_overflow && (orig_op1 != 0 || l1 != 0) && (orig_op2 != 0 || l2 != 0)) { + dfsan_label xor_l1 = do_taint_union(l1, label, __dfsan::Xor, size, orig_op1, result); + dfsan_label xor_l2 = do_taint_union(l2, label, __dfsan::Xor, size, orig_op2, result); + dfsan_label and_xors = do_taint_union(xor_l1, xor_l2, __dfsan::And, size, xor1, xor2); + dfsan_label cond = do_taint_union(and_xors, 0, (bvslt << 8) | __dfsan::ICmp, + size, overflow_check, 0); __taint_trace_cond(cond, 0, UndefinedCheck, ub_integer_overflow); + } else { + AOUT("WARNING: signed integer overflow\n"); + __taint_trace_event_addr(label, EVENT_INT_OVERFLOW, 0, __builtin_return_address(0), 0); } // Unsigned overflow: for multiplication, check if result / op1 != op2 (when op1 != 0) - if (orig_op1 != 0 && result / orig_op1 == orig_op2) { - // No overflow currently, check if overflow can happen - // Approximate: result < op1 || result < op2 when both > 1 - if (orig_op1 > 1 && orig_op2 > 1) { - dfsan_label cond = __taint_union(label, l1, (bvult << 8) | __dfsan::ICmp, - size, result, orig_op1); + // When orig_op1 == 0, no overflow possible concretely (0 * x = 0), but if symbolic, still check + bool no_unsigned_overflow = (orig_op1 == 0 || result / orig_op1 == orig_op2); + if (no_unsigned_overflow && (orig_op1 > 1 || l1 != 0) && (orig_op2 > 1 || l2 != 0)) { + dfsan_label cond = do_taint_union(label, l1, (bvult << 8) | __dfsan::ICmp, + size, result, orig_op1); __taint_trace_cond(cond, 0, UndefinedCheck, ub_integer_overflow); - } } } else if (op == __dfsan::Sub) { // check for integer overflow (underflow for subtraction) @@ -503,19 +566,22 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, if (!has_signed_overflow) { // Build symbolic expression: ((l1 ^ l2) & (l1 ^ label)) < 0 - dfsan_label xor_l1l2 = __taint_union(l1, l2, __dfsan::Xor, size, orig_op1, orig_op2); - dfsan_label xor_l1r = __taint_union(l1, label, __dfsan::Xor, size, orig_op1, result); - dfsan_label and_xors = __taint_union(xor_l1l2, xor_l1r, __dfsan::And, size, xor_ab, xor_ar); - dfsan_label cond = __taint_union(and_xors, 0, (bvslt << 8) | __dfsan::ICmp, - size, overflow_check, 0); + dfsan_label xor_l1l2 = do_taint_union(l1, l2, __dfsan::Xor, size, orig_op1, orig_op2); + dfsan_label xor_l1r = do_taint_union(l1, label, __dfsan::Xor, size, orig_op1, result); + dfsan_label and_xors = do_taint_union(xor_l1l2, xor_l1r, __dfsan::And, size, xor_ab, xor_ar); + dfsan_label cond = do_taint_union(and_xors, 0, (bvslt << 8) | __dfsan::ICmp, + size, overflow_check, 0); __taint_trace_cond(cond, 0, UndefinedCheck, ub_integer_overflow); + } else { + AOUT("WARNING: signed integer overflow\n"); + __taint_trace_event_addr(label, EVENT_INT_OVERFLOW, 0, __builtin_return_address(0), 0); } // Unsigned underflow: result > op1 when op2 > 0 // When subtracting, if a < b, result wraps around to large value (result > a) if (result <= orig_op1 && orig_op2 != 0) { - dfsan_label cond = __taint_union(label, l1, (bvugt << 8) | __dfsan::ICmp, - size, result, orig_op1); + dfsan_label cond = do_taint_union(label, l1, (bvugt << 8) | __dfsan::ICmp, + size, result, orig_op1); __taint_trace_cond(cond, 0, UndefinedCheck, ub_integer_overflow); } } @@ -523,8 +589,56 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, return label; } +// If label is zero or kInitializingLabel, return it directly +// If label is bounds (Alloca), return it directly +// If label is a string op, create a new fstr_off label +// If label is other symbolic label, create an Add label with offset +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +dfsan_label __taint_gep_offset(dfsan_label label, char* result, char* base) { + // check label + if (label == 0 || label == kInitializingLabel) + return label; + + dfsan_label_info *info = get_label_info(label); + + // concrete ptrs, op == Alloca, just propagate bounds info + if (info->op == __dfsan::Alloca) { + return label; + } + + // symbolic ptrs, record the offset + int64_t offset = result - base; + + // Check if base_label is or derives from a string op + dfsan_label str_op_label = taint_find_string_op_source(label); + if (str_op_label != 0) { + // string op - create fstr_off label: l1=str_op_label, op1=offset + // This represents the content at (string_op_position + offset) + dfsan_label off_label = do_taint_union(str_op_label, 0, __dfsan::fstr_off, + sizeof(void*) * 8, + 0, (uint64_t)offset); + AOUT("str: label=%u, str_op=%u, offset=%ld, result=%u\n", + label, str_op_label, offset, off_label); + + // record the label (fstr_off is an indexOf-type op) + taint_set_str_indexof_label(result, off_label); + + return off_label; + } else if (offset == 0) { + // no offset, return original label + return label; + } + + if (info->size != sizeof(base) * 8) { + AOUT("WARNING: unexpected size %u for label %u\n", info->size, label); + return label; + } + + return do_taint_union(0, label, __dfsan::Add, info->size, (uint64_t)offset, 0); +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE -dfsan_label __taint_union_load(const dfsan_label *ls, uptr n, uint64_t align) { +dfsan_label __taint_union_load(const dfsan_label *ls, uptr n, uint64_t size_in_bits, uint64_t align) { if ((uptr)ls < 4096) { AOUT("WARNING: nullptr deref\n"); return 0; @@ -570,10 +684,16 @@ dfsan_label __taint_union_load(const dfsan_label *ls, uptr n, uint64_t align) { } } if (shape) { - if (n == 1) return label0; - - AOUT("shape: label0: %d %lu\n", label0, n); - return __taint_union(label0, (dfsan_label)n, Load, n * 8, 0, 0); + dfsan_label result; + if (n == 1) + result = label0; + else { + AOUT("shape: label0: %d %lu\n", label0, n); + result = do_taint_union(label0, (dfsan_label)n, Load, n * 8, 0, 0); + } + if (size_in_bits < n * 8) + result = do_taint_union(result, CONST_LABEL, Trunc, size_in_bits, 0, 0); + return result; } // fast path 2: all labels are extracted from a n-size label, @@ -592,6 +712,8 @@ dfsan_label __taint_union_load(const dfsan_label *ls, uptr n, uint64_t align) { } if (get_label_info(parent)->size == offset && offset == n * 8) { AOUT("Fast path (2): all labels are extracts: %u\n", parent); + if (size_in_bits < n * 8) + return do_taint_union(parent, CONST_LABEL, Trunc, size_in_bits, 0, 0); return parent; } } @@ -607,21 +729,26 @@ dfsan_label __taint_union_load(const dfsan_label *ls, uptr n, uint64_t align) { if (!is_constant_label(next_label)) { if (next_size <= (n - i) * 8) { i += next_size / 8; - label = __taint_union(label, next_label, Concat, i * 8, 0, 0); + label = do_taint_union(label, next_label, Concat, i * 8, 0, 0); } else { Report("WARNING: partial loading expected=%lu has=%d\n", n-i, next_size); uptr size = n - i; - dfsan_label trunc = __taint_union(next_label, CONST_LABEL, Trunc, size * 8, 0, 0); - return __taint_union(label, trunc, Concat, n * 8, 0, 0); + dfsan_label trunc = do_taint_union(next_label, CONST_LABEL, Trunc, size * 8, 0, 0); + dfsan_label result = do_taint_union(label, trunc, Concat, n * 8, 0, 0); + if (size_in_bits < n * 8) + result = do_taint_union(result, CONST_LABEL, Trunc, size_in_bits, 0, 0); + return result; } } else { Report("WARNING: taint mixed with concrete %lu\n", i); char *c = (char *)app_for(&ls[i]); ++i; - label = __taint_union(label, 0, Concat, i * 8, 0, *c); + label = do_taint_union(label, 0, Concat, i * 8, 0, *c); } } AOUT("\n"); + if (size_in_bits < n * 8) + label = do_taint_union(label, CONST_LABEL, Trunc, size_in_bits, 0, 0); return label; } @@ -676,16 +803,20 @@ void __taint_union_store(dfsan_label l, dfsan_label *ls, uptr n, uint64_t align) // default fall through for (uptr i = 0; i < n; ++i) { - ls[i] = __taint_union(l, CONST_LABEL, Extract, 8, 0, i * 8); + ls[i] = do_taint_union(l, CONST_LABEL, Extract, 8, 0, i * 8); } } +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_loop_push_stack(); +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_loop_pop_stack(); + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_push_stack_frame() { if (flags().trace_bounds) { if (__current_saved_stack_index < MAX_SAVED_STACK_ENTRIES) __saved_alloca_stack_top[++__current_saved_stack_index] = __alloca_stack_top; } + __taint_trace_loop_push_stack(); } extern "C" SANITIZER_INTERFACE_ATTRIBUTE @@ -693,6 +824,7 @@ void __taint_pop_stack_frame() { if (flags().trace_bounds) { __alloca_stack_top = __saved_alloca_stack_top[__current_saved_stack_index--]; } + __taint_trace_loop_pop_stack(); } extern "C" SANITIZER_INTERFACE_ATTRIBUTE @@ -838,15 +970,15 @@ void __taint_solve_bounds(dfsan_label ptr_label, uint64_t ptr, // array with known size // // check underflow, index < 0 - dfsan_label lb = __taint_union(index_label, 0, (bvslt << 8) | ICmp, - index_bits, index, 0); + dfsan_label lb = do_taint_union(index_label, 0, (bvslt << 8) | ICmp, + index_bits, index, 0); // assume the result is false, as bounds check should happen before solving // no flag, no nested __taint_trace_cond(lb, 0, UndefinedCheck, ub_index_underflow); // check overflow, index >= num_elems - dfsan_label ub = __taint_union(index_label, 0, (bvsge << 8) | ICmp, - index_bits, index, num_elems); + dfsan_label ub = do_taint_union(index_label, 0, (bvsge << 8) | ICmp, + index_bits, index, num_elems); __taint_trace_cond(ub, 0, UndefinedCheck, ub_index_overflow); } else { // array with unknown size @@ -854,46 +986,46 @@ void __taint_solve_bounds(dfsan_label ptr_label, uint64_t ptr, if (bounds_info->op == __dfsan::Alloca) { // bounds information is available, check if allocation size is symbolic if (index_bits < 64) // extends index to 64 bits - index_label = __taint_union(index_label, 0, ZExt, 64, index, 0); + index_label = do_taint_union(index_label, 0, ZExt, 64, index, 0); if (bounds_info->l2 == 0) { // concrete allocation size, check bounds // check underflow, index * elem_size + current_offset + ptr < lower_bound // => index < (lower_bound - current_offset - ptr) / elem_size uint64_t lower_bound = (bounds_info->op1.i - current_offset - ptr) / elem_size; - dfsan_label lb = __taint_union(index_label, 0, (bvult << 8) | ICmp, - 64, index, lower_bound); + dfsan_label lb = do_taint_union(index_label, 0, (bvult << 8) | ICmp, + 64, index, lower_bound); __taint_trace_cond(lb, 0, UndefinedCheck, ub_index_underflow); // check overflow, (index + 1) * elem_size + current_offset + ptr > upper_bound // => index > (upper_bound - current_offset - ptr) / elem_size - 1 uint64_t upper_bound = (bounds_info->op2.i - current_offset - ptr) / elem_size - 1; - dfsan_label ub = __taint_union(index_label, 0, (bvugt << 8) | ICmp, - 64, index, upper_bound); + dfsan_label ub = do_taint_union(index_label, 0, (bvugt << 8) | ICmp, + 64, index, upper_bound); __taint_trace_cond(ub, 0, UndefinedCheck, ub_index_overflow); } else { // index * elem_size + current_offset + (ptr - lower_bound) > array_size * alloc_elem_size dfsan_label size_label = elem_size == 1 ? index_label : - __taint_union(index_label, 0, Mul, 64, index, elem_size); + do_taint_union(index_label, 0, Mul, 64, index, elem_size); uint64_t size = index * elem_size; uint64_t offset = current_offset + ptr - bounds_info->op1.i; size_label = offset == 0 ? size : - __taint_union(size_label, 0, Add, 64, size, offset); + do_taint_union(size_label, 0, Add, 64, size, offset); size += offset; uint64_t alloc_size = bounds_info->op2.i - bounds_info->op1.i; dfsan_label overflow = - __taint_union(size_label, bounds_info->l2, (bvugt << 8) | ICmp, - 64, size, alloc_size); + do_taint_union(size_label, bounds_info->l2, (bvugt << 8) | ICmp, + 64, size, alloc_size); __taint_trace_cond(overflow, 0, UndefinedCheck, ub_integer_to_buffer_overflow); } } else { // symbolic pointer but no bounds info? AOUT("WARNING: symbolic pointer %p = %u with no bounds info @%p\n", (void*)ptr, ptr_label, addr); - // check if null is possible? - dfsan_label null = __taint_union(ptr_label, 0, bveq, 64, ptr, 0); - __taint_trace_cond(null, 0, UndefinedCheck, ub_null_pointer); + // FIXME: check if null is possible? + // dfsan_label null = do_taint_union(ptr_label, 0, bveq, 64, ptr, 0); + // __taint_trace_cond(null, 0, UndefinedCheck, ub_null_pointer); } } } @@ -902,7 +1034,7 @@ extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_solve_size(dfsan_label ptr_label, uint64_t ptr, dfsan_label size_label, uint64_t size, uint32_t cid) { - if (size_label == 0 || !flags().solve_ub) + if (!flags().solve_ub) return; void *addr = __builtin_return_address(0); @@ -926,7 +1058,10 @@ void __taint_solve_size(dfsan_label ptr_label, uint64_t ptr, size, size_label, (void*)ptr, ptr_label); // construct size solving tasks here - uint16_t size_bits = get_label_info(size_label)->size; + uint16_t size_bits = 64; // Default to 64 bits + if (size_label != 0) { + size_bits = get_label_info(size_label)->size; + } // check overflow with buffer bounds if ptr has bounds info if (ptr_label != 0) { @@ -934,48 +1069,102 @@ void __taint_solve_size(dfsan_label ptr_label, uint64_t ptr, if (bounds_info->op == __dfsan::Alloca) { // bounds information is available if (size_bits < 64) // extend size to 64 bits - size_label = __taint_union(size_label, 0, ZExt, 64, size, 0); + size_label = do_taint_union(size_label, 0, ZExt, 64, size, 0); if (bounds_info->l2 == 0) { // concrete allocation size + if (size_label == 0) { + // concrete size, concrete allocation size, nothing to solve + return; + } // check underflow: ptr + size < lower_bound (wrap around) // => size < lower_bound - ptr (when lower_bound > ptr, but this shouldn't happen in valid code) // or equivalently, check that ptr < lower_bound (shouldn't happen) uint64_t min_size = bounds_info->op1.i - ptr; - dfsan_label underflow = __taint_union(size_label, 0, (bvult << 8) | ICmp, - 64, size, min_size); + dfsan_label underflow = do_taint_union(size_label, 0, (bvult << 8) | ICmp, + 64, size, min_size); __taint_trace_cond(underflow, 0, UndefinedCheck, ub_size_underflow); // check overflow: ptr + size > upper_bound // => size > upper_bound - ptr uint64_t max_size = bounds_info->op2.i - ptr; - dfsan_label overflow = __taint_union(size_label, 0, (bvugt << 8) | ICmp, - 64, size, max_size); + dfsan_label overflow = do_taint_union(size_label, 0, (bvugt << 8) | ICmp, + 64, size, max_size); __taint_trace_cond(overflow, 0, UndefinedCheck, ub_size_overflow); } else { // symbolic allocation size // check: size > alloc_size uint64_t offset = ptr - bounds_info->op1.i; uint64_t alloc_size = bounds_info->op2.i - bounds_info->op1.i; - dfsan_label adjusted_size = offset == 0 ? size_label : - __taint_union(size_label, 0, Add, 64, size, offset); + dfsan_label adjusted_size = (offset == 0 || size_label == 0) ? size_label : + do_taint_union(size_label, 0, Add, 64, size, offset); uint64_t actual_size = size + offset; - dfsan_label overflow = __taint_union(adjusted_size, bounds_info->l2, - (bvugt << 8) | ICmp, 64, - actual_size, alloc_size); + dfsan_label overflow = do_taint_union(adjusted_size, bounds_info->l2, + (bvugt << 8) | ICmp, 64, + actual_size, alloc_size); __taint_trace_cond(overflow, 0, UndefinedCheck, ub_size_to_buffer_overflow); } } else if (ptr_label != 0) { // symbolic pointer but no bounds info AOUT("WARNING: symbolic pointer %p = %u with no bounds info @%p\n", (void*)ptr, ptr_label, addr); - // check if null is possible - dfsan_label null = __taint_union(ptr_label, 0, bveq, 64, ptr, 0); - __taint_trace_cond(null, 0, UndefinedCheck, ub_null_pointer); + // FIXME: check if null is possible + // dfsan_label null = do_taint_union(ptr_label, 0, bveq, 64, ptr, 0); + // __taint_trace_cond(null, 0, UndefinedCheck, ub_null_pointer); } } } +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_solve_str_bounds(const char *str_ptr, + dfsan_label buf_label, uint64_t buf_ptr, + uint64_t step) { + if (!flags().solve_ub) + return; + + size_t len = strlen(str_ptr); + AOUT("solve_str_bounds: str_ptr=%p, strlen=%zu, buf_label=%u, buf_ptr=%p, step=%lu\n", + str_ptr, len, buf_label, (void*)buf_ptr, step); + bool tainted_zero = false; + if (len == 0) { + const dfsan_label *sp = shadow_for(str_ptr); + AOUT("solve_str_bounds: strlen==0, shadow[0]=%u\n", sp[0]); + if (sp[0] == 0) + return; + len = 1; + while (sp[len] != 0) + len++; + tainted_zero = true; + AOUT("solve_str_bounds: tainted extent=%zu\n", len); + } + + dfsan_label str_label = dfsan_read_label(str_ptr, len + 1); + AOUT("solve_str_bounds: str_label=%u\n", str_label); + if (str_label == 0) + return; + + dfsan_label null_label = dfsan_read_label(str_ptr + len, 1); + bool null_from_input = (null_label != 0 || tainted_zero); + + dfsan_label strlen_label = do_taint_union(0, str_label, fstrlen, + 64, null_from_input ? 1 : 0, len); + + uint64_t total = len; + if (step > 1) { + strlen_label = do_taint_union(strlen_label, 0, Mul, 64, len, step); + total = len * step; + } + + AOUT("solve str bounds: strlen=%zu, step=%lu, total=%lu, buf=%p, buf_label=%u\n", + len, step, total, (void*)buf_ptr, buf_label); + + // Concrete OOB detection (same as __taint_check_bounds) + __taint_check_bounds(buf_label, buf_ptr, 0, total); + + // Symbolic solving + __taint_solve_size(buf_label, buf_ptr, strlen_label, total, 0); +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void dfsan_store_label(dfsan_label l, void *addr, uptr size) { if (l == 0) return; @@ -1016,16 +1205,18 @@ dfsan_union(dfsan_label l1, dfsan_label l2, uint16_t op, uint16_t size, } extern "C" SANITIZER_INTERFACE_ATTRIBUTE -dfsan_label dfsan_create_label(off_t offset) { +dfsan_label dfsan_create_label(uint64_t input_id, uint64_t offset, uint32_t size_in_bytes) { dfsan_label label = atomic_fetch_add(&__dfsan_last_label, 1, memory_order_relaxed) + 1; dfsan_check_label(label); + AOUT("creating label %u: input %lu, offset %lu, size %u\n", + label, input_id, offset, size_in_bytes); internal_memset(&__dfsan_label_info[label], 0, sizeof(dfsan_label_info)); - __dfsan_label_info[label].size = 8; - // label may not equal to offset when using stdin + __dfsan_label_info[label].size = 8 * size_in_bytes; __dfsan_label_info[label].op1.i = offset; + __dfsan_label_info[label].op2.i = input_id; // init a non-zero hash - __dfsan_label_info[label].hash = xxhash(offset, 0, 8); + __dfsan_label_info[label].hash = xxhash(offset, input_id, 8); return label; } @@ -1056,8 +1247,7 @@ void dfsan_set_label(dfsan_label label, void *addr, uptr size) { SANITIZER_INTERFACE_ATTRIBUTE void dfsan_add_label(dfsan_label label, uint8_t op, void *addr, uptr size) { - for (dfsan_label *labelp = shadow_for(addr); size != 0; --size, ++labelp) - *labelp = __taint_union(*labelp, label, op, 1, 0, 0); + return; // not used, do nothing } // Unlike the other dfsan interface functions the behavior of this function @@ -1074,7 +1264,7 @@ SANITIZER_INTERFACE_ATTRIBUTE dfsan_label dfsan_read_label(const void *addr, uptr size) { if (size == 0) return 0; - return __taint_union_load(shadow_for(addr), size, sizeof(dfsan_label)); + return __taint_union_load(shadow_for(addr), size, size * 8, sizeof(dfsan_label)); } SANITIZER_INTERFACE_ATTRIBUTE dfsan_label @@ -1330,7 +1520,7 @@ static void InitializeTaintFile() { if (tainted.fd != -1 && !tainted.is_stdin) { for (off_t i = 0; i < tainted.size; i++) { - dfsan_label label = dfsan_create_label(i); + dfsan_label label = dfsan_create_label(0, i, 1); dfsan_check_label(label); } } @@ -1407,9 +1597,9 @@ static inline uptr hash_addr(uptr addr, uptr capacity) { // Grow content map when load factor exceeds 0.7 static void grow_content_map() { uptr new_capacity = content_map_capacity * 2; - typeof(__taint_content_map) new_map = (typeof(__taint_content_map))InternalAlloc( - new_capacity * sizeof(*__taint_content_map)); - internal_memset(new_map, 0, new_capacity * sizeof(*__taint_content_map)); + uptr new_size = RoundUpTo(new_capacity * sizeof(*__taint_content_map), GetPageSizeCached()); + typeof(__taint_content_map) new_map = (typeof(__taint_content_map))MmapOrDie( + new_size, "taint_content_map"); // Rehash existing entries for (uptr i = 0; i < content_map_capacity; i++) { @@ -1422,7 +1612,8 @@ static void grow_content_map() { } } - InternalFree(__taint_content_map); + uptr old_size = RoundUpTo(content_map_capacity * sizeof(*__taint_content_map), GetPageSizeCached()); + UnmapOrDie(__taint_content_map, old_size); __taint_content_map = new_map; content_map_capacity = new_capacity; } @@ -1430,9 +1621,9 @@ static void grow_content_map() { // Grow indexOf map static void grow_indexof_map() { uptr new_capacity = indexof_map_capacity * 2; - typeof(__taint_indexof_map) new_map = (typeof(__taint_indexof_map))InternalAlloc( - new_capacity * sizeof(*__taint_indexof_map)); - internal_memset(new_map, 0, new_capacity * sizeof(*__taint_indexof_map)); + uptr new_size = RoundUpTo(new_capacity * sizeof(*__taint_indexof_map), GetPageSizeCached()); + typeof(__taint_indexof_map) new_map = (typeof(__taint_indexof_map))MmapOrDie( + new_size, "taint_indexof_map"); for (uptr i = 0; i < indexof_map_capacity; i++) { if (__taint_indexof_map[i].addr != 0) { @@ -1444,7 +1635,8 @@ static void grow_indexof_map() { } } - InternalFree(__taint_indexof_map); + uptr old_size = RoundUpTo(indexof_map_capacity * sizeof(*__taint_indexof_map), GetPageSizeCached()); + UnmapOrDie(__taint_indexof_map, old_size); __taint_indexof_map = new_map; indexof_map_capacity = new_capacity; } @@ -1465,18 +1657,16 @@ static void InitializeStringMaps() { // Content map content_map_capacity = capacity; - __taint_content_map = (typeof(__taint_content_map))InternalAlloc( - content_map_capacity * sizeof(*__taint_content_map)); - internal_memset(__taint_content_map, 0, - content_map_capacity * sizeof(*__taint_content_map)); + __taint_content_map = (typeof(__taint_content_map))MmapOrDie( + RoundUpTo(content_map_capacity * sizeof(*__taint_content_map), GetPageSizeCached()), + "taint_content_map"); content_map_count = 0; // IndexOf map indexof_map_capacity = capacity; - __taint_indexof_map = (typeof(__taint_indexof_map))InternalAlloc( - indexof_map_capacity * sizeof(*__taint_indexof_map)); - internal_memset(__taint_indexof_map, 0, - indexof_map_capacity * sizeof(*__taint_indexof_map)); + __taint_indexof_map = (typeof(__taint_indexof_map))MmapOrDie( + RoundUpTo(indexof_map_capacity * sizeof(*__taint_indexof_map), GetPageSizeCached()), + "taint_indexof_map"); indexof_map_count = 0; } @@ -1563,8 +1753,68 @@ extern "C" dfsan_label taint_get_str_indexof_label(const void *addr) { return 0; } +// Helper: Find if a label derives from a string op (fstrchr, fstrrchr, fstrstr) +// by walking through PtrToInt, Sub, Add operations. +// Returns the string op label if found, 0 otherwise. +extern "C" dfsan_label taint_find_string_op_source(dfsan_label label) { + if (label < CONST_OFFSET) return 0; + + dfsan_label_info *info = dfsan_get_label_info(label); + uint16_t op = info->op; + + // Check if this is directly a string op + if (is_string_op(op)) { + return label; + } + + // Follow through PtrToInt, Sub, Add to find the source string op + if (op == __dfsan::PtrToInt || op == __dfsan::Sub || op == __dfsan::Add) { + // Recursively check l1 (the primary operand) + if (info->l1 >= CONST_OFFSET) { + dfsan_label result = taint_find_string_op_source(info->l1); + if (result != 0) return result; + } + // For Sub/Add, also check l2 + if ((op == __dfsan::Sub || op == __dfsan::Add) && info->l2 >= CONST_OFFSET) { + dfsan_label result = taint_find_string_op_source(info->l2); + if (result != 0) return result; + } + } + + return 0; +} + +// Helper: Find the first (base) input byte label from a content label. +// Walks through Concat chains and Load operations to find the starting input. +// Returns the base label, or 0 if not found. +extern "C" dfsan_label taint_get_base_input_label(dfsan_label label) { + if (label < CONST_OFFSET) return 0; + + dfsan_label_info *info = dfsan_get_label_info(label); + + // Base input label has op == 0 + if (info->op == 0) return label; + + // For Concat (op 72), walk left (l1) to find the base + if (info->op == __dfsan::Concat) { + return taint_get_base_input_label(info->l1); + } + + // For Load (op 32), l1 is the starting label + if (info->op == __dfsan::Load) { + return info->l1; + } + + // For other ops, try l1 + if (info->l1 >= CONST_OFFSET) { + return taint_get_base_input_label(info->l1); + } + + return 0; +} + // information is passed implicitly through flags() -extern "C" void InitializeSolver(); +extern "C" void InitializeSymSanSolver(); static void InitializeFlags() { SetCommonFlagsDefaults(); @@ -1612,12 +1862,18 @@ static void dfsan_fini() { if (tainted.buf) { UnmapOrDie(tainted.buf, tainted.buf_size); } - if (flags().shm_fd != -1) { + if (flags().shm_fd != -1 || internal_strcmp(flags().shm_name, "") != 0) { internal_munmap((void *)UnionTableAddr(), uniontable_size); } } +static bool dfsan_initialized; + static void dfsan_init(int argc, char **argv, char **envp) { + if (dfsan_initialized) + return; + dfsan_initialized = true; + InitializeFlags(); print_debug = flags().debug; @@ -1632,10 +1888,27 @@ static void dfsan_init(int argc, char **argv, char **envp) { // init union table __dfsan_label_info = (dfsan_label_info *)UnionTableAddr(); -if (flags().shm_fd != -1) { + if (flags().shm_size != 0) { + if (flags().shm_size > minimum_uniontable_size) { + uniontable_size = flags().shm_size; + } else { + Report("Warning: shm_size %zu is smaller than minimum %zu\n", + flags().shm_size, minimum_uniontable_size); + // use the default size + } + } + if (flags().shm_fd != -1) { AOUT("shm_fd %d\n", flags().shm_fd); ret = internal_mmap((void*)UnionTableAddr(), uniontable_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, flags().shm_fd, 0); + } else if (internal_strcmp(flags().shm_name, "") != 0) { + int shm = shm_open(flags().shm_name, O_RDWR, S_IRUSR | S_IWUSR); + if (shm == -1) { + Printf("FATAL: error creating shared union table\n"); + Die(); + } + ret = internal_mmap((void *)UnionTableAddr(), uniontable_size, + PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, shm, 0); } else { ret = MmapFixedSuperNoReserve(UnionTableAddr(), uniontable_size); } @@ -1672,7 +1945,7 @@ if (flags().shm_fd != -1) { InitializeStringMaps(); - InitializeSolver(); + InitializeSymSanSolver(); // Register the fini callback to run when the program terminates successfully // or it is killed by the runtime. @@ -1686,7 +1959,14 @@ static void (*dfsan_init_ptr)(int, char **, char **) = dfsan_init; #endif extern "C" { -SANITIZER_INTERFACE_WEAK_DEF(void, InitializeSolver, void) {} + +// Called by ucsan_init_internal to ensure dfsan is initialized before the fork server +SANITIZER_INTERFACE_ATTRIBUTE +void __dfsan_ensure_init(int argc, char **argv, char **envp) { + dfsan_init(argc, argv, envp); +} + +SANITIZER_INTERFACE_WEAK_DEF(void, InitializeSymSanSolver, void) {} // Default empty implementations (weak) for hooks SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_cmp, dfsan_label, dfsan_label, @@ -1694,6 +1974,8 @@ SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_cmp, dfsan_label, dfsan_label, SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_cond, dfsan_label, bool, uint8_t, uint32_t) {} SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_loop, uint32_t, uint32_t) {} +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_loop_push_stack, void) {} +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_loop_pop_stack, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_switch_end, uint32_t) {} SANITIZER_INTERFACE_WEAK_DEF(dfsan_label, __taint_trace_select, dfsan_label, dfsan_label, dfsan_label, uint8_t, uint8_t, uint8_t, @@ -1706,5 +1988,133 @@ SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_offset, dfsan_label, int64_t, unsigned) {} SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_memcmp, dfsan_label) {} SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_distance, uint64_t, uint64_t) {} +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_add_constraint, dfsan_label, uint8_t) {} +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_minimize_label, dfsan_label, uint64_t, dfsan_label) {} SANITIZER_WEAK_ATTRIBUTE THREADLOCAL uint32_t __taint_trace_callstack; } // extern "C" + +//===----------------------------------------------------------------------===// +// SymSan Bridge - Strong Implementations +//===----------------------------------------------------------------------===// +// These strong definitions override the weak stubs in ucsan.cpp when linked. +// They enable UCSan to propagate symbolic state to SymSan. + +extern "C" { + +// Create a SymSan label for input bytes +// @param input_id: input source identifier (fd, socket, ucsan object, etc.) +// @param offset: byte offset within the source +// @param size_in_bytes: size of the input in bytes +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +dfsan_label __taint_create_label(uint32_t input_id, uint64_t offset, uint32_t size_in_bytes) { + return dfsan_create_label(input_id, offset, size_in_bytes); +} + +// Set SymSan arg TLS entry +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +void __taint_set_arg_tls(uint32_t index, dfsan_label label, uint32_t size_in_bits) { + if (index < kArgTlsSize / sizeof(dfsan_label)) { + // Truncate if size_in_bits is not byte-aligned + uint32_t size_in_bytes = (size_in_bits + 7) / 8; + if (size_in_bits < size_in_bytes * 8) { + label = do_taint_union(label, CONST_LABEL, Trunc, size_in_bits, 0, 0); + } + AOUT("set arg tls[%u] = %u\n", index, label); + __dfsan_arg_tls[index] = label; + } +} + +// Set SymSan retval TLS entry +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +void __taint_set_retval_tls(uint32_t index, dfsan_label label, uint32_t size_in_bits) { + if (index >= kRetvalTlsSize / sizeof(dfsan_label)) return; + // Truncate if size_in_bits is not byte-aligned + uint32_t size_in_bytes = (size_in_bits + 7) / 8; + if (size_in_bits < size_in_bytes * 8) { + label = do_taint_union(label, CONST_LABEL, Trunc, size_in_bits, 0, 0); + } + AOUT("set retval tls[%u] = %u\n", index, label); + __dfsan_retval_tls[index] = label; +} + +// Set SymSan shadow memory for a region +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +void __taint_set_label(dfsan_label label, void *addr, uint64_t size) { + dfsan_set_label(label, addr, size); +} + +// Copy SymSan shadow memory from src to dst +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +void __taint_copy_shadow(void *dst, void *src, uint64_t size) { + dfsan_label *dst_shadow = shadow_for(dst); + dfsan_label *src_shadow = shadow_for(src); + internal_memcpy(dst_shadow, src_shadow, size * sizeof(dfsan_label)); + // Propagate string content label from src to dst + dfsan_label str_label = taint_get_str_content_label(src); + if (str_label != 0) { + taint_set_str_content_label(dst, str_label); + } +} + +// Move SymSan shadow memory from src to dst (handles overlapping regions) +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +void __taint_move_shadow(void *dst, void *src, uint64_t size) { + dfsan_label *dst_shadow = shadow_for(dst); + dfsan_label *src_shadow = shadow_for(src); + internal_memmove(dst_shadow, src_shadow, size * sizeof(dfsan_label)); + // Propagate string content label from src to dst + dfsan_label str_label = taint_get_str_content_label(src); + if (str_label != 0) { + taint_set_str_content_label(dst, str_label); + } +} + +// Extend a SymSan label to a wider bit width via ZExt or SExt. +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +dfsan_label __taint_extend_label(dfsan_label label, bool sign_extend, uint16_t new_size_in_bits) { + if (label == 0) return 0; + uint16_t op = sign_extend ? __dfsan::SExt : __dfsan::ZExt; + return do_taint_union(label, CONST_LABEL, op, new_size_in_bits, 0, 0); +} + +// Get or create an Alloca bounds label for a pointer +// If ptr != NULL, checks shadow_for(&ptr) for existing Alloca to update +// If ptr == NULL or no existing Alloca, creates a new one +// Returns the Alloca label; caller stores it (e.g., via __taint_set_retval_tls) +// Overrides weak stub in ucsan.cpp +SANITIZER_INTERFACE_ATTRIBUTE +dfsan_label __taint_get_ptr_bounds_label(void *ptr, uint64_t lower, uint64_t upper) { + if (!flags().trace_bounds) return 0; + if (ptr != nullptr) { + dfsan_label label = *shadow_for(&ptr); + if (label != 0) { + dfsan_label_info *info = get_label_info(label); + if (info->op == __dfsan::Alloca) { + info->op1.i = lower; + info->op2.i = upper; + AOUT("update ptr bounds %p = %d, lower = %p, upper = %p\n", + ptr, label, (void*)lower, (void*)upper); + return label; + } + } + } + // Allocate new Alloca label + dfsan_label bound = dfsan_union(0, 0, Alloca, sizeof(void*) * 8, lower, upper); + AOUT("new ptr bounds label %d, lower = %p, upper = %p\n", + bound, (void*)lower, (void*)upper); + return bound; +} + +// Weak stub for UCSan's event tracing +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_event_addr, + uint32_t, uint32_t, uint64_t, void*, + uint32_t) {} + +} // extern "C" diff --git a/runtime/dfsan/dfsan.h b/runtime/dfsan/dfsan.h index 348211be..300a9a9d 100644 --- a/runtime/dfsan/dfsan.h +++ b/runtime/dfsan/dfsan.h @@ -57,7 +57,8 @@ struct dfsan_label_info { #define CONST_OFFSET 1 #define CONST_LABEL 0 -static const size_t uniontable_size = 0xc00000000; // FIXME +static const size_t minimum_uniontable_size = 0x10000 * sizeof(dfsan_label_info); // 64K entries +static size_t uniontable_size = 0xc00000000; // FIXME struct taint_file { char filename[PATH_MAX]; @@ -87,7 +88,7 @@ dfsan_label dfsan_read_label(const void *addr, uptr size); void dfsan_store_label(dfsan_label l1, void *addr, uptr size); dfsan_label dfsan_union(dfsan_label l1, dfsan_label l2, uint16_t op, uint16_t size, uint64_t op1, uint64_t op2); -dfsan_label dfsan_create_label(off_t offset); +dfsan_label dfsan_create_label(uint64_t input_id, uint64_t offset, uint32_t size_in_bytes); dfsan_label dfsan_get_label(const void *addr); dfsan_label_info* dfsan_get_label_info(dfsan_label label); @@ -105,6 +106,8 @@ void taint_set_str_content_label(void *addr, dfsan_label label); dfsan_label taint_get_str_content_label(const void *addr); void taint_set_str_indexof_label(void *addr, dfsan_label label); dfsan_label taint_get_str_indexof_label(const void *addr); +dfsan_label taint_find_string_op_source(dfsan_label label); +dfsan_label taint_get_base_input_label(dfsan_label label); // taint source utmp off_t get_utmp_offset(void); @@ -198,7 +201,8 @@ enum operators { fstrcmp = last_llvm_op + 18, // 85 strcmp using Z3 string theory fprefixof = last_llvm_op + 19, // 86 prefixof(str, prefix) using Z3 string theory fsuffixof = last_llvm_op + 20, // 87 suffixof(str, suffix) using Z3 string theory - LastOp = last_llvm_op + 21, // 88 + flength = last_llvm_op + 21, // 88 z3::length(str_var), Int sort + LastOp = last_llvm_op + 22, // 89 }; enum predicate { @@ -231,7 +235,7 @@ static inline uint8_t get_const_result(uint64_t c1, uint64_t c2, uint32_t predic return 0; } -static inline bool is_commutative(unsigned char op) { +static inline bool is_commutative(uint16_t op) { switch(op) { case Not: case And: @@ -247,14 +251,37 @@ static inline bool is_commutative(unsigned char op) { } } +// Check if an op is a string operation (fstr_op_start to fstr_op_end) +static inline bool is_string_op(uint16_t op) { + return op >= __dfsan::fstr_op_start && op < __dfsan::fstr_op_end; +} + +// Check if an op is an indexOf-type operation (returns position, not content) +// These are: fstrchr, fstrrchr, fstrstr, fstrpbrk, fstr_off +static inline bool is_indexof_op(uint16_t op) { + return op >= __dfsan::fstrchr && op <= __dfsan::fstr_off; +} + +// Check if an op is a content-type string operation (fsubstr, fstrcat) +static inline bool is_content_string_op(uint16_t op) { + return op == __dfsan::fsubstr || op == __dfsan::fstrcat; +} + // for out-of-process solving enum pipe_msg_type { cond_type = 0, gep_type = 1, memcmp_type = 2, - fsize_type = 3, + add_constraint_type = 3, memerr_type = 4, + // from thoroupy + exit_type, + loop_type, + bb_type, + event_type, + gv_type, + minimize_type, }; static const uint8_t TrueBranchLoopLatch = 0x8; @@ -280,6 +307,7 @@ enum undefined_check_ids { ub_unsigned_integer_truncation, ub_signed_integer_truncation, ub_integer_sign_change, + ub_assertion_failure, }; #define F_ADD_CONS 0x1 diff --git a/runtime/dfsan/dfsan_custom.cpp b/runtime/dfsan/dfsan_custom.cpp index a03c98a6..c545a86e 100644 --- a/runtime/dfsan/dfsan_custom.cpp +++ b/runtime/dfsan/dfsan_custom.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -63,79 +64,23 @@ SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void f(__VA_ARGS__); static off_t current_stdin_offset = 0; -// Check if an op is a string operation (fstr_op_start to fstr_op_end) -static inline bool is_string_op(uint16_t op) { - return op >= __dfsan::fstr_op_start && op < __dfsan::fstr_op_end; -} - -// Check if an op is an indexOf-type operation (returns position, not content) -// These are: fstrchr, fstrrchr, fstrstr, fstrpbrk, fstr_off -static inline bool is_indexof_op(uint16_t op) { - return op >= __dfsan::fstrchr && op <= __dfsan::fstr_off; -} - -// Check if an op is a content-type string operation (fsubstr, fstrcat) -static inline bool is_content_string_op(uint16_t op) { - return op == __dfsan::fsubstr || op == __dfsan::fstrcat; -} - -// Helper: Find the first (base) input byte label from a content label. -// Walks through Concat chains and Load operations to find the starting input. -// Returns the base label, or 0 if not found. -static dfsan_label get_base_input_label(dfsan_label label) { +// Get the concrete string length represented by a taint label. +// Returns 0 if the length cannot be determined. +static size_t get_label_string_length(dfsan_label label) { if (label < CONST_OFFSET) return 0; - dfsan_label_info *info = dfsan_get_label_info(label); + if (!info) return 0; - // Base input label has op == 0 - if (info->op == 0) return label; - - // For Concat (op 72), walk left (l1) to find the base - if (info->op == __dfsan::Concat) { - return get_base_input_label(info->l1); - } - - // For Load (op 32), l1 is the starting label if (info->op == __dfsan::Load) { - return info->l1; - } - - // For other ops, try l1 - if (info->l1 >= CONST_OFFSET) { - return get_base_input_label(info->l1); - } - - return 0; -} - -// Helper: Find if a label derives from a string op (fstrchr, fstrrchr, fstrstr) -// by walking through PtrToInt, Sub, Add operations. -// Returns the string op label if found, 0 otherwise. -static dfsan_label find_string_op_source(dfsan_label label) { - if (label < CONST_OFFSET) return 0; - - dfsan_label_info *info = dfsan_get_label_info(label); - uint16_t op = info->op; - - // Check if this is directly a string op - if (is_string_op(op)) { - return label; - } - - // Follow through PtrToInt, Sub, Add to find the source string op - if (op == __dfsan::PtrToInt || op == __dfsan::Sub || op == __dfsan::Add) { - // Recursively check l1 (the primary operand) - if (info->l1 >= CONST_OFFSET) { - dfsan_label result = find_string_op_source(info->l1); - if (result != 0) return result; - } - // For Sub/Add, also check l2 - if ((op == __dfsan::Sub || op == __dfsan::Add) && info->l2 >= CONST_OFFSET) { - dfsan_label result = find_string_op_source(info->l2); - if (result != 0) return result; - } + return info->l2; // number of bytes loaded + } else if (info->op == __dfsan::fsubstr) { + return (size_t)info->op1.i; // concrete substring length + } else if (info->op == __dfsan::fstrcat) { + size_t left = get_label_string_length(info->l1); + size_t right = get_label_string_length(info->l2); + if (left > 0 && right > 0) return left + right; + return 0; } - return 0; } @@ -207,7 +152,7 @@ static inline dfsan_label get_str_label_n(const void *s, dfsan_label s_label, // 4. Check if n_label derives from a string op (e.g., ptr arithmetic on memchr result) // If so, create fsubstr to represent substr(content, 0, idx) where idx is the string op result // IMPORTANT: Do this even when n=0 to preserve the symbolic constraint! - dfsan_label str_op_label = find_string_op_source(n_label); + dfsan_label str_op_label = taint_find_string_op_source(n_label); if (str_op_label != 0) { dfsan_label_info *str_op_info = dfsan_get_label_info(str_op_label); dfsan_label str_op_content = str_op_info->l1; @@ -219,8 +164,8 @@ static inline dfsan_label get_str_label_n(const void *s, dfsan_label s_label, // Verify same underlying buffer only if content is available bool same_buffer = true; if (content_label != 0) { - dfsan_label src_base = get_base_input_label(content_label); - dfsan_label str_op_base = get_base_input_label(str_op_content); + dfsan_label src_base = taint_get_base_input_label(content_label); + dfsan_label str_op_base = taint_get_base_input_label(str_op_content); same_buffer = (src_base != 0 && src_base == str_op_base); } // When n=0, trust that n_label derives from same buffer @@ -236,6 +181,37 @@ static inline dfsan_label get_str_label_n(const void *s, dfsan_label s_label, } // 5. Fall back to reading buffer content labels + // When length is symbolic and pointer has Alloca bounds, read the full object + // to create a complete string variable, then substr to the requested length. + // This ensures string theory operations (strchr, memrchr, etc.) see the full + // buffer even when the symbolic length is smaller than the actual object. + if (n_label != 0 && s_label != 0) { + dfsan_label_info *info = dfsan_get_label_info(s_label); + if (info && info->op == __dfsan::Alloca) { + uint64_t lower = info->op1.i; + uint64_t upper = info->op2.i; + uint64_t ptr_offset = (uint64_t)s - lower; + uint64_t remaining = upper - lower - ptr_offset; + AOUT("get_str_label_n: step 5 Alloca bounds lower=%p, upper=%p, " + "remaining=%lu, n=%zu\n", + (void*)lower, (void*)upper, remaining, n); + // Trim trailing untainted bytes (e.g., null terminators) + const dfsan_label *shadow = shadow_for(s); + while (remaining > n && shadow[remaining - 1] == 0) { + remaining--; + } + if (remaining > n) { + dfsan_label full_label = dfsan_read_label(s, remaining); + if (full_label != 0) { + return dfsan_union(full_label, n_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)n, 0); + } + } else if (remaining < n) { + AOUT("ERROR: OOB read in get_str_label_n: n=%zu exceeds " + "remaining=%lu bytes in object\n", n, remaining); + } + } + } return dfsan_read_label(s, n); } @@ -256,7 +232,7 @@ static inline dfsan_label get_str_label(const char *s, dfsan_label s_label) { static inline dfsan_label get_label_for(int fd, off_t offset) { // check if fd is stdin, if so, the label hasn't been pre-allocated if (is_stdin_taint() || (fd ==0 && flags().force_stdin)) - return dfsan_create_label(current_stdin_offset++); + return dfsan_create_label((uint64_t)fd, (uint64_t)(current_stdin_offset++), 1); // if fd is a tainted file, the label should have been pre-allocated else return (offset + CONST_OFFSET); } @@ -276,9 +252,52 @@ static void dfsan_memset(void *s, int c, dfsan_label c_label, size_t n) { dfsan_set_label(c_label, s, n); } +static inline dfsan_label bswap_label(dfsan_label label, uint16_t bits) { + if (!label || bits <= 8) + return label; + + const uint16_t num_bytes = bits / 8; + dfsan_label bytes[8] = {}; + for (uint16_t i = 0; i < num_bytes; ++i) { + bytes[i] = dfsan_union(label, 0, __dfsan::Extract, 8, 0, i * 8); + } + + dfsan_label result = bytes[num_bytes - 1]; + uint16_t accum_bits = 8; + for (int i = (int)num_bytes - 2; i >= 0; --i) { + accum_bits += 8; + result = dfsan_union(result, bytes[i], __dfsan::Concat, accum_bits, 0, 0); + } + return result; +} + +static inline dfsan_label net16_label(dfsan_label label) { +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \ + __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + return bswap_label(label, 16); +#else + return label; +#endif +} + +static inline dfsan_label net32_label(dfsan_label label) { +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \ + __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + return bswap_label(label, 32); +#else + return label; +#endif +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_offset(dfsan_label offset_label, int64_t offset, unsigned size); +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_add_constraint(dfsan_label label, uint8_t result); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_minimize_label(dfsan_label label, uint64_t size, dfsan_label bounds); + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_memcmp(dfsan_label label); @@ -286,6 +305,20 @@ extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_check_bounds(dfsan_label addr_label, uptr addr, dfsan_label size_label, uint64_t size); +// Encode the haystack base-pointer label into the high 32 bits of a string +// search op's op2 (whose low 8 bits hold the needle char). Under UC the search +// result is base+index; the solver needs the base pointer's label so it can +// pull its dependency / constrain it non-null. Returns op2 with both fields. +// Without UCSan the pointer label is concrete (0) so this is a no-op. +static inline uint64_t encode_strchr_op2(uint8_t needle, dfsan_label s_label) { +#ifdef USE_UCSAN_CUSTOM + return (uint64_t)needle | ((uint64_t)s_label << 32); +#else + (void)s_label; + return (uint64_t)needle; +#endif +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_cond(dfsan_label label, bool r, uint8_t flag, uint32_t cid); @@ -306,6 +339,11 @@ void __taint_trace_memerr(dfsan_label ptr_label, uptr ptr, uint16_t flag, void *addr); extern "C" { +SANITIZER_INTERFACE_ATTRIBUTE __attribute__((noreturn)) void +__dfsw_exit(int status, dfsan_label status_label) { + exit(status); +} + SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_stat(const char *path, struct stat *buf, dfsan_label path_label, dfsan_label buf_label, dfsan_label *ret_label) { @@ -402,32 +440,6 @@ __dfsw_lstat(const char *path, struct stat *buf, dfsan_label path_label, return ret; } -// Create a label for string op + constant offset (for pointer arithmetic like sep + 1) -// If base_label is a string op, returns a new fstr_off label; otherwise returns base_label -extern "C" SANITIZER_INTERFACE_ATTRIBUTE -void __taint_trace_gep_ptr(dfsan_label base_label, char *result, char *base) { - if (base_label < CONST_OFFSET) return; - - // Check if base_label is or derives from a string op - dfsan_label str_op_label = find_string_op_source(base_label); - if (str_op_label == 0) { - // Not a string op - return base label unchanged - return; - } - - // Create fstr_off label: l1=str_op_label, op1=offset - // This represents the content at (string_op_position + offset) - uint64_t offset = (uint64_t)(result - base); - dfsan_label off_label = dfsan_union(str_op_label, 0, __dfsan::fstr_off, - sizeof(void*) * 8, - 0, (uint64_t)offset); - AOUT("gep_ptr: base=%u, str_op=%u, offset=%ld, result=%u\n", - base_label, str_op_label, offset, off_label); - - // record the label (fstr_off is an indexOf-type op) - taint_set_str_indexof_label(result, off_label); -} - SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strchr(char *s, int c, dfsan_label s_label, dfsan_label c_label, @@ -452,7 +464,7 @@ SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strchr(char *s, int c, *ret_label = dfsan_union(src_label, c_label, __dfsan::fstrchr, content_len, (uint64_t)s, - (uint64_t)(uint8_t)c); + encode_strchr_op2((uint8_t)c, s_label)); // Send concrete haystack content if haystack is concrete if (content_len > 0 && *ret_label) { @@ -562,6 +574,8 @@ SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_bcmp(const void *s1, const void *s2, dfsan_label *ret_label) { __taint_check_bounds(s1_label, (uptr)s1, n_label, n); __taint_check_bounds(s2_label, (uptr)s2, n_label, n); + __taint_solve_size(s1_label, (uint64_t)s1, n_label, n, 0); + __taint_solve_size(s2_label, (uint64_t)s2, n_label, n, 0); int ret = bcmp(s1, s2, n); // Check for fsubstr labels (from strncpy with symbolic length) @@ -883,6 +897,9 @@ __dfsw_strlen(const char *s, dfsan_label s_label, dfsan_label *ret_label) { return ret; } +// When USE_UCSAN_CUSTOM is defined, ucsan_custom.cpp provides these functions +// which handle both UCSan and SymSan shadow memory via the bridge. +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memcpy(void *dest, const void *src, size_t n, dfsan_label dest_label, dfsan_label src_label, @@ -893,6 +910,11 @@ void *__dfsw_memcpy(void *dest, const void *src, size_t n, __taint_solve_bounds(src_label, (uint64_t)src, n_label, n, 0, 1, 0, 0); __taint_solve_bounds(dest_label, (uint64_t)dest, n_label, n, 0, 1, 0, 0); } + // Propagate string content label from src to dest + dfsan_label str_label = taint_get_str_content_label(src); + if (str_label != 0) { + taint_set_str_content_label(dest, str_label); + } *ret_label = dest_label; return dfsan_memcpy(dest, src, n); } @@ -907,6 +929,11 @@ void *__dfsw_memmove(void *dest, const void *src, size_t n, __taint_solve_bounds(src_label, (uint64_t)src, n_label, n, 0, 1, 0, 0); __taint_solve_bounds(dest_label, (uint64_t)dest, n_label, n, 0, 1, 0, 0); } + // Propagate string content label from src to dest + dfsan_label str_label = taint_get_str_content_label(src); + if (str_label != 0) { + taint_set_str_content_label(dest, str_label); + } dfsan_label tmp[n]; dfsan_label *sdest = shadow_for(dest); const dfsan_label *ssrc = shadow_for(src); @@ -928,6 +955,7 @@ void *__dfsw_memset(void *s, int c, size_t n, *ret_label = s_label; return s; } +#endif // USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_tolower(int c, dfsan_label c_label, dfsan_label *ret_label) { @@ -943,6 +971,78 @@ int __dfsw_toupper(int c, dfsan_label c_label, dfsan_label *ret_label) { return ret; } +SANITIZER_INTERFACE_ATTRIBUTE +uint16_t __dfsw_htons(uint16_t hostshort, dfsan_label hostshort_label, + dfsan_label *ret_label) { + uint16_t ret = htons(hostshort); + *ret_label = net16_label(hostshort_label); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint16_t __dfsw_ntohs(uint16_t netshort, dfsan_label netshort_label, + dfsan_label *ret_label) { + uint16_t ret = ntohs(netshort); + *ret_label = net16_label(netshort_label); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint32_t __dfsw_htonl(uint32_t hostlong, dfsan_label hostlong_label, + dfsan_label *ret_label) { + uint32_t ret = htonl(hostlong); + *ret_label = net32_label(hostlong_label); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint32_t __dfsw_ntohl(uint32_t netlong, dfsan_label netlong_label, + dfsan_label *ret_label) { + uint32_t ret = ntohl(netlong); + *ret_label = net32_label(netlong_label); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint16_t __dfsw___bswap_16(uint16_t x, dfsan_label x_label, + dfsan_label *ret_label) { + uint16_t ret = __builtin_bswap16(x); + *ret_label = bswap_label(x_label, 16); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint32_t __dfsw___bswap_32(uint32_t x, dfsan_label x_label, + dfsan_label *ret_label) { + uint32_t ret = __builtin_bswap32(x); + *ret_label = bswap_label(x_label, 32); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint64_t __dfsw___bswap_64(uint64_t x, dfsan_label x_label, + dfsan_label *ret_label) { + uint64_t ret = __builtin_bswap64(x); + *ret_label = bswap_label(x_label, 64); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint32_t __dfsw___bswapsi2(uint32_t x, dfsan_label x_label, + dfsan_label *ret_label) { + uint32_t ret = __builtin_bswap32(x); + *ret_label = bswap_label(x_label, 32); + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE +uint64_t __dfsw___bswapdi2(uint64_t x, dfsan_label x_label, + dfsan_label *ret_label) { + uint64_t ret = __builtin_bswap64(x); + *ret_label = bswap_label(x_label, 64); + return ret; +} + SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strcat(char *dest, const char *src, dfsan_label d_label, dfsan_label s_label, dfsan_label *ret_label) { @@ -1141,7 +1241,7 @@ __dfsw_strncpy(char *s1, const char *s2, size_t n, dfsan_label s1_label, __taint_solve_bounds(s1_label, (uint64_t)s1, n_label, n, 0, 1, 0, 0); // Check if n_label derives from a string op (e.g., strchr index) - dfsan_label str_op_label = n_label ? find_string_op_source(n_label) : 0; + dfsan_label str_op_label = n_label ? taint_find_string_op_source(n_label) : 0; bool created_fsubstr = false; if (str_op_label != 0) { @@ -1156,8 +1256,8 @@ __dfsw_strncpy(char *s1, const char *s2, size_t n, dfsan_label s1_label, if (copy_len > 0) { dfsan_label src_content = dfsan_read_label(s2, copy_len); if (src_content >= CONST_OFFSET) { - dfsan_label src_base = get_base_input_label(src_content); - dfsan_label str_op_base = get_base_input_label(str_op_content); + dfsan_label src_base = taint_get_base_input_label(src_content); + dfsan_label str_op_base = taint_get_base_input_label(str_op_content); buffers_match = (src_base != 0 && src_base == str_op_base); } } else { @@ -1198,6 +1298,7 @@ __dfsw_strncpy(char *s1, const char *s2, size_t n, dfsan_label s1_label, return s1; } +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE ssize_t __dfsw_pread(int fd, void *buf, size_t count, off_t offset, dfsan_label fd_label, dfsan_label buf_label, @@ -1270,6 +1371,7 @@ __dfsw_read(int fd, void *buf, size_t count, } return ret; } +#endif // USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_clock_gettime(clockid_t clk_id, struct timespec *tp, @@ -1874,17 +1976,29 @@ SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memchr(void *s, int c, size_t n, dfsan_label src_label = get_str_label_n(s, s_label, n, n_label); if (src_label != 0 || c_label != 0) { + // When n is symbolic, bound the haystack to first n bytes so the solver + // produces indexof(substr(s, 0, n), char) instead of indexof(s, char). + // Skip if get_str_label_n already returned an fsubstr (avoids nesting). + dfsan_label bounded_src = src_label; + if (src_label != 0 && n_label != 0) { + dfsan_label_info *src_info = dfsan_get_label_info(src_label); + if (src_info && !is_content_string_op(src_info->op)) { + bounded_src = dfsan_union(src_label, n_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)n, 0); + } + } + // Determine which operand is concrete and set size accordingly uint16_t content_len = (src_label == 0) ? (uint16_t)n : 0; - // l1 = src_label (haystack content) + // l1 = bounded_src (haystack content, bounded by n when symbolic) // l2 = c_label (character to find) // op1 = haystack pointer (for concrete content retrieval) // op2 = character value // size = haystack length if haystack concrete, else 0 - *ret_label = dfsan_union(src_label, c_label, __dfsan::fstrchr, + *ret_label = dfsan_union(bounded_src, c_label, __dfsan::fstrchr, content_len, - (uint64_t)s, (uint64_t)(uint8_t)c); + (uint64_t)s, encode_strchr_op2((uint8_t)c, s_label)); // Send concrete content if haystack is concrete if (content_len > 0 && *ret_label) { @@ -1923,7 +2037,7 @@ SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strrchr(char *s, int c, *ret_label = dfsan_union(src_label, c_label, __dfsan::fstrrchr, content_len, (uint64_t)s, - (uint64_t)(uint8_t)c); + encode_strchr_op2((uint8_t)c, s_label)); // Send concrete haystack content if haystack is concrete if (content_len > 0 && *ret_label) { @@ -1952,17 +2066,31 @@ SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memrchr(const void *s, int c, size_t dfsan_label src_label = get_str_label_n(s, s_label, n, n_label); if (src_label != 0 || c_label != 0) { + // When n is symbolic, bound the haystack to first n bytes so the solver + // produces last_indexof(substr(s, 0, n), char) instead of + // last_indexof(s, char). Without this, the solver can grow the string + // variable beyond the actual symbolized region when it increases n. + // Skip if get_str_label_n already returned an fsubstr (avoids nesting). + dfsan_label bounded_src = src_label; + if (src_label != 0 && n_label != 0) { + dfsan_label_info *src_info = dfsan_get_label_info(src_label); + if (src_info && !is_content_string_op(src_info->op)) { + bounded_src = dfsan_union(src_label, n_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)n, 0); + } + } + // Determine which operand is concrete and set size accordingly uint16_t content_len = (src_label == 0) ? (uint16_t)n : 0; - // l1 = src_label (haystack content) + // l1 = bounded_src (haystack content, bounded by n when symbolic) // l2 = c_label (character to find) // op1 = haystack pointer (for concrete content retrieval) // op2 = character value // size = haystack length if haystack concrete, else 0 - *ret_label = dfsan_union(src_label, c_label, __dfsan::fstrrchr, + *ret_label = dfsan_union(bounded_src, c_label, __dfsan::fstrrchr, content_len, - (uint64_t)s, (uint64_t)(uint8_t)c); + (uint64_t)s, encode_strchr_op2((uint8_t)c, s_label)); // Send concrete content if haystack is concrete if (content_len > 0 && *ret_label) { @@ -2109,6 +2237,19 @@ SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memmem(const void *haystack, size_t h dfsan_label src_label = get_str_label_n(haystack, haystack_label, haystacklen, haystacklen_label); + // When haystacklen is symbolic, bound the haystack to first haystacklen bytes + // so the solver produces indexof(substr(haystack, 0, n), needle) instead of + // indexof(haystack, needle). Without this, the solver can grow the string + // variable beyond the actual symbolized region when it increases haystacklen. + // Skip if get_str_label_n already returned an fsubstr (avoids nesting). + if (src_label != 0 && haystacklen_label != 0) { + dfsan_label_info *src_info = dfsan_get_label_info(src_label); + if (src_info && !is_content_string_op(src_info->op)) { + src_label = dfsan_union(src_label, haystacklen_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)haystacklen, 0); + } + } + // Use unified get_str_label_n for needle dfsan_label real_needle_label = get_str_label_n(needle, needle_label, needlelen, needlelen_label); @@ -2177,7 +2318,8 @@ SANITIZER_INTERFACE_ATTRIBUTE ssize_t __dfsw_recv( if (offset >= 0) { AOUT("recv: fd = %d, offset = %ld, ret = %ld\n", sockfd, offset, ret); for (ssize_t i = 0; i < ret; i++) { - dfsan_set_label(dfsan_create_label(offset + i), (char *)buf + i, 1); + dfsan_label lbl = dfsan_create_label((uint64_t)sockfd, (uint64_t)(offset + i), 1); + dfsan_set_label(lbl, (char *)buf + i, 1); } taint_update_socket_offset(sockfd, ret); } else { @@ -2209,7 +2351,8 @@ SANITIZER_INTERFACE_ATTRIBUTE ssize_t __dfsw_recvfrom( off_t offset = taint_get_socket(sockfd); if (offset >= 0) { for (ssize_t i = 0; i < ret; i++) { - dfsan_set_label(dfsan_create_label(offset + i), (char *)buf + i, 1); + dfsan_label lbl = dfsan_create_label((uint64_t)sockfd, (uint64_t)(offset + i), 1); + dfsan_set_label(lbl, (char *)buf + i, 1); } taint_update_socket_offset(sockfd, ret); } else { @@ -2239,7 +2382,8 @@ static void taint_handle_msg(int sockfd, struct msghdr *msg, size_t msg_len) { bytes_written < iov->iov_len ? bytes_written : iov->iov_len; if (offset >= 0) { for (size_t j = 0; j < iov_written; ++j) { - dfsan_set_label(dfsan_create_label(offset + j), (char *)iov->iov_base + j, 1); + dfsan_label lbl = dfsan_create_label((uint64_t)sockfd, (uint64_t)(offset + j), 1); + dfsan_set_label(lbl, (char *)iov->iov_base + j, 1); } taint_update_socket_offset(sockfd, iov_written); offset += iov_written; @@ -2617,6 +2761,7 @@ SANITIZER_INTERFACE_WEAK_DEF(void, __dfsw___sanitizer_cov_trace_const_cmp8, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __dfsw___sanitizer_cov_trace_switch, void) {} +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_open(const char *path, int oflags, dfsan_label path_label, dfsan_label flag_label, dfsan_label *va_labels, @@ -2700,7 +2845,9 @@ __dfsw_fclose(FILE *fp, dfsan_label fp_label, dfsan_label *ret_label) { *ret_label = 0; return ret; } +#endif // USE_UCSAN_CUSTOM +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE size_t __dfsw_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, dfsan_label ptr_label, dfsan_label size_label, @@ -2722,7 +2869,8 @@ __dfsw_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, fwrite(ptr, size, nmemb, stream); // update taint for (size_t i = 0; i < size * nmemb; i++) { - dfsan_set_label(dfsan_create_label(offset + i), (char *)ptr + i, 1); + dfsan_label lbl = dfsan_create_label((uint64_t)fd, (uint64_t)(offset + i), 1); + dfsan_set_label(lbl, (char *)ptr + i, 1); } return nmemb; // directly return } @@ -2771,7 +2919,8 @@ __dfsw_fread_unlocked( fwrite(ptr, size, nmemb, stream); // update taint for (size_t i = 0; i < size * nmemb; i++) { - dfsan_set_label(dfsan_create_label(offset + i), (char *)ptr + i, 1); + dfsan_label lbl = dfsan_create_label((uint64_t)fd, (uint64_t)(offset + i), 1); + dfsan_set_label(lbl, (char *)ptr + i, 1); } return nmemb; // directly return } @@ -2797,6 +2946,7 @@ __dfsw_fread_unlocked( } return ret; } +#endif // USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE ssize_t __dfsw_getline(char **lineptr, size_t *n, FILE *stream, @@ -2984,14 +3134,19 @@ char *__dfsw_fgets_unlocked(char *s, int size, FILE *stream, dfsan_label s_label } static inline void __taint_check_malloc_size(size_t size, dfsan_label size_label) { - if (size_label && flags().solve_ub) { + if (size_label) { AOUT("*alloc size: %lu = %d\n", size, size_label); - // -fsanitize=unsigned-integer-overflow - dfsan_label os = dfsan_union(0, size_label, (bveq << 8) | ICmp, 64, 0, size); - __taint_trace_cond(os, 0, UndefinedCheck, ub_integer_overflow); + // hint solver to minimize allocation size + __taint_minimize_label(size_label, (uint64_t)size, 0); + if (flags().solve_ub) { + // -fsanitize=unsigned-integer-overflow + dfsan_label os = dfsan_union(0, size_label, (bveq << 8) | ICmp, 64, 0, size); + __taint_trace_cond(os, 0, UndefinedCheck, ub_integer_overflow); + } } } +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE void * __dfsw_realloc(void *ptr, size_t new_size, dfsan_label ptr_label, dfsan_label new_size_label, @@ -3445,6 +3600,7 @@ void __dfsw___libc_free(void *ptr, dfsan_label ptr_label) { free(ptr); } } +#endif // USE_UCSAN_CUSTOM static dfsan_label taint_getc(int fd, off_t offset, int ret) { if (ret != EOF && taint_get_file(fd)) { @@ -3454,6 +3610,7 @@ static dfsan_label taint_getc(int fd, off_t offset, int ret) { return 0; } +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_fgetc(FILE *stream, dfsan_label stream_label, dfsan_label *ret_label) { int fd = fileno(stream); @@ -3507,6 +3664,7 @@ __dfsw_getchar(dfsan_label *ret_label) { *ret_label = taint_getc(0, offset, ret); return ret; } +#endif // USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE size_t __dfsw_mbrtowc(wchar_t *pwc, const char *s, size_t n, mbstate_t *ps, @@ -3560,6 +3718,7 @@ __dfsw_munmap(void *addr, size_t length, dfsan_label addr_label, return ret; } +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE off_t __dfsw_lseek(int fd, off_t offset, int whence, dfsan_label fd_label, dfsan_label offset_label, dfsan_label whence_label, @@ -3576,6 +3735,7 @@ __dfsw_lseek(int fd, off_t offset, int whence, dfsan_label fd_label, } else *ret_label = 0; return ret; } +#endif // USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE off64_t __dfsw_lseek64(int fd, off64_t offset, int whence, dfsan_label fd_label, @@ -3594,6 +3754,7 @@ __dfsw_lseek64(int fd, off64_t offset, int whence, dfsan_label fd_label, return ret; } +#ifndef USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_fseek(FILE *stream, long offset, int whence, dfsan_label stream_label, dfsan_label offset_label, dfsan_label whence_label, @@ -3625,6 +3786,7 @@ __dfsw_fseeko(FILE *stream, off_t offset, int whence, dfsan_label stream_label, } return ret; } +#endif // USE_UCSAN_CUSTOM SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_fseeko64(FILE *stream, off64_t offset, int whence, dfsan_label stream_label, @@ -3642,4 +3804,34 @@ __dfsw_fseeko64(FILE *stream, off64_t offset, int whence, dfsan_label stream_lab return ret; } +/// for assertion and assumption + +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_event_addr, + uint16_t, uint32_t, uint64_t, void*, uint32_t) {} + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_assert_cond(bool result, uint64_t id, dfsan_label result_label, dfsan_label id_label) { + if (!result) { + AOUT("ERROR: assertion %lu failure: result %d, label %d\n", id, result, result_label); + __taint_trace_event_addr(0, 103, id, (void *)__builtin_return_address(0), 8); + } else { + __taint_trace_event_addr(0, 103, id, (void *)__builtin_return_address(0), 9); + __taint_trace_cond(result_label, result, UndefinedCheck, ub_assertion_failure); + } +} + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_assume_cond(bool result, uint64_t id, dfsan_label result_label, dfsan_label id_label) { + if (result_label) { + AOUT("WARNING: assumption label is concrete for id %lu\n", id); + } + if (!result) { + __taint_trace_cond(result_label, result, 0, id); + AOUT("WARNING: assumption %lu is false, exiting\n", id); + __taint_trace_event_addr(result_label, 103, id, (void*)result, 10); + exit(201); + } + __taint_add_constraint(result_label, 1); +} + } // extern "C" diff --git a/runtime/dfsan/dfsan_flags.inc b/runtime/dfsan/dfsan_flags.inc index b39c47fa..046e3f36 100644 --- a/runtime/dfsan/dfsan_flags.inc +++ b/runtime/dfsan/dfsan_flags.inc @@ -35,11 +35,18 @@ DFSAN_FLAG(const char *, taint_socket, "", "The network source which " "will be tainted.") DFSAN_FLAG(const char *, union_table, "union.txt", "union table.") DFSAN_FLAG(int, shm_fd, -1, "shared union table.") -DFSAN_FLAG(int, pipe_fd, -1, "communication fd.") +DFSAN_FLAG(const char *, shm_name, "", "shared union table by key.") +DFSAN_FLAG(size_t, shm_size, 0, "shared union table size.") +DFSAN_FLAG(int, pipe_fd, -1, "communication pipe fd.") +DFSAN_FLAG(const char *, pipe_name, "", "communication pipe filename.") +DFSAN_FLAG(int, control_pipe_fd, -1, "control pipe fd.") +DFSAN_FLAG(const char *, control_pipe_name, "", "control pipe filename.") DFSAN_FLAG(bool, trace_bounds, false, "trace bounds info.") DFSAN_FLAG(bool, trace_fsize, false, "trace file size.") DFSAN_FLAG(bool, exit_on_memerror, true, "terminate on memory error.") DFSAN_FLAG(bool, solve_ub, false, "solve undefined behavior.") +DFSAN_FLAG(bool, allow_zero_size_alloc, false, + "Allow malloc-size minimization to solve allocation sizes to zero.") DFSAN_FLAG(bool, debug, false, "Print debug output.") DFSAN_FLAG(const char *, output_dir, ".", "The path for output file.") DFSAN_FLAG(int, instance_id, 0, "instance id for multi-instance fuzzing.") diff --git a/runtime/dfsan/done_abilist.txt b/runtime/dfsan/done_abilist.txt index c454b3b3..a588c823 100644 --- a/runtime/dfsan/done_abilist.txt +++ b/runtime/dfsan/done_abilist.txt @@ -149,6 +149,69 @@ fun:sqrtf=functional fun:wctob=functional fun:wctob=functional +# C++ exception-handling subsystem. EH must run CONCRETELY: the unwinder, the +# personality routine, and the Itanium __cxa_* / type-matching internals cannot +# be meaningfully instrumented — instrumenting any of it breaks the unwind, so a +# throw never reaches its catch. EH is also tightly coupled (the entry points +# call internal helpers and the DWARF unwinder), so the whole connected set must +# be uninstrumented together; a plain caller of an instrumented callee is an +# undefined-symbol link error. Instrumented code reaches these via dfsw$ +# wrappers, and the thrown value's symbolic label still propagates through them +# via address-keyed shadow memory. Functions NOT listed (operator new/delete +# = _Znwm/_ZdlPv, __cxa_guard_*) stay instrumented so the STL keeps taint +# tracking. (The _Unwind_* entry points are in the per-OS libc abilist.) +# +# IMPORTANT: only the functions on the *unwind* path are uninstrumented. The +# allocation and deallocation of the exception object run in normal context and +# stay INSTRUMENTED so malloc/free remain captured (they are custom wrappers +# recorded only at instrumented call sites) — symsan/ucsan needs this to track +# allocation sizes for heap bug detection. Specifically these stay instrumented +# (NOT listed here): __cxa_allocate_exception(+dependent), __cxa_end_catch, +# __cxa_free_exception(+dependent), __cxa_decrement_exception_refcount, and the +# __aligned_malloc/__calloc/__free/__aligned_free_with_fallback wrappers. +# __cxa_end_catch and friends are only called from the landing pad (instrumented +# user code), never from the unwinder, so instrumenting them is safe. +# +# Unwind-path entry points (Itanium ABI, extern "C"). __cxa_begin_catch is here +# because the unwinder's failed_throw (no-handler path) calls it directly, so it +# must be concrete; __cxa_increment_exception_refcount follows because +# begin_catch calls it. +fun:__cxa_throw=uninstrumented +fun:__cxa_rethrow=uninstrumented +fun:__cxa_begin_catch=uninstrumented +fun:__cxa_get_exception_ptr=uninstrumented +fun:__cxa_call_unexpected=uninstrumented +fun:__cxa_increment_exception_refcount=uninstrumented +fun:__cxa_get_globals=uninstrumented +fun:__cxa_get_globals_fast=uninstrumented +fun:__gxx_personality_v0=uninstrumented +fun:__dynamic_cast=uninstrumented +# libc++abi C++ internals on the unwind path: personality statics (scan_eh_tab, +# exception_spec_can_catch, failed_throw = _ZN10__cxxabiv1L*) and the can_catch / +# dynamic-cast type-matching machinery (_ZNK10__cxxabiv1*). The *_with_fallback +# malloc/free wrappers are NOT matched by these patterns (they are +# _ZN10__cxxabiv1..., not L/K) so they stay instrumented and captured. +fun:_ZN10__cxxabiv1L*=uninstrumented +fun:_ZNK10__cxxabiv1*=uninstrumented +# std::terminate / std::unexpected and their get/set handlers. +fun:_ZSt9terminatev=uninstrumented +fun:_ZSt10unexpectedv=uninstrumented +fun:_ZSt11__terminatePFvvE=uninstrumented +fun:_ZSt12__unexpectedPFvvE=uninstrumented +fun:_ZSt13get_terminatev=uninstrumented +fun:_ZSt13set_terminatePFvvE=uninstrumented +fun:_ZSt14get_unexpectedv=uninstrumented +fun:_ZSt14set_unexpectedPFvvE=uninstrumented +# libunwind: the DWARF unwinder (entry API + statics). The heavy lifting is +# inlined into the __unw_* functions. +fun:__unw_*=uninstrumented +fun:unwind_phase2=uninstrumented +fun:unwind_phase2_forced=uninstrumented +fun:logAPIs=uninstrumented +fun:logDWARF=uninstrumented +fun:logUnwinding=uninstrumented +fun:_ZN9libunwind*=uninstrumented + # Functions that produce an output that does not depend on the input (shadow is # zeroed automatically). fun:__assert_fail=discard @@ -297,6 +360,20 @@ fun:strtoull=custom fun:atoi=custom fun:atol=custom fun:atoll=custom +fun:htonl=custom +fun:htons=custom +fun:ntohl=custom +fun:ntohs=custom +fun:__bswap_16=custom +fun:__bswap_32=custom +fun:__bswap_64=custom +fun:__bswapsi2=custom +fun:__bswapdi2=custom +# __bswap_16/32/64 have no libc abilist entry; mark uninstrumented (like other +# custom libc funcs) so TaintPass wraps them instead of renaming to .taint. +fun:__bswap_16=uninstrumented +fun:__bswap_32=uninstrumented +fun:__bswap_64=uninstrumented fun:tolower=custom fun:toupper=custom @@ -601,6 +678,12 @@ fun:__dfsw_*=discard fun:__taint_*=uninstrumented fun:__taint_*=discard +# custom assertions +fun:assert_cond=custom +fun:assert_cond=uninstrumented +fun:assume_cond=custom +fun:assume_cond=uninstrumented + # Don't add extra parameters to the Fuzzer callback. fun:LLVMFuzzerTestOneInput=uninstrumented fun:__afl_manual_init=uninstrumented diff --git a/runtime/dfsan/ucsan.cpp b/runtime/dfsan/ucsan.cpp new file mode 100644 index 00000000..82c78765 --- /dev/null +++ b/runtime/dfsan/ucsan.cpp @@ -0,0 +1,2151 @@ +//===-- ucsan.cpp - UCSan Runtime Implementation ------------------===// +// +// Under-Constrained Symbolic Sanitizer Runtime +// +// This file implements the UCSan runtime for: +// - Lazy object initialization +// - Pseudo-pointer resolution +// - Pointer shadow tracking +// +// UCSan is designed to work alongside SymSan but with independent +// shadow memory for pointer/object tracking (vs symbolic expressions). +// +//===----------------------------------------------------------------------===// + +#include "ucsan.h" +#include "ucsan_containers.h" +#include "sanitizer_common/sanitizer_atomic.h" +#include "sanitizer_common/sanitizer_common.h" +#include "sanitizer_common/sanitizer_file.h" +#include "sanitizer_common/sanitizer_flag_parser.h" +#include "sanitizer_common/sanitizer_flags.h" +#include "sanitizer_common/sanitizer_libc.h" +#include "sanitizer_common/sanitizer_posix.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace __sanitizer; + +// Forward declarations for trace callbacks (implemented in solvers) +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_trace_event_addr(uint32_t label, uint32_t event_id, + uint64_t info, void* addr, uint32_t info2); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_trace_global_var(uint32_t obj_id, uint64_t offset, uint64_t size, void *gv); + +// Forward declaration for UCSan solver initialization (thoroupy backend) +extern "C" void InitializeUCSanSolver(); + +// Ensure dfsan is initialized before ucsan (weak, no-op when standalone) +extern "C" SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE +void __dfsan_ensure_init(int argc, char **argv, char **envp); + +// Forward declarations for SymSan bridge functions +// (weak stubs, overridden when linked with SymSan) +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +dfsan_label __taint_create_label(uint32_t object_id, uint64_t offset, + uint32_t size_in_bytes); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_set_arg_tls(uint32_t index, dfsan_label label, + uint32_t size_in_bits); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_set_retval_tls(uint32_t index, dfsan_label label, uint32_t size_in_bits); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_set_label(dfsan_label label, void *addr, uint64_t size); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __taint_copy_shadow(void *dst, void *src, uint64_t size); + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +dfsan_label __taint_get_ptr_bounds_label(void *ptr, uint64_t lower, uint64_t upper); + +//===----------------------------------------------------------------------===// +// Global State +//===----------------------------------------------------------------------===// + +namespace __ucsan { + +// Global input/taint source +ucsan_input ucsan_tainted; + +// Configuration flags +UCSanFlags ucsan_flags_data; + +// Debug flag +bool ucsan_debug = false; + +// Label management +static atomic_uint32_t __ucsan_last_label; +static ucsan_label_info *__ucsan_label_info = nullptr; + +// Object counter +static atomic_uint32_t __ucsan_inited_objects; + +// Address range map for __ucsan_symbolize_input pointer relationship discovery +static AddrRangeMap __symbolize_addr_map; +static bool __symbolize_addr_map_inited = false; + +// Address range map for fixed-size global object bounds. +static AddrRangeMap __global_bounds_map; +static bool __global_bounds_map_inited = false; + +// Stack management for bounds tracking +static ucsan_label __alloca_stack_bottom; +static ucsan_label __alloca_stack_top; +static ucsan_label __saved_alloca_stack_top[UCSAN_MAX_SAVED_STACK_ENTRIES]; +static int __current_saved_stack_index = 0; + +} // namespace __ucsan + +// DSO section markers (linker-provided, must be at global scope with C linkage) +extern "C" { +extern char __dso_handle; +extern char edata; +extern char end; +} + +namespace __ucsan { + +bool in_data_section(void *p) { + return (&__dso_handle <= (char*)p && (char*)p <= &end); +} + +bool in_bss_section(void *p) { + return (&edata <= (char*)p && (char*)p <= &end); +} + +} // namespace __ucsan + +//===----------------------------------------------------------------------===// +// TLS Variables +//===----------------------------------------------------------------------===// + +using __ucsan::ucsan_label; + +extern "C" { + +SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL ucsan_label + __ucsan_retval_tls[UCSAN_TLS_SIZE / sizeof(ucsan_label)]; + +SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL ucsan_label + __ucsan_arg_tls[UCSAN_TLS_SIZE / sizeof(ucsan_label)]; + +SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint8_t + __ucsan_wrapped_return_tls[UCSAN_TLS_SIZE]; + +void* __ucsan_null_deref_flag = nullptr; + +} // extern "C" + +//===----------------------------------------------------------------------===// +// Container Implementations (replaces STL) +//===----------------------------------------------------------------------===// + +using namespace __ucsan; +using __sanitizer::MmapOrDie; +using __sanitizer::UnmapOrDie; +using __sanitizer::RoundUpTo; +using __sanitizer::GetPageSizeCached; +using __sanitizer::internal_memcpy; +using __sanitizer::internal_memset; + +void ByteBuffer::destroy() { + if (buf) { + UnmapOrDie(buf, RoundUpTo(cap, GetPageSizeCached())); + buf = nullptr; + } + len = cap = 0; +} + +void ByteBuffer::resize(uint32_t new_size) { + if (new_size > cap) { + uint32_t new_cap = cap ? cap : 16; + while (new_cap < new_size) new_cap *= 2; + uptr old_alloc = RoundUpTo(cap, GetPageSizeCached()); + uptr new_alloc = RoundUpTo(new_cap, GetPageSizeCached()); + if (buf) { + uptr r = internal_mremap(buf, old_alloc, new_alloc, MREMAP_MAYMOVE, nullptr); + if (!internal_iserror(r)) { + buf = (unsigned char *)r; + } else { + unsigned char *new_buf = (unsigned char *)MmapOrDie(new_alloc, "ByteBuffer"); + __builtin_memcpy(new_buf, buf, len); + UnmapOrDie(buf, old_alloc); + buf = new_buf; + } + } else { + buf = (unsigned char *)MmapOrDie(new_alloc, "ByteBuffer"); + } + cap = new_cap; + } + len = new_size; +} + +void ByteBuffer::assign(const unsigned char *src, uint32_t n) { + resize(n); + __builtin_memcpy(buf, src, n); +} + +void ObjectStorage::destroy() { + if (items) { + for (uint32_t i = 0; i < len; i++) + items[i].data.destroy(); + UnmapOrDie(items, RoundUpTo(cap * sizeof(UCSanObject), GetPageSizeCached())); + items = nullptr; + } + len = cap = 0; +} + +void ObjectStorage::resize(uint32_t new_size) { + if (new_size > cap) { + uint32_t new_cap = cap ? cap : 4; + while (new_cap < new_size) new_cap *= 2; + uptr alloc_size = RoundUpTo(new_cap * sizeof(UCSanObject), GetPageSizeCached()); + UCSanObject *new_items = (UCSanObject *)MmapOrDie(alloc_size, "ObjectStorage"); + if (items) { + internal_memcpy(new_items, items, len * sizeof(UCSanObject)); + UnmapOrDie(items, RoundUpTo(cap * sizeof(UCSanObject), GetPageSizeCached())); + } + items = new_items; + cap = new_cap; + } + // Initialize new entries (MmapOrDie zero-fills, but init ByteBuffer explicitly) + for (uint32_t i = len; i < new_size; i++) { + items[i].offset = 0; + items[i].data.init(); + items[i].origin = {0, 0}; + } + len = new_size; +} + +void ObjectStorage::clear() { + for (uint32_t i = 0; i < len; i++) + items[i].data.destroy(); + len = 0; +} + +void ObjectStorage::push_back(const UCSanObject &obj) { + if (len >= cap) { + resize(len + 1); + items[len - 1] = obj; + } else { + items[len++] = obj; + } +} + +UCSanObject& ObjectStorage::emplace_back(const UCSanObject &obj) { + push_back(obj); + return items[len - 1]; +} + +void ObjectMap::init(uint32_t initial_cap) { + cap = initial_cap; + count = 0; + uptr alloc_size = RoundUpTo(cap * sizeof(ObjectMapEntry), GetPageSizeCached()); + entries = (ObjectMapEntry *)MmapOrDie(alloc_size, "ObjectMap"); + // MmapOrDie returns zero-filled, so occupied=0 for all entries +} + +void ObjectMap::destroy() { + if (entries) { + UnmapOrDie(entries, RoundUpTo(cap * sizeof(ObjectMapEntry), GetPageSizeCached())); + entries = nullptr; + } + cap = count = 0; +} + +uint32_t ObjectMap::hash(uint32_t parent_id, int32_t offset) const { + uint64_t key = ((uint64_t)parent_id << 32) | (uint32_t)offset; + key *= 2654435769ULL; + return (uint32_t)(key & (cap - 1)); +} + +uint32_t *ObjectMap::find_val(uint32_t parent_id, int32_t offset) { + if (!entries) return nullptr; + uint32_t h = hash(parent_id, offset); + for (uint32_t i = 0; i < cap; i++) { + uint32_t idx = (h + i) & (cap - 1); + if (!entries[idx].occupied) return nullptr; + if (entries[idx].key.first == parent_id && entries[idx].key.second == offset) + return &entries[idx].value; + } + return nullptr; +} + +void ObjectMap::insert(uint32_t parent_id, int32_t offset, uint32_t value) { + if (count * 10 > cap * 7) grow(); + uint32_t h = hash(parent_id, offset); + for (;;) { + uint32_t idx = h & (cap - 1); + if (!entries[idx].occupied) { + entries[idx].key = {parent_id, offset}; + entries[idx].value = value; + entries[idx].occupied = 1; + count++; + return; + } + if (entries[idx].key.first == parent_id && entries[idx].key.second == offset) { + entries[idx].value = value; + return; + } + h++; + } +} + +void ObjectMap::grow() { + uint32_t old_cap = cap; + ObjectMapEntry *old = entries; + cap *= 2; + uptr alloc_size = RoundUpTo(cap * sizeof(ObjectMapEntry), GetPageSizeCached()); + entries = (ObjectMapEntry *)MmapOrDie(alloc_size, "ObjectMap"); + count = 0; + for (uint32_t i = 0; i < old_cap; i++) { + if (old[i].occupied) + insert(old[i].key.first, old[i].key.second, old[i].value); + } + UnmapOrDie(old, RoundUpTo(old_cap * sizeof(ObjectMapEntry), GetPageSizeCached())); +} + +//===----------------------------------------------------------------------===// +// UCSan Input Implementation +//===----------------------------------------------------------------------===// + +namespace __ucsan { + +// NOTE: ucsan_input uses C-style initialization to avoid constructor ordering issues +// with preinit_array. + +static void ucsan_init_input_struct() { + internal_memset(ucsan_tainted.filename, 0, sizeof(ucsan_tainted.filename)); + ucsan_tainted.fd = -1; + ucsan_tainted.offset = 0; + ucsan_tainted.offset_label = 0; + ucsan_tainted.label = 0; + ucsan_tainted.size = 0; + ucsan_tainted.is_stdin = 0; + ucsan_tainted.is_utmp = 0; + ucsan_tainted.buf = nullptr; + ucsan_tainted.buf_size = 0; + // Allocate C++ objects with constructors dynamically to avoid ordering issues + if (!ucsan_tainted.objects) { + ucsan_tainted.objects = new ObjectStorage(); + } + if (!ucsan_tainted.obj_map) { + ucsan_tainted.obj_map = new ObjectMap(); + ucsan_tainted.obj_map->init(); + } + ucsan_tainted.arg_used.val_dont_use = 0; +} + +static void ucsan_fini_input_struct() { + if (ucsan_tainted.objects) { + ucsan_tainted.objects->destroy(); + delete ucsan_tainted.objects; + ucsan_tainted.objects = nullptr; + } + if (ucsan_tainted.obj_map) { + ucsan_tainted.obj_map->destroy(); + delete ucsan_tainted.obj_map; + ucsan_tainted.obj_map = nullptr; + } +} + +char* ucsan_input::dump() { + uint64_t length = sizeof(uint64_t); + for (uint32_t i = 0; i < objects->size(); i++) { + length += objects->at(i).data.size() + sizeof(uint64_t) + sizeof(uint32_t); + } + // Include metadata: origin info per object (obj_id, offset, target_offset) + length += objects->size() * 3 * sizeof(uint64_t); + + char *ret = static_cast(malloc(length)); + char *pos = ret; + + // Write object count + uint64_t tmp = objects->size(); + internal_memcpy(pos, &tmp, sizeof(uint64_t)); + pos += sizeof(uint64_t); + + // Write offsets + for (uint32_t i = 0; i < objects->size(); i++) { + uint32_t tmp32 = objects->at(i).offset; + internal_memcpy(pos, &tmp32, sizeof(uint32_t)); + pos += sizeof(uint32_t); + } + + // Write sizes + for (uint32_t i = 0; i < objects->size(); i++) { + tmp = objects->at(i).data.size(); + internal_memcpy(pos, &tmp, sizeof(uint64_t)); + pos += sizeof(uint64_t); + } + + // Write data + for (uint32_t i = 0; i < objects->size(); i++) { + tmp = objects->at(i).data.size(); + internal_memcpy(pos, objects->at(i).data.data(), tmp); + pos += tmp; + } + + // Write metadata: origin info (obj_id, offset, target_offset) per object + for (uint32_t i = 0; i < objects->size(); i++) { + tmp = objects->at(i).origin.obj_id; + internal_memcpy(pos, &tmp, sizeof(uint64_t)); + pos += sizeof(uint64_t); + tmp = (uint64_t)(int64_t)objects->at(i).origin.offset; + internal_memcpy(pos, &tmp, sizeof(uint64_t)); + pos += sizeof(uint64_t); + tmp = objects->at(i).origin.target_offset; + internal_memcpy(pos, &tmp, sizeof(uint64_t)); + pos += sizeof(uint64_t); + } + + buf_size = length; + return ret; +} + +uint64_t ucsan_input::load(const char *buf, size_t file_size) { + // Parse deseralized objects from buffer: + // [object_cnt: uint64_t] + // [offset1: uint32_t, offset2: uint32_t, ...] (object_cnt entries) + // [size1: uint64_t, size2: uint64_t, ...] (object_cnt entries) + // [data1, data2, ...] (variable length) + // [metadata: obj_id + offset pairs] (optional) + + uint64_t *header = (uint64_t *)buf; + uint64_t object_cnt = *header; + header++; + + uint32_t *offsets = (uint32_t *)header; + uint64_t *sizes = (uint64_t *)(offsets + object_cnt); + + const char *cursor = buf + sizeof(uint64_t) + // object_cnt + sizeof(uint32_t) * object_cnt + // offset entries + sizeof(uint64_t) * object_cnt; // size entries + + // Initialize objects storage + if (!ucsan_tainted.objects) { + ucsan_tainted.objects = new ObjectStorage(); + } + ucsan_tainted.objects->clear(); + + for (uint64_t i = 0; i < object_cnt; ++i) { + UCSanObject obj; + obj.offset = offsets[i]; + obj.data.init(); + obj.data.assign((const unsigned char *)cursor, (uint32_t)sizes[i]); + obj.origin = {0, 0, 0}; + ucsan_tainted.objects->push_back(obj); + cursor += sizes[i]; + } + + // Load metadata if present + if (cursor < buf + file_size) { + // Determine metadata entry size: 3 uint64_t (with target_offset) or 2 (legacy) + size_t remaining = (buf + file_size) - cursor; + size_t meta_entry_size = (remaining >= object_cnt * sizeof(uint64_t) * 3) + ? sizeof(uint64_t) * 3 : sizeof(uint64_t) * 2; + for (uint64_t i = 0; i < object_cnt && cursor + meta_entry_size <= buf + file_size; ++i) { + ucsan_tainted.objects->at(i).origin.obj_id = *(uint64_t *)cursor; + cursor += sizeof(uint64_t); + ucsan_tainted.objects->at(i).origin.offset = *(uint64_t *)cursor; + cursor += sizeof(uint64_t); + if (meta_entry_size == sizeof(uint64_t) * 3) { + uint64_t target_offset = *(uint64_t *)cursor; + cursor += sizeof(uint64_t); + ucsan_tainted.objects->at(i).origin.target_offset = (uint32_t)target_offset; + // Override obj.offset with target_offset from metadata if non-zero + if (target_offset > 0) { + ucsan_tainted.objects->at(i).offset = (uint32_t)target_offset; + } + } + } + } + + // Build object map for lookup + if (object_cnt > 1) { + ucsan_tainted.build_obj_map(); + } + + ucsan_tainted.buf = buf; + ucsan_tainted.buf_size = file_size; + + return object_cnt; +} + +void ucsan_input::build_obj_map() { + uint32_t object_id = 1; + UCSAN_OUT("build_obj_map: objects->size()=%lu\n", (uint64_t)objects->size()); + + for (; object_id < objects->size(); ++object_id) { + ObjectOrigin& object_meta = objects->at(object_id).origin; + UCSAN_OUT(" obj[%u]: from.obj_id=%u, from.offset=%d\n", + object_id, object_meta.obj_id, object_meta.offset); + obj_map->insert(object_meta.obj_id, object_meta.offset, object_id); + } + + atomic_store(&__ucsan_inited_objects, object_id, memory_order_relaxed); + UCSAN_OUT("build_obj_map done: __ucsan_inited_objects=%u, obj_map->size()=%lu\n", + object_id, (uint64_t)obj_map->size()); + + // Debug: dump obj_map contents + UCSAN_OUT("obj_map contents:\n"); + for (uint32_t i = 0; i < obj_map->cap; i++) { + if (obj_map->entries[i].occupied) + UCSAN_OUT(" {%u, %d} -> %u\n", obj_map->entries[i].key.first, + obj_map->entries[i].key.second, obj_map->entries[i].value); + } +} + +//===----------------------------------------------------------------------===// +// Label Management +//===----------------------------------------------------------------------===// + +ucsan_label_info* get_label_info(ucsan_label label) { + return &__ucsan_label_info[label]; +} + +void check_label(ucsan_label label) { + if (label >= __alloca_stack_top) { + Report("FATAL: UCSan: exhausted labels\n"); + Die(); + } +} + +ucsan_label allocate_label() { + ucsan_label label = atomic_fetch_add(&__ucsan_last_label, 1, memory_order_relaxed) + 1; + check_label(label); + return label; +} + +//===----------------------------------------------------------------------===// +// Memory Helpers +//===----------------------------------------------------------------------===// + +void* customized_malloc(uint64_t size) { + if (size > ucsan_object_size_limit()) { + UCSAN_OUT("Object size too large: %lu\n", size); + exit(exit_reason::REASON_OBJ_OOB); + } + uint64_t gap = Max((uint64_t)UCSAN_SAFE_GAP, UCSAN_ROUNDUPGAP(size)); + uint64_t crafted_size = size + (gap << 1); + char* ret = (char*)malloc(crafted_size) + gap; + return (void*)ret; +} + +static inline bool is_writeable(void *p) { + int fd = open("/dev/zero", O_RDONLY); + if (fd < 0) return false; + bool writeable = read(fd, p, 1) == 1; + close(fd); + return writeable; +} + +//===----------------------------------------------------------------------===// +// Object Lookup +//===----------------------------------------------------------------------===// + +UCSanObject& lookup_object(ucsan_label label, uint64_t offset, void* return_addr, uint32_t *ret_object_id, uint32_t type_id, uint32_t size) { + if (label) { + ucsan_label_info *label_info = get_label_info(label); + uint32_t object_id = 0; + // Use wider types for range checking before casting + uint64_t parent_obj_id_64 = 0; + int64_t offset_in_parent_64 = 0; + + uint16_t op = label_info->common.op; + if (op == OP_NONE) { + // Byte info - look up by (object_id, offset) + ucsan_byte_info *byte = &label_info->byte; + UCSAN_OUT("lookup_object: label=%u, op=BYTE, obj_id=%u, offset=%ld\n", + label, byte->object_id, byte->offset); + parent_obj_id_64 = byte->object_id; + offset_in_parent_64 = byte->offset; + } else if (op == OP_EXTERNAL) { + // External pointer - look up by (parent_obj_id, offset) + // This matches dfsan's {op2.i, op1.i} pattern + ucsan_ptr_info *ptr = &label_info->ptr; + parent_obj_id_64 = ptr->obj_label; + offset_in_parent_64 = (int64_t)(uint64_t)ptr->pseudo_base; + UCSAN_OUT("lookup_object: label=%u, op=EXTERNAL, parent_obj=%lu, offset=%ld\n", + label, parent_obj_id_64, offset_in_parent_64); + UCSAN_OUT(" obj_map size=%lu, searching for {%lu, %ld}\n", + (uint64_t)ucsan_tainted.obj_map->size(), parent_obj_id_64, offset_in_parent_64); + } else { + UCSAN_OUT("WARNING: unexpected op=%u label=%u\n", op, label); + } + + // Range check before casting to narrower types for map key + if (parent_obj_id_64 > UINT32_MAX) { + UCSAN_OUT("WARNING: UCSan: parent_obj_id too large: %lu\n", parent_obj_id_64); + goto out_default; + } + if (offset_in_parent_64 > INT32_MAX || offset_in_parent_64 < INT32_MIN) { + UCSAN_OUT("WARNING: UCSan: offset_in_parent out of range: %ld\n", offset_in_parent_64); + goto out_default; + } + + uint32_t parent_obj_id = (uint32_t)parent_obj_id_64; + int32_t offset_in_parent = (int32_t)offset_in_parent_64; + + uint32_t *found_val = ucsan_tainted.obj_map->find_val(parent_obj_id, offset_in_parent); + if (found_val) { + object_id = *found_val; + UCSAN_OUT(" found object_id=%u\n", object_id); + } else { + UCSAN_OUT(" NOT found in obj_map\n"); + } + + bool created = false; + if (object_id == 0) { + // Allocate next object ID + // Counter starts at 1, so first allocation gets ID 1 (object 0 is super object) + // fetch_add returns old value, then increments for next allocation + object_id = atomic_fetch_add(&__ucsan_inited_objects, 1, memory_order_relaxed); + created = true; + } + + if (object_id >= ucsan_tainted.objects->size()) { + UCSanObject new_obj; + new_obj.offset = 0; + new_obj.data.init(); + new_obj.origin = {0, 0}; + ucsan_tainted.objects->push_back(new_obj); + } + + // Trace lazy init event if enabled + if (created && ucsan_flags().trace_object) { + // Pack object_id (lower 32 bits) and parent_obj_id (upper 32 bits) into uint64_t + uint64_t info = ((uint64_t)parent_obj_id << 32) | object_id; + __taint_trace_event_addr(label, EVENT_LAZY_INIT, info, return_addr, + (uint32_t)offset_in_parent); + // Send type/size binding for the newly created object + // result = object_id (lower 32) | size (upper 32), id = type_id + uint64_t bind_info = ((uint64_t)size << 32) | object_id; + __taint_trace_event_addr(label, EVENT_TYPE_BIND, bind_info, return_addr, type_id); + } + + if (ret_object_id) *ret_object_id = object_id; + return ucsan_tainted.objects->at(object_id); + } + +out_default: + + // Should not reach here with label == 0 + static UCSanObject empty_object; + return empty_object; +} + +//===----------------------------------------------------------------------===// +// Super Object Label Creation +//===----------------------------------------------------------------------===// + +object_info create_label_from_super_object(size_t size, bool is_pointer) { + uint64_t offset = atomic_fetch_add(&ucsan_tainted.arg_used, size, memory_order_relaxed); + ucsan_label label = allocate_label(); + + ucsan_label_info *label_info = get_label_info(label); + + if (is_pointer) { + // Create pointer info + // For lookup purposes, we store parent_obj_id in obj_label and offset in _padding + // This matches dfsan's approach where op2.i=parent_obj_id and op1.i=offset + ucsan_ptr_info *ptr = &label_info->ptr; + ptr->op = OP_EXTERNAL; + ptr->status = PTR_UNINITIALIZED; + ptr->obj_label = 0; // parent object ID (0 = super object) + ptr->pseudo_base = (void*)offset; // Repurpose to store offset for lookup + } else { + // Create byte info for super object (object 0) + ucsan_byte_info *byte = &label_info->byte; + byte->op = OP_NONE; + byte->object_id = 0; // super object + byte->offset = (int64_t)offset; + } + + return {label, offset}; +} + +} // namespace __ucsan + +//===----------------------------------------------------------------------===// +// Core UCSan Functions +//===----------------------------------------------------------------------===// + +using namespace __ucsan; + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_check_pointer(void* p, ucsan_label label, size_t size, bool dereferencing, uint32_t type_id) { + UCSAN_OUT("%p: label: %d, size: %zu, dereferencing: %d, type_id: %u\n", p, label, size, dereferencing, type_id); + __ucsan_null_deref_flag = nullptr; + + // Global variable handling + if (in_data_section(p)) { + // if label is not zero, perform bounds check + if (label >= UCSAN_CONST_OFFSET) { + ucsan_label_info *label_info = get_label_info(label); + if (label_info->common.op != OP_ALLOCA) { + UCSAN_OUT("WARNING: global variable with non-alloca label %u\n", label); + } else { + // Check for out-of-bounds access to global variable + ucsan_obj_info *obj = to_obj_info(label_info); + void *base = obj->real_ptr; + uint64_t base_addr = (uint64_t)base; + uint64_t ptr_addr = (uint64_t)p; + uint64_t upper_bound = (uint64_t)obj->upper_bound; + uint64_t offset = ptr_addr - base_addr; + if (ucsan_flags().trace_bounds && + (ptr_addr < base_addr || + offset > upper_bound || + size > upper_bound - offset)) { + UCSAN_OUT("ERROR: Out-of-bounds access to global variable @%p, label=%u, access size=%lu, object size=%u\n", + p, label, size, obj->upper_bound); + if (dereferencing) { + __taint_trace_event_addr(label, EVENT_OOB, 0, __builtin_return_address(0), (uint32_t)obj->upper_bound); + exit(exit_reason::EVENT_OOB); + } + } + } + } else if (size > ucsan_object_size_limit()) { + UCSAN_OUT("WARNING: global variable size too large: %lu\n", size); + size = ucsan_object_size_limit(); + } + // Symbolize global variable on first access (label == 0) + ucsan_label *shadow = ucsan_shadow_for(p); + if (*shadow == 0 && dereferencing) { + // only symbolize on deref + bool sent_data = false; + uint64_t first_byte_offset = 0; + for (uint64_t i = 0; i < size; ++i) { + void *byte_addr = (void*)((uint64_t)p + i); + shadow = ucsan_shadow_for(byte_addr); + auto ret = create_label_from_super_object(1, false); + *shadow = ret.label; + if (i == 0) first_byte_offset = ret.offset; + UCSAN_OUT("Symbolize global variable byte @%p with %u from offset %lu\n", + ((char*)p+i), ret.label, ret.offset); + + // Bridge to SymSan: create symbolic label for this byte and set shadow + dfsan_label symsan_label = __taint_create_label(0, ret.offset, 1); + __taint_set_label(symsan_label, byte_addr, 1); + + // Initialize from seed data if available + if (ucsan_tainted.objects->size() && ret.offset < ucsan_tainted.objects->at(0).data.size()) { + *((char*)p + i) = ucsan_tainted.objects->at(0).data.at(ret.offset); + } else if (!in_bss_section(p) && *((char*)p + i) != 0) { + sent_data = true; + } + } + + // Trace global variable usage + if (ucsan_flags().trace_object) { + ucsan_label first_label = *(ucsan_shadow_for(p)); + ucsan_label_info *info = get_label_info(first_label); + // globals are from super object, so object_id is always 0 + int64_t offset = to_byte_info(info)->offset; + if (offset > INT32_MAX || offset < INT32_MIN) { + UCSAN_OUT("WARNING: global variable offset (%ld) too large\n", offset); + } else { + __taint_trace_event_addr(*(ucsan_shadow_for(p)), EVENT_USAGE_CITE, type_id, + __builtin_return_address(0), (uint32_t)offset); + } + } + + // Send initial data if needed + if (sent_data) { + UCSAN_OUT("Sent init data for global variable @%p\n", p); + __taint_trace_global_var(0, first_byte_offset, size, p); + } + } + return p; + } + + // if non-global yet without label, can't do much + if (label < UCSAN_CONST_OFFSET) { + // Null dereference checking + if (dereferencing && ucsan_flags().checker_nullderef) { + if ((uint64_t)p < size || (uint64_t)p < ucsan_object_size_limit()) { + // Trace null dereference event + __taint_trace_event_addr(label, EVENT_NULL_DEREF, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_NULL_DEREF); + } else { + __ucsan_null_deref_flag = __builtin_return_address(0); + } + } + return p; + } + + // Use-before-initialization check + UCSAN_OUT("UBI check: label=%u, kUninitializedLabel=%u, equal=%d, dereferencing=%d\n", + label, kUninitializedLabel, (label == kUninitializedLabel), dereferencing); + if (label == kUninitializedLabel) { + UCSAN_OUT("UBI detected!\n"); + if (dereferencing) { + // Trace use-before-initialization event + __taint_trace_event_addr(label, EVENT_UBI, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_UBI); + } + return p; + } + + ucsan_label_info *label_info = get_label_info(label); + + UCSAN_OUT("label %u: op=%u, trace_bounds=%d\n", label, label_info->common.op, ucsan_flags().trace_bounds); + + // Bounds checking when trace_bounds is enabled + if (ucsan_flags().trace_bounds) { + // Check for stack UAF - accessing freed stack allocations + // Heap labels: UCSAN_CONST_OFFSET to __ucsan_last_label (growing upward) + // Stack labels: __alloca_stack_top to __alloca_stack_bottom (growing downward from top) + // Freed stack region: __ucsan_last_label < label < __alloca_stack_top + ucsan_label last_heap_label = atomic_load(&__ucsan_last_label, memory_order_relaxed); + if (label > last_heap_label && label < __alloca_stack_top) { + // Label is in the freed stack region + UCSAN_OUT("ERROR: Stack UAF detected ptr %p, label = %u (last_heap = %u, stack_top = %u)\n", + p, label, last_heap_label, __alloca_stack_top); + if (dereferencing) { + __taint_trace_event_addr(label, EVENT_UAF, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_UAF); + } + return p; + } + + // Check for heap UAF - freed heap memory + if (label_info->common.op == OP_FREE) { + UCSAN_OUT("ERROR: Heap UAF detected ptr %p, label = %u\n", p, label); + if (dereferencing) { + __taint_trace_event_addr(label, EVENT_UAF, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_UAF); + } + return p; + } + + // Check for OOB - heap/stack allocated memory with bounds tracking + if (label_info->common.op == OP_ALLOCA) { + ucsan_obj_info *obj = to_obj_info(label_info); + void *base = obj->real_ptr; + uint64_t lower = (uint64_t)base - obj->lower_bound; + uint64_t upper = (uint64_t)base + obj->upper_bound; + + UCSAN_OUT("OOB check: ptr=%p, base=%p, lower=%p, upper=%p, size=%zu\n", + p, base, (void*)lower, (void*)upper, size); + + uint64_t ptr_addr = (uint64_t)p; + uint64_t offset = ptr_addr - lower; + uint64_t object_size = upper - lower; + if (ptr_addr < lower || offset >= object_size || + size > object_size - offset) { + UCSAN_OUT("ERROR: OOB access ptr %p, lower = %p, upper = %p, size = %zu, label = %u, deref=%d\n", + p, (void*)lower, (void*)upper, size, label, dereferencing); + if (dereferencing) { + __taint_trace_event_addr(label, EVENT_OOB, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_OOB); + } + } + return p; + } + } + + ucsan_ptr_info *ptr_info = to_ptr_info(label_info); + + // Mark as external if needed - create a new pointer label for non-pointer types + if (ptr_info->op != OP_EXTERNAL && ptr_info->op != OP_NONE) { + ucsan_label new_label = allocate_label(); + ucsan_label_info *new_info = get_label_info(new_label); + ucsan_ptr_info *new_ptr = to_ptr_info(new_info); + new_ptr->op = OP_BITCAST; + new_ptr->status = PTR_UNINITIALIZED; + new_ptr->obj_label = UCSAN_CONST_LABEL; + new_ptr->pseudo_base = nullptr; + ptr_info = new_ptr; + } + + if (ptr_info->op == OP_NONE || ptr_info->op == OP_BITCAST) { + ptr_info->op = OP_EXTERNAL; + ptr_info->status = PTR_UNINITIALIZED; + } + + // Handle uninitialized pointer - lazy initialization + if (ptr_info->status == PTR_UNINITIALIZED) { + UCSAN_OUT("check pointer: uninit pointer\n"); + + uint32_t object_id; + void *return_addr = __builtin_return_address(0); + auto& obj = lookup_object(label, 0, return_addr, &object_id, type_id, (uint32_t)size); + size_t object_size = obj.data.size(); + + UCSAN_OUT("Find object_id: %u (size = %zu) for label %u, addr %p\n", + object_id, object_size, label, return_addr); + + if (type_id == 0 && size == 0) { + // Typeless with unknown length (e.g. strcmp): ensure at least 1 byte '\0' + // If seed data populated the object, use that size instead + if (obj.data.size() == 0) { + obj.data.resize(1); + obj.data[0] = '\0'; + } + } else if (obj.data.size() < size) { + obj.data.resize(size); + } + object_size = obj.data.size(); + + // Create object label + ucsan_label obj_label = allocate_label(); + + void* np = customized_malloc(object_size); + internal_memcpy(np, obj.data.data(), object_size); + + ucsan_obj_info *obj_label_info = to_obj_info(get_label_info(obj_label)); + obj_label_info->lower_bound = obj.offset; + obj_label_info->upper_bound = (uint32_t)object_size - obj.offset; + obj_label_info->real_ptr = (char*)np + obj.offset; + obj_label_info->op = OP_RESERVED_OBJ; + obj_label_info->type_id = (uint16_t)type_id; + obj_label_info->object_id = object_id; + + ptr_info->obj_label = obj_label; + ptr_info->pseudo_base = p; // Store original pseudo pointer in ptr_info + + // Create labels for each byte in the object and set up shadow memory + for (uint64_t offset = 0; offset < object_size; ++offset) { + ucsan_label byte_label = allocate_label(); + + ucsan_label_info *byte_info = get_label_info(byte_label); + ucsan_byte_info *byte = to_byte_info(byte_info); + byte->op = OP_NONE; + byte->object_id = object_id; + byte->offset = offset - obj.offset; + + void *nptr = (void*)((uint64_t)np + offset); + ucsan_label *shadow = ucsan_shadow_for(nptr); + *shadow = byte_label; + + // Bridge to SymSan: use 0-based offset so offsets stay consistent + // across seed re-executions regardless of obj.offset growth + dfsan_label symsan_label = __taint_create_label(object_id, offset, 1); + __taint_set_label(symsan_label, nptr, 1); + } + + ptr_info->status = PTR_INITIALIZED; + return (char*)np + obj.offset; + + } else if (ptr_info->status == PTR_INITIALIZED) { + UCSAN_OUT("check pointer: pointer allocated\n"); + + ucsan_label obj_label = ptr_info->obj_label; + ucsan_obj_info *obj_label_info = to_obj_info(get_label_info(obj_label)); + uint32_t object_id = obj_label_info->object_id; + + void *obj_base = obj_label_info->real_ptr; + int64_t pseudo_base = (int64_t)ptr_info->pseudo_base; // Read from ptr_info + int64_t desired_offset = (int64_t)p - pseudo_base; + + if (size > ucsan_object_size_limit()) { + UCSAN_OUT("Object access size too large: %zu\n", size); + exit(exit_reason::REASON_OBJ_OOB); + } + + int64_t access_size = (int64_t)size; + if (desired_offset > INT64_MAX - access_size) { + UCSAN_OUT("Object access offset overflow: offset=%ld, size=%zu\n", + desired_offset, size); + exit(exit_reason::REASON_OBJ_OOB); + } + int64_t desired_end = desired_offset + access_size; + + if (ucsan_flags().no_upcast && desired_offset < 0) { + UCSAN_OUT("Upcast disallowed by no_upcast flag\n"); + exit(exit_reason::EVENT_OOB_UPCAST); + } + + // Fast path: within current bounds + int64_t lower_bound = -(int64_t)obj_label_info->lower_bound; + int64_t upper_bound = (int64_t)obj_label_info->upper_bound; + + if (lower_bound <= desired_offset && desired_end <= upper_bound) { + void* target = (void*)((int64_t)obj_base + desired_offset); + UCSAN_OUT("Fast path: returning %p\n", target); + return target; + } + + /* + remarkable offsets in an ascending order: + --------------------- + 0 -> new lowest offset + --------------------- + extended_lower -> extended bytes in the lower bound + --------------------- + new_lower_bound -> the new lower bound, the data between extended_lower + and new_lower_bound is copied from the original object + [new_size] -> e.g. array access to the previous element, like a[-1] + --------------------- + original_size -> the original size, the data from new_lower_bound to + this is copied from the original object + --------------------- + [new_size] -> e.g. use container_of access to a larger object + --------------------- + */ + + // Slow path: need to enlarge object + uint64_t original_size = obj_label_info->lower_bound + obj_label_info->upper_bound; + uint64_t extended_lower = 0; + uint64_t new_lower_bound = obj_label_info->lower_bound; + + if (desired_offset < 0 && desired_offset < -(int64_t)new_lower_bound) { + extended_lower = -desired_offset - new_lower_bound; + new_lower_bound = -desired_offset; + UCSAN_OUT("Extended lower bound: %lu, new lower bound: %lu\n", + extended_lower, new_lower_bound); + if (ucsan_flags().trace_object) { + __taint_trace_event_addr(label, EVENT_EXTENSION, object_id, + __builtin_return_address(0), + (uint32_t)new_lower_bound); + { + uint64_t bind_info = ((uint64_t)size << 32) | object_id; + __taint_trace_event_addr(label, EVENT_TYPE_BIND, bind_info, + __builtin_return_address(0), type_id); + } + } + } + + uint64_t new_size = new_lower_bound + Max(desired_end, + (int64_t)obj_label_info->upper_bound); + + if (new_size > ucsan_object_size_limit()) { + UCSAN_OUT("Object size too large: %lu\n", new_size); + exit(exit_reason::REASON_OBJ_OOB); + } + + void *np = customized_malloc(new_size); + UCSAN_OUT("Enlarged object: np=%p, new_size=%lu\n", np, new_size); + + // Copy and extend shadow memory + // Padding the left lower bounds with new labels + for (uint64_t offset = 0; offset < extended_lower; ++offset) { + ucsan_label byte_label = allocate_label(); + + ucsan_label_info *byte_info = get_label_info(byte_label); + ucsan_byte_info *byte = to_byte_info(byte_info); + byte->op = OP_NONE; + byte->object_id = object_id; + byte->offset = (int64_t)offset - (int64_t)new_lower_bound; + + void *nptr = (void*)((uint64_t)np + offset); + *ucsan_shadow_for(nptr) = byte_label; + + // Bridge to SymSan: use signed offset (negative for backward extension) + dfsan_label symsan_label = __taint_create_label(object_id, byte->offset, 1); + __taint_set_label(symsan_label, nptr, 1); + } + + // Copy shadow memory from original object + void *dst_addr = (void*)((int64_t)np + extended_lower); + ucsan_label *shadow = ucsan_shadow_for(dst_addr); + internal_memcpy(shadow, ucsan_shadow_for(obj_base), original_size * sizeof(ucsan_label)); + + // Bridge to SymSan: copy shadow memory + __taint_copy_shadow(dst_addr, obj_base, original_size); + + // Copy original data + internal_memcpy((char*)np + extended_lower, obj_base, original_size); + + // Padding the right upper bounds with new labels + for (uint64_t offset = extended_lower + original_size; offset < new_size; ++offset) { + ucsan_label byte_label = allocate_label(); + + ucsan_label_info *byte_info = get_label_info(byte_label); + ucsan_byte_info *byte = to_byte_info(byte_info); + byte->op = OP_NONE; + byte->object_id = object_id; + byte->offset = offset - new_lower_bound; + + void *nptr = (void*)((uint64_t)np + offset); + *ucsan_shadow_for(nptr) = byte_label; + + // Bridge to SymSan: offset relative to original pointer base + dfsan_label symsan_label = __taint_create_label(object_id, byte->offset, 1); + __taint_set_label(symsan_label, nptr, 1); + } + + // Update object info + obj_label_info->lower_bound = (uint32_t)new_lower_bound; + obj_label_info->upper_bound = (uint32_t)(new_size - new_lower_bound); + obj_label_info->real_ptr = (char*)np + new_lower_bound; + + return (void*)((int64_t)np + desired_offset); + } + + UCSAN_OUT("Unknown pointer state: %d\n", ptr_info->status); + return nullptr; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_uncheck_pointer(void* real_ptr, ucsan_label label) { + // Inverse of ucsan_check_pointer for an already-initialized UC pointer. + // ucsan_check_pointer maps pseudo -> real as: + // real = obj_base + (pseudo - pseudo_base) + // so the inverse maps real -> pseudo as: + // pseudo = pseudo_base + (real - obj_base) + // This is pure pointer arithmetic (like phys_to_virt): no object is + // allocated, provisioned, or grown here. + if (real_ptr == nullptr || label < UCSAN_CONST_OFFSET) + return real_ptr; + + ucsan_label_info *info = get_label_info(label); + // Only translate genuine UC (external/pseudo) pointers. File-mode pointers + // (no ptr_info) pass through unchanged. + if (info->common.op != OP_EXTERNAL) + return real_ptr; + + ucsan_ptr_info *ptr_info = to_ptr_info(info); + // We only uncheck a pointer that was previously resolved by + // ucsan_check_pointer, so it must already have a backing object. + if (ptr_info->status != PTR_INITIALIZED) { + Report("BUG: ucsan_uncheck_pointer on uninitialized UC pointer " + "(label=%u, status=%d)\n", label, ptr_info->status); + return real_ptr; + } + + ucsan_obj_info *obj_label_info = to_obj_info(get_label_info(ptr_info->obj_label)); + int64_t offset = (int64_t)real_ptr - (int64_t)obj_label_info->real_ptr; + void *pseudo = (void*)((int64_t)ptr_info->pseudo_base + offset); + UCSAN_OUT("uncheck_pointer: real=%p label=%u -> pseudo=%p (obj_base=%p, pseudo_base=%p, off=%ld)\n", + real_ptr, label, pseudo, obj_label_info->real_ptr, ptr_info->pseudo_base, offset); + return pseudo; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_check_ptr_arg(ucsan_label *label, uint32_t arg_index, void* ret_addr) { + if (label[0] == kUninitializedLabel) { + // Trace use-before-initialization event + __taint_trace_event_addr(label[0], EVENT_UBI, arg_index, ret_addr, 0); + exit(exit_reason::EVENT_UBI); + } +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_check_ubi(ucsan_label label) { + UCSAN_OUT("ucsan_check_ubi: label=%u, kUninitializedLabel=%u\n", label, kUninitializedLabel); + if (label == kUninitializedLabel) { + UCSAN_OUT("UBI detected in scalar load!\n"); + // Trace use-before-initialization event + __taint_trace_event_addr(label, EVENT_UBI, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_UBI); + } +} + +// Check if a copy operation can overflow the destination buffer. +// Phase 1: if src object already exceeds dst bound, exit with OOB. +// Phase 2: if src type is variable-size (type_id==0), ask thoroupy to +// enlarge the src object past dst bound for the next run. +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_check_copy_bounds(void *dst, ucsan_label dst_label, + void *src, ucsan_label src_label, + uint64_t gv_dst_bound) { + UCSAN_OUT("ucsan_check_copy_bounds: dst=%p (label=%u), src=%p (label=%u), gv_dst_bound=%lu\n", + dst, dst_label, src, src_label, gv_dst_bound); + + // Get dst bound: from alloca label or from compile-time GV size + uint64_t dst_bound = 0; + if (dst_label >= UCSAN_CONST_OFFSET) { + ucsan_label_info *dst_info = get_label_info(dst_label); + if (dst_info->common.op == OP_ALLOCA) { + ucsan_obj_info *dst_obj = to_obj_info(dst_info); + dst_bound = dst_obj->upper_bound; + } + } else dst_bound = gv_dst_bound; + if (dst_bound == 0) return; + + // src must be under-constrained (OP_EXTERNAL) + if (src_label < UCSAN_CONST_OFFSET) return; + ucsan_label_info *src_info = get_label_info(src_label); + if (src_info->common.op != OP_EXTERNAL) return; + + ucsan_ptr_info *src_ptr = to_ptr_info(src_info); + if (src_ptr->status != PTR_INITIALIZED) return; + + // Get src object info + ucsan_label src_obj_label = src_ptr->obj_label; + if (src_obj_label < UCSAN_CONST_OFFSET) return; + ucsan_label_info *src_obj_info = get_label_info(src_obj_label); + if (src_obj_info->common.op != OP_RESERVED_OBJ) return; + + ucsan_obj_info *src_obj = to_obj_info(src_obj_info); + uint32_t src_object_id = src_obj->object_id; + uint64_t src_size = src_obj->lower_bound + src_obj->upper_bound; + + UCSAN_OUT(" dst_bound=%lu, src_object_id=%u, src_size=%lu, src_type_id=%u\n", + dst_bound, src_object_id, src_size, src_obj->type_id); + + // Phase 1: src already exceeds dst bound → OOB witness + if (src_size > dst_bound) { + UCSAN_OUT(" COPY OVERFLOW: src_size=%lu > dst_bound=%lu\n", src_size, dst_bound); + __taint_trace_event_addr(src_label, EVENT_OOB, 0, + __builtin_return_address(0), 0); + exit(exit_reason::EVENT_OOB); + } + + // Phase 2: request enlargement for variable-size or char-typed objects + if (src_obj->type_id == 0 || src_obj->type_id == 2) { + __taint_trace_event_addr(src_label, EVENT_COPY_OVERFLOW, src_object_id, + __builtin_return_address(0), (uint32_t)dst_bound); + } +} + +// Combine two labels for binary operations +// Returns kUninitializedLabel if either operand is uninitialized +// Warns if a pointer label is used in arithmetic +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +ucsan_label ucsan_combine_label(ucsan_label l1, ucsan_label l2) { + // If either label is uninitialized, propagate it + if (l1 == kUninitializedLabel || l2 == kUninitializedLabel) { + UCSAN_OUT("ucsan_combine_label: propagating kUninitializedLabel (l1=%u, l2=%u)\n", l1, l2); + return kUninitializedLabel; + } + + // Check if a pointer is used with a non-zero non-pointer value + // Pointer with zero is fine (comparison or null-offset arithmetic) + bool l1_is_ptr = false, l2_is_ptr = false; + if (l1 != 0) { + ucsan_label_info *info = get_label_info(l1); + l1_is_ptr = info && (info->common.op == OP_EXTERNAL || info->common.op == OP_ALLOCA); + if (info->common.op == OP_NONE && !l2) { + // l1 is under-constrained and l2 is concrete, allow it + return l1; + } + } + if (l2 != 0) { + ucsan_label_info *info = get_label_info(l2); + l2_is_ptr = info && (info->common.op == OP_EXTERNAL || info->common.op == OP_ALLOCA); + if (info->common.op == OP_NONE && !l1) { + // l2 is under-constrained and l1 is concrete, allow it + return l2; + } + } + // Warn only if pointer is combined with non-zero non-pointer + if (l1_is_ptr && l2 != 0 && !l2_is_ptr) { + UCSAN_OUT("WARNING: pointer label %u used in binary operation with non-pointer %u\n", l1, l2); + } + if (l2_is_ptr && l1 != 0 && !l1_is_ptr) { + UCSAN_OUT("WARNING: pointer label %u used in binary operation with non-pointer %u\n", l2, l1); + } + + // Return 0 - we don't track symbolic expressions in standalone mode + return 0; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +ucsan_label ucsan_load_pointer_shadow(ucsan_label *ls, uint64_t n, bool is_pointer, + void *concrete_addr) { + if (ls == nullptr) return 0; + + ucsan_label label0 = ls[0]; + + UCSAN_OUT("load_pointer_shadow: *label(%p)=%d is_pointer=%d, size=%lu\n", + ls, label0, is_pointer, n); + + // Allow loading kUninitializedLabel for UBI detection + if (label0 == kUninitializedLabel) { + return label0; + } + + ucsan_label_info *label_info = get_label_info(label0); + + if (is_pointer) { + if (label0 >= UCSAN_CONST_OFFSET && label_info->common.op == OP_NONE) { + // Check if the concrete pointer value points to a symbolize_input object + if (__symbolize_addr_map_inited && concrete_addr && n == sizeof(void*)) { + void *concrete_val = *(void **)concrete_addr; + if (concrete_val) { + uint32_t target_id, target_offset; + ucsan_label target_ptr_label; + if (__symbolize_addr_map.find(concrete_val, &target_id, &target_offset, + &target_ptr_label)) { + UCSAN_OUT("load_pointer_shadow: symbolize_input hit: target_id=%u, " + "target_offset=%u, ptr_label=%u\n", + target_id, target_offset, target_ptr_label); + + // Populate obj_map for lookup_object to find later + ucsan_byte_info *byte = to_byte_info(label_info); + uint32_t parent_id = byte->object_id; + int32_t parent_offset = (int32_t)byte->offset; + ucsan_tainted.obj_map->insert(parent_id, parent_offset, target_id); + + // Set target's origin + if (target_id < ucsan_tainted.objects->size()) { + ucsan_tainted.objects->at(target_id).origin = { + parent_id, parent_offset, target_offset + }; + } + + // Return the saved ptr_label instead of converting to OP_EXTERNAL + if (target_ptr_label) { + return target_ptr_label; + } + } + } + } + + // Default: convert byte label to pointer label + ucsan_ptr_info *ptr = to_ptr_info(label_info); + ptr->op = OP_EXTERNAL; + ptr->status = PTR_UNINITIALIZED; + } + return label0; + } + + // Load pointer but not as pointer type + if (label0 >= UCSAN_CONST_OFFSET && + (label_info->common.op == OP_EXTERNAL || label_info->common.op == OP_NONE || + label_info->common.op == OP_ALLOCA || label_info->common.op == OP_FREE) && + n == sizeof(void*)) { + return label0; + } + + return 0; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_store_pointer_shadow(ucsan_label l, ucsan_label *ls, uint64_t n) { + UCSAN_OUT("store_pointer_shadow: label=%d, n=%lu, ls=%p\n", l, n, ls); + + if (l == 0 || l == kUninitializedLabel) { + for (uint64_t i = 0; i < n; ++i) ls[i] = l; + return; + } + + ucsan_label_info *label_info = get_label_info(l); + if (label_info->common.op == OP_EXTERNAL || + label_info->common.op == OP_NONE || + label_info->common.op == OP_ALLOCA || + label_info->common.op == OP_FREE) { + assert(n == sizeof(void*)); + for (uint64_t i = 0; i < n; ++i) ls[i] = l; + return; + } + + Report("WARNING: storing non-pointer label %d (op=%d) shadow\n", l, label_info->common.op); +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_set_label(ucsan_label label, void *addr, uint64_t size) { + UCSAN_OUT("ucsan_set_label: label=%d, addr=%p, size=%lu\n", label, addr, size); + + // Get shadow address for the memory region + ucsan_label *shadow = ucsan_shadow_for(addr); + + // Set the label for each byte in the region + for (uint64_t i = 0; i < size; ++i) { + shadow[i] = label; + } +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_set_label_for_args(uint32_t index, uint32_t size_in_bits, uint8_t is_pointer, uint64_t given) { + UCSAN_OUT("set_label_for_args: index=%u, size_in_bits=%u, is_pointer=%d\n", + index, size_in_bits, is_pointer); + + size_t size_in_bytes = (size_in_bits + 7) / 8; + auto ret = create_label_from_super_object(size_in_bytes, is_pointer); + + UCSAN_OUT("set label %u for index %u\n", ret.label, index); + + // Handle truncation if size_in_bits < size_in_bytes * 8 + // Note: UCSan doesn't have union operations, truncation is implicit + // The label represents the full byte(s), actual bit size is tracked elsewhere + + __ucsan_arg_tls[index] = ret.label; + + // Bridge to SymSan: create symbolic label and set arg TLS + // object_id=0 for function arguments, offset from super object + dfsan_label symsan_label = __taint_create_label(0, ret.offset, size_in_bytes); + __taint_set_arg_tls(index, symsan_label, size_in_bits); + + uint64_t last_offset = ret.offset; + + // Ensure objects is allocated + if (!ucsan_tainted.objects) { + ucsan_tainted.objects = new ObjectStorage(); + } + + if (ucsan_tainted.objects->size() && + last_offset + size_in_bytes <= ucsan_tainted.objects->at(0).data.size()) { + uint64_t result = *((uint64_t*)(ucsan_tainted.objects->at(0).data.data() + last_offset)); + UCSAN_OUT("Returning: 0x%lx\n", result); + return (void*)result; + } + + if (!ucsan_tainted.objects->size()) ucsan_tainted.objects->resize(1); + ucsan_tainted.objects->at(0).data.resize(last_offset + size_in_bytes); + + return (void*)given; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void __ucsan_symbolize_input(void *ptr, size_t size, int id) { + UCSAN_OUT("symbolize_input: ptr=%p, size=%zu, id=%d\n", ptr, size, id); + + if (!ptr || size == 0) return; + + uint32_t object_id = (uint32_t)id; // direct 1-based mapping + + // Read the pointer's label from arg_tls (stored by UCSanPass at call site) + ucsan_label ptr_label = __ucsan_arg_tls[0]; + UCSAN_OUT("symbolize_input: ptr_label=%u\n", ptr_label); + + // Ensure objects storage is allocated and large enough + if (!ucsan_tainted.objects) { + ucsan_tainted.objects = new ObjectStorage(); + } + while (ucsan_tainted.objects->size() <= object_id) { + UCSanObject empty; + empty.offset = 0; + empty.data.init(); + empty.origin = {0, 0, 0}; + ucsan_tainted.objects->push_back(empty); + } + + // Bump __ucsan_inited_objects to avoid collision with lazy alloc + uint32_t current = atomic_load(&__ucsan_inited_objects, memory_order_relaxed); + while (current <= object_id) { + if (atomic_compare_exchange_weak(&__ucsan_inited_objects, + ¤t, object_id + 1, memory_order_relaxed)) + break; + } + + // Ensure obj_map is initialized + if (!ucsan_tainted.obj_map) { + ucsan_tainted.obj_map = new ObjectMap(); + ucsan_tainted.obj_map->init(); + } + + // Seed data handling + UCSanObject &obj = ucsan_tainted.objects->at(object_id); + bool first_run = (obj.data.size() == 0); + if (!first_run && obj.data.size() >= (uint32_t)size) { + // Re-run: solver provided data, overwrite ptr + internal_memcpy(ptr, obj.data.data(), size); + UCSAN_OUT("symbolize_input: re-run, overwrote ptr with solver data\n"); + } else { + // First run: snapshot concrete bytes as initial seed + obj.data.assign((const unsigned char *)ptr, (uint32_t)size); + UCSAN_OUT("symbolize_input: first run, snapshotted %zu bytes\n", size); + } + + // Initialize addr range map if needed + if (!__symbolize_addr_map_inited) { + __symbolize_addr_map.init(); + __symbolize_addr_map_inited = true; + } + + // Register in addr range map for lazy pointer relationship discovery + __symbolize_addr_map.insert(ptr, size, object_id, ptr_label); + + // Label every byte OP_NONE — same pattern as ucsan_check_pointer:861-879 + ucsan_label *shadow = ucsan_shadow_for(ptr); + for (size_t offset = 0; offset < size; ++offset) { + ucsan_label byte_label = allocate_label(); + + ucsan_label_info *byte_info = get_label_info(byte_label); + ucsan_byte_info *byte = to_byte_info(byte_info); + byte->op = OP_NONE; + byte->object_id = object_id; + byte->offset = offset; + + shadow[offset] = byte_label; + + // Bridge to SymSan + void *byte_addr = (void *)((uintptr_t)ptr + offset); + dfsan_label symsan_label = __taint_create_label(object_id, offset, 1); + __taint_set_label(symsan_label, byte_addr, 1); + } + + // Trace object usage event so thoroupy knows about this object + if (ucsan_flags().trace_object) { + ucsan_label first_label = *(ucsan_shadow_for(ptr)); + uint64_t cite_info = ((uint64_t)size << 32) | object_id; + __taint_trace_event_addr(first_label, EVENT_USAGE_CITE, cite_info, + __builtin_return_address(0), 0); + } + + // Send initial concrete data to solver (first run only) + if (first_run) { + __taint_trace_global_var(object_id, 0, size, ptr); + } + + UCSAN_OUT("symbolize_input: done, labeled %zu bytes for object %u\n", size, object_id); +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +ucsan_label ucsan_resign_shadow(void *ptr, ucsan_label *orig_label, uint64_t n, void *ret_addr) { + ucsan_label label_for_ptr = *orig_label; + + if (ptr == nullptr && label_for_ptr == 0) { + UCSAN_OUT("Resigning null pointer, skip\n"); + return UCSAN_CONST_LABEL; + } + + if (label_for_ptr != 0) { + ucsan_label_info *orig = get_label_info(label_for_ptr); + + if (orig->common.op == OP_ALLOCA) { + // Handle alloca case - symbolize uninitialized bytes in the allocated region + UCSAN_OUT("Resign alloca object\n"); + ucsan_obj_info *obj_info = to_obj_info(orig); + + // Calculate bounds: real_ptr is the base, lower_bound is bytes before, upper_bound is bytes after + char *lower = (char*)obj_info->real_ptr - obj_info->lower_bound; + char *upper = (char*)obj_info->real_ptr + obj_info->upper_bound; + size_t alloca_size = upper - lower; + if (alloca_size > ucsan_object_size_limit()) { + UCSAN_OUT("WARNING: alloca resign size %zu exceeds limit %lu, capping\n", + alloca_size, ucsan_object_size_limit()); + upper = lower + ucsan_object_size_limit(); + } + ucsan_label *lp = ucsan_shadow_for(lower); + ucsan_label *le = ucsan_shadow_for(upper); + char *obj_ptr = lower; + + for (; lp < le; ++lp, ++obj_ptr) { + if (*lp == kUninitializedLabel) { + // Allocate a byte from super object + auto ret = create_label_from_super_object(1, false); + UCSAN_OUT("resign alloca ret: %u %lu\n", ret.label, ret.offset); + *lp = ret.label; + + // Bridge to SymSan: create symbolic label for this byte + dfsan_label symsan_label = __taint_create_label(0, ret.offset, 1); + __taint_set_label(symsan_label, obj_ptr, 1); + + if (ucsan_tainted.objects->size() && ret.offset < ucsan_tainted.objects->at(0).data.size()) { + UCSAN_OUT("resign alloca super object: %u\n", ucsan_tainted.objects->at(0).data.at(ret.offset)); + *obj_ptr = ucsan_tainted.objects->at(0).data.at(ret.offset); + } else { + *obj_ptr = 0; + } + } + } + return label_for_ptr; + } else if (orig->common.op == OP_FREE) { + // Freed memory - return as-is for UAF detection + UCSAN_OUT("Resign freed object\n"); + return label_for_ptr; + } else { + // Mark as external pointer + ucsan_ptr_info *ptr_info = to_ptr_info(orig); + ptr_info->op = OP_EXTERNAL; + ptr_info->status = PTR_UNINITIALIZED; + } + return label_for_ptr; + + } else { + if (!is_writeable(ptr)) { + return UCSAN_CONST_LABEL; + } + + if (ret_addr == nullptr) { + ret_addr = __builtin_return_address(0); + } + + UCSAN_OUT("Concrete external object: p=%p, size=%lu\n", ptr, n); + + // allocate from super object + auto ret = create_label_from_super_object(n, true); + uint32_t object_id = 0; + auto& object = lookup_object(ret.label, 0, ret_addr, &object_id); + + // Set up shadow memory for object bytes + ucsan_label *shadow = ucsan_shadow_for(ptr); + for (uptr i = 0; i < n; ++i) { + ucsan_label next_label = allocate_label(); + + shadow[i] = next_label; + ucsan_label_info *new_label_info = get_label_info(next_label); + ucsan_byte_info *byte = to_byte_info(new_label_info); + byte->op = OP_NONE; + byte->object_id = object_id; + byte->offset = i; + + // Bridge to SymSan: create symbolic label for this byte and set shadow + void *byte_addr = (void*)((uint64_t)ptr + i); + dfsan_label symsan_label = __taint_create_label(object_id, byte->offset, 1); + __taint_set_label(symsan_label, byte_addr, 1); + } + + if (object.data.size() < n) object.data.resize(n); + + for (uptr i = 0; i < n; ++i) { + ((uint8_t*)ptr)[i] = object.data[i]; + } + + return ret.label; + } +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_wrap_retval(uint64_t size, ucsan_label *ret_label, bool is_ptr, void* ret_addr) { + uint64_t bits = size; + size = (size + 7) >> 3; // Round up to bytes + + if (ret_addr == nullptr) ret_addr = __builtin_return_address(0); + + // Treat pointer-sized non-pointers as pointers for lazy init + bool treat_as_ptr = is_ptr || size == sizeof(void*); + auto lbl = create_label_from_super_object(size, treat_as_ptr); + ucsan_label label = lbl.label; + uint64_t last_offset = lbl.offset; + + UCSAN_OUT("Ret object created: label=%u, offset=%lu\n", label, last_offset); + + // Note: create_label_from_super_object already initializes the label_info + // with the correct op (OP_EXTERNAL for pointers, OP_NONE for bytes) + // and stores offset/object_id in byte_info or sets up ptr_info + + // TODO: Handle truncation if bits < size * 8 + + *ret_label = label; + + // Bridge to SymSan: create symbolic label and set retval TLS at correct index + // Calculate index from pointer offset into __ucsan_retval_tls + uint32_t retval_tls_index = (uint32_t)(ret_label - __ucsan_retval_tls); + dfsan_label symsan_label = __taint_create_label(0, last_offset, (uint32_t)size); + __taint_set_retval_tls(retval_tls_index, symsan_label, (uint32_t)bits); + + if (ucsan_tainted.objects->size() && + last_offset + size <= ucsan_tainted.objects->at(0).data.size()) { + UCSAN_OUT("offset (%lu + %lu) in the seed\n", last_offset, size); + for (uptr i = 0; i < size; ++i) { + __ucsan_wrapped_return_tls[i] = ucsan_tainted.objects->at(0).data[last_offset + i]; + } + } else { + for (uptr i = 0; i < size; ++i) { + __ucsan_wrapped_return_tls[i] = 0; + } + } + + return (void*)__ucsan_wrapped_return_tls; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +ucsan_label ucsan_trace_alloca(uint64_t size, uint64_t elem_size, uint64_t addr) { + // Track stack allocation bounds using stack-based label allocation + // Labels are allocated from __alloca_stack_top (top of label space, growing downward) + // and automatically freed when the function exits via __taint_pop_stack_frame + // + // size: array size (number of elements) + // elem_size: size of each element in bytes + // addr: address of the stack allocation + + if (ucsan_flags().trace_bounds) { + uint64_t total_size = size * elem_size; + void *ptr = (void*)addr; + + // Allocate label from stack top (grows downward) + __alloca_stack_top -= 1; + ucsan_label label = __alloca_stack_top; + + UCSAN_OUT("ucsan_trace_alloca: label=%u, base=%p, size=%lu, elem_size=%lu, total=%lu\n", + label, ptr, size, elem_size, total_size); + + ucsan_label_info *info = get_label_info(label); + ucsan_obj_info *obj = to_obj_info(info); + + // Set up bounds tracking for stack allocation + obj->op = OP_ALLOCA; + obj->type_id = 0; + obj->object_id = 0; // Not tracked in objects array (stack allocation) + obj->real_ptr = ptr; + obj->lower_bound = 0; // No bytes before base + obj->upper_bound = (uint32_t)total_size; // Total size in bytes + + UCSAN_OUT(" created stack label %u: ptr=%p, lower=0, upper=%u\n", + label, ptr, obj->upper_bound); + + // Set shadow memory to kUninitializedLabel for UBI detection + ucsan_label *shadow = ucsan_shadow_for(ptr); + for (uptr i = 0; i < total_size; i++) { + shadow[i] = kUninitializedLabel; + } + + return label; + } else { + return 0; + } +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +ucsan_label ucsan_trace_global(uint64_t addr, uint64_t size) { + if (!ucsan_flags().trace_bounds || addr == 0 || size == 0) + return 0; + + if (size > UINT32_MAX) { + UCSAN_OUT("WARNING: global bounds size %lu exceeds limit %u, capping\n", + size, UINT32_MAX); + size = UINT32_MAX; + } + + void *ptr = (void *)addr; + if (!__global_bounds_map_inited) { + __global_bounds_map.init(); + __global_bounds_map_inited = true; + } + + uint32_t object_id = 0; + uint32_t offset = 0; + ucsan_label label = 0; + if (__global_bounds_map.find(ptr, &object_id, &offset, &label)) { + UCSAN_OUT("ucsan_trace_global: reuse label=%u, base=%p, size=%lu\n", + label, ptr, size); + return label; + } + + label = allocate_label(); + UCSAN_OUT("ucsan_trace_global: label=%u, base=%p, size=%lu\n", + label, ptr, size); + + ucsan_label_info *info = get_label_info(label); + ucsan_obj_info *obj = to_obj_info(info); + obj->op = OP_ALLOCA; + obj->type_id = 0; + obj->object_id = 0; + obj->real_ptr = ptr; + obj->lower_bound = 0; + obj->upper_bound = (uint32_t)size; + + __global_bounds_map.insert(ptr, (size_t)size, 0, label); + return label; +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_push_stack_frame() { + // Save current stack top when entering a function + // This allows automatic cleanup of stack allocation labels on function exit + if (ucsan_flags().trace_bounds) { + if (__current_saved_stack_index < UCSAN_MAX_SAVED_STACK_ENTRIES) { + __saved_alloca_stack_top[++__current_saved_stack_index] = __alloca_stack_top; + UCSAN_OUT("ucsan_push_stack_frame: saved index=%d, stack_top=%u\n", + __current_saved_stack_index, __alloca_stack_top); + } else { + Report("WARNING: UCSan: stack frame save index overflow\n"); + } + } +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_pop_stack_frame() { + // Restore stack top when exiting a function + // This automatically frees all stack allocation labels created in this function + if (ucsan_flags().trace_bounds) { + if (__current_saved_stack_index > 0) { + __alloca_stack_top = __saved_alloca_stack_top[__current_saved_stack_index--]; + UCSAN_OUT("ucsan_pop_stack_frame: restored index=%d, stack_top=%u\n", + __current_saved_stack_index, __alloca_stack_top); + } else { + Report("WARNING: UCSan: stack frame save index underflow\n"); + } + } +} + +/// scan shadow for ptr labels and recursively mark +void ucsan_scan_mark_reachable(void *base, uint64_t size) { + + UCSAN_OUT("ucsan_scan_mark_reachable: base=%p, size=%lu\n", base, size); + + ucsan_label *shadow = ucsan_shadow_for(base); + for (uptr i = 0; i < size; ++i) { + // ptr labels can occupy multiple bytes, but once marked reachable, + // the duplicated ones will be skipped + ucsan_label label = shadow[i]; + if (label >= UCSAN_CONST_OFFSET) { + ucsan_label_info *info = get_label_info(label); + if (info->common.op == OP_EXTERNAL) { + // external pointer, find the real object and continue scanning + // Mark this object as reachable by setting op to OP_FREE + info->common.op = OP_FREE; + ucsan_ptr_info *ptr_info = to_ptr_info(info); + if (ptr_info->status == PTR_INITIALIZED) { + ucsan_label obj_label = ptr_info->obj_label; + if (obj_label >= UCSAN_CONST_OFFSET) { + ucsan_label_info *obj_info = get_label_info(obj_label); + if (obj_info->common.op == OP_RESERVED_OBJ) { + ucsan_obj_info *obj = to_obj_info(obj_info); + // object spans [real_ptr - lower_bound, real_ptr + upper_bound) + ucsan_scan_mark_reachable((char*)obj->real_ptr - obj->lower_bound, + obj->lower_bound + obj->upper_bound); + } else { + UCSAN_OUT("WARNING: expected obj label %u, got op %d\n", + obj_label, obj_info->common.op); + } + } else { + UCSAN_OUT("WARNING: expected obj label %u, got non-pointer label\n", obj_label); + } + } // else uninitialized pointer, skip + } else if (info->common.op == OP_ALLOCA) { + // explicitly allocated object, mark as reachable and continue scanning + info->common.op = OP_FREE; + ucsan_obj_info *obj = to_obj_info(info); + // object spans [real_ptr - lower_bound, real_ptr + upper_bound) + ucsan_scan_mark_reachable((char*)obj->real_ptr - obj->lower_bound, + obj->lower_bound + obj->upper_bound); + } + } + } +} + +/// mark and sweep style detector for unreachable memory regions +void ucsan_scan_shadow_for_leaks() { + // This function can be called at program exit to detect memory leaks + // by scanning the shadow memory for objects that are still live (not freed) + // yet unreachable. + + UCSAN_OUT("ucsan_scan_shadow_for_leaks: scanning shadow memory for leaks\n"); + uint32_t last_label = atomic_load(&__ucsan_last_label, memory_order_relaxed); + + // step 1: mark reachable objects as freed + // first set of roots: global variables + if (__global_bounds_map_inited) { + for (auto &entry : __global_bounds_map) { + ucsan_label label = entry.ptr_label; + void *base = (void*)entry.start; + ucsan_label_info *info = get_label_info(label); + UCSAN_OUT("global var: label=%u, base=%p, size=%lu, op=%d\n", + label, base, entry.size, info->common.op); + if (info->common.op != OP_ALLOCA) { + UCSAN_OUT("WARNING: unexpected global var label op %d\n", info->common.op); + continue; + } + info->common.op = OP_FREE; // mark as freed to indicate reachable + ucsan_scan_mark_reachable(base, entry.size); + } + } + + // second set of roots: pseudo pointers in the union table + for (unsigned i = 0; i <= last_label; ++i) { + ucsan_label_info *info = get_label_info(i); + if (info->common.op == OP_EXTERNAL) { + // external pointer, find the real object and continue scanning + info->common.op = OP_FREE; // mark as freed to indicate reachable + ucsan_ptr_info *ptr_info = to_ptr_info(info); + if (ptr_info->status == PTR_INITIALIZED) { + ucsan_label obj_label = ptr_info->obj_label; + if (obj_label >= UCSAN_CONST_OFFSET) { + ucsan_label_info *obj_info = get_label_info(obj_label); + if (obj_info->common.op == OP_RESERVED_OBJ) { + ucsan_obj_info *obj = to_obj_info(obj_info); + // object spans [real_ptr - lower_bound, real_ptr + upper_bound) + ucsan_scan_mark_reachable((char*)obj->real_ptr - obj->lower_bound, + obj->lower_bound + obj->upper_bound); + } else { + UCSAN_OUT("WARNING: expected obj label %u, got op %d\n", + obj_label, obj_info->common.op); + } + } else { + UCSAN_OUT("WARNING: expected obj label %u, got non-pointer label\n", obj_label); + } + } + } + } + + // third set of root: return value labels in TLS + { + // FIXME: assume scalar return value for now + ucsan_label retval_label = __ucsan_retval_tls[0]; + if (retval_label >= UCSAN_CONST_OFFSET) { + ucsan_label_info *info = get_label_info(retval_label); + if (info->common.op == OP_ALLOCA) { + // external pointer, find the real object and continue scanning + info->common.op = OP_FREE; // mark as freed to indicate reachable + ucsan_obj_info *obj = to_obj_info(info); + // object spans [real_ptr - lower_bound, real_ptr + upper_bound) + ucsan_scan_mark_reachable((char*)obj->real_ptr - obj->lower_bound, + obj->lower_bound + obj->upper_bound); + } + } + } + + // step 2: report any remaining non-freed objects as leaks + for (unsigned i = 0; i <= last_label; ++i) { + ucsan_label_info *info = get_label_info(i); + if (info->common.op == OP_ALLOCA) { + ucsan_obj_info *obj = to_obj_info(info); + UCSAN_OUT("ERROR: MEMLEAK detected: label=%u, ptr=%p, size=%u\n", + i, obj->real_ptr, obj->lower_bound + obj->upper_bound); + __taint_trace_event_addr(i, EVENT_MEMLEAK, 0, obj->real_ptr, 0); + } + } +} + + +//===----------------------------------------------------------------------===// +// Initialization +//===----------------------------------------------------------------------===// + +void UCSanFlags::SetDefaults() { +#define UCSAN_FLAG(Type, Name, DefaultValue, Description) Name = DefaultValue; +#include "ucsan_flags.inc" +#undef UCSAN_FLAG +} + +static void RegisterUCSanFlags(FlagParser *parser, UCSanFlags *f) { +#define UCSAN_FLAG(Type, Name, DefaultValue, Description) \ + RegisterFlag(parser, #Name, Description, &f->Name); +#include "ucsan_flags.inc" +#undef UCSAN_FLAG +} + +static void ucsan_parse_flags() { + // Set common flags defaults + SetCommonFlagsDefaults(); + + // Set UCSan-specific flag defaults + ucsan_flags_data.SetDefaults(); + + // Create flag parser and register flags + FlagParser parser; + RegisterCommonFlags(&parser); + RegisterUCSanFlags(&parser, &ucsan_flags_data); + + // Parse UCSAN_OPTIONS environment variable + parser.ParseString(GetEnv("UCSAN_OPTIONS")); + + // Initialize common flags + InitializeCommonFlags(); + + // Report unrecognized flags if verbose + if (Verbosity()) ReportUnrecognizedFlags(); + + // Print help if requested + if (common_flags()->help) parser.PrintFlagDescriptions(); + + // Set debug flag + ucsan_debug = ucsan_flags_data.debug; +} + +static void ucsan_init_shadow_memory() { + // Map UCSan shadow memory region + // Shadow: kShadowBase (0x480000000000) to kUnionTableAddr (0x680000000000) + uptr shadow_size = kUnionTableAddr - kShadowBase; + if (!MmapFixedSuperNoReserve(kShadowBase, shadow_size)) { + Printf("FATAL: UCSan: failed to map shadow memory\n"); + Die(); + } + + // Map UCSan union table + if (!MmapFixedSuperNoReserve(kUnionTableAddr, kUnionTableSize)) { + Printf("FATAL: UCSan: failed to map union table\n"); + Die(); + } + + // Initialize label info pointer + __ucsan_label_info = (ucsan_label_info *)kUnionTableAddr; + + // Initialize constant label (label 0) + internal_memset(&__ucsan_label_info[UCSAN_CONST_LABEL], 0, sizeof(ucsan_label_info)); + + // Initialize label counter (start from 1, 0 is CONST_LABEL) + atomic_store(&__ucsan_last_label, UCSAN_CONST_OFFSET, memory_order_relaxed); + + // Initialize object counter + atomic_store(&__ucsan_inited_objects, 1, memory_order_relaxed); + + // Initialize alloca stack for bounds tracking + uptr num_labels = kUnionTableSize / sizeof(ucsan_label_info); + __alloca_stack_top = __alloca_stack_bottom = (ucsan_label)(num_labels - 2); +} + +static void ucsan_fini_internal() { + UCSAN_OUT("UCSan runtime finalized\n"); + + if (ucsan_flags().check_memleak) { + ucsan_scan_shadow_for_leaks(); + } + + if (__global_bounds_map_inited) { + __global_bounds_map.destroy(); + __global_bounds_map_inited = false; + } + if (__symbolize_addr_map_inited) { + __symbolize_addr_map.destroy(); + __symbolize_addr_map_inited = false; + } + + // Clean up input struct + ucsan_fini_input_struct(); +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_init() { + // Initialize input struct first (C-style init for preinit_array safety) + ucsan_init_input_struct(); + + // Parse options + ucsan_parse_flags(); + + // Initialize shadow memory and label table + ucsan_init_shadow_memory(); + + // Load input file if specified + if (ucsan_flags_data.input_file && ucsan_flags_data.input_file[0] != '\0') { + ucsan_init_input(ucsan_flags_data.input_file); + } + + // Initialize UCSan solver (thoroupy backend) + InitializeUCSanSolver(); + + // Register cleanup callback + Atexit(ucsan_fini_internal); + AddDieCallback(ucsan_fini_internal); + + UCSAN_OUT("UCSan runtime initialized\n"); + UCSAN_OUT(" Shadow: 0x%lx - 0x%lx\n", (uint64_t)kShadowBase, (uint64_t)kUnionTableAddr); + UCSAN_OUT(" UnionTable: 0x%lx (size: 0x%lx)\n", (uint64_t)kUnionTableAddr, (uint64_t)kUnionTableSize); + UCSAN_OUT(" Max labels: %lu\n", (uint64_t)(kUnionTableSize / sizeof(ucsan_label_info))); +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_fini() { + ucsan_fini_internal(); +} + +extern "C" SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_init_input(const char *filename) { + UCSAN_OUT("ucsan_init_input: %s\n", filename); + + if (!filename || internal_strlen(filename) == 0) { + return; + } + + // Map input file + uptr file_size; + char *buf = (char *)MapFileToMemory(filename, &file_size); + if (!buf) { + Report("WARNING: UCSan: failed to map input file %s\n", filename); + return; + } + + uint64_t object_cnt = ucsan_tainted.load(buf, file_size); + + UCSAN_OUT("Loaded %lu objects from %s\n", object_cnt, filename); +} + +//===----------------------------------------------------------------------===// +// Preinit Array for Automatic Initialization +//===----------------------------------------------------------------------===// + +// Internal init function with standard signature for preinit_array +static void ucsan_init_internal(int argc, char **argv, char **envp) { + // When linked with dfsan, ensure it is initialized before ucsan + // (ucsan's fork server must not fork before dfsan_init completes) + __dfsan_ensure_init(argc, argv, envp); + ucsan_init(); +} + +#if SANITIZER_CAN_USE_PREINIT_ARRAY +__attribute__((section(".preinit_array"), used)) +static void (*ucsan_init_ptr)(int, char **, char **) = ucsan_init_internal; +#endif + +//===----------------------------------------------------------------------===// +// Weak Definitions for Trace Callbacks +//===----------------------------------------------------------------------===// + +// These are weak definitions that allow the solver (e.g., thoroupy.cpp) to +// override them with actual implementations. If no solver is linked, these +// empty stubs are used. + +extern "C" { + +// Weak definition for event tracing +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_event_addr, + uint32_t, uint32_t, uint64_t, void*, + uint32_t) {} + +// Weak definition for global variable tracing +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_global_var, + uint32_t, uint64_t, uint64_t, void*) {} + +// Weak definition for basic block tracing +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_trace_bb, + uint32_t, uint32_t) {} + +// Weak definition for UCSan solver initialization +SANITIZER_INTERFACE_WEAK_DEF(void, InitializeUCSanSolver, void) {} + +// Weak definition for dfsan init ordering (overridden by dfsan.cpp when linked) +SANITIZER_INTERFACE_WEAK_DEF(void, __dfsan_ensure_init, int, char**, char**) {} + +//===----------------------------------------------------------------------===// +// SymSan Bridge - Weak Stubs +//===----------------------------------------------------------------------===// +// These weak definitions are overridden by SymSan's dfsan.cpp when linked. +// When UCSan runs standalone, these are no-ops. + +// Create a SymSan label for input bytes +// @param object_id: input source identifier (fd, socket, etc.) +// @param offset: byte offset within the source +// @param size_in_bytes: size of the input in bytes +// @return: SymSan label (0 when standalone) +SANITIZER_INTERFACE_WEAK_DEF(dfsan_label, __taint_create_label, uint32_t, uint64_t, uint32_t) { + return 0; // CONST_LABEL when standalone +} + +// Set SymSan arg TLS entry +// @param index: argument index +// @param label: SymSan label to set +// @param size_in_bits: size of argument in bits (for truncation) +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_set_arg_tls, uint32_t, dfsan_label, uint32_t) {} + +// Set SymSan retval TLS entry +// @param index: index into retval TLS array (for struct elements) +// @param label: SymSan label to set +// @param size_in_bits: size of return value in bits (for truncation) +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_set_retval_tls, uint32_t, dfsan_label, uint32_t) {} + +// Set SymSan shadow memory for a region +// @param label: SymSan label to set +// @param addr: memory address +// @param size: size in bytes +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_set_label, dfsan_label, void*, uint64_t) {} + +// Copy SymSan shadow memory from src to dst +// @param dst: destination address +// @param src: source address +// @param size: size in bytes +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_copy_shadow, void*, void*, uint64_t) {} + +// Move SymSan shadow memory from src to dst (handles overlapping regions) +// @param dst: destination address +// @param src: source address +// @param size: size in bytes +SANITIZER_INTERFACE_WEAK_DEF(void, __taint_move_shadow, void*, void*, uint64_t) {} + +// Get or create an Alloca bounds label for a pointer +// @param ptr: pointer (NULL to always create new) +// @param lower: lower bound address +// @param upper: upper bound address +// @return: Alloca label (0 when standalone) +SANITIZER_INTERFACE_WEAK_DEF(dfsan_label, __taint_get_ptr_bounds_label, void*, uint64_t, uint64_t) { + return 0; +} + +// Extend a SymSan label to a wider bit width via ZExt or SExt. +// @param label: input label (typically narrow, e.g. 8-bit byte label) +// @param sign_extend: true for SExt, false for ZExt +// @param new_size_in_bits: target bit width (must be > current size) +// @return: extended label (0 when standalone, or unchanged label if input is 0) +SANITIZER_INTERFACE_WEAK_DEF(dfsan_label, __taint_extend_label, dfsan_label, bool, uint16_t) { + return 0; +} + +} // extern "C" diff --git a/runtime/dfsan/ucsan.h b/runtime/dfsan/ucsan.h new file mode 100644 index 00000000..e8024be1 --- /dev/null +++ b/runtime/dfsan/ucsan.h @@ -0,0 +1,509 @@ +//===-- ucsan.h - UCSan Runtime Header -------------------------*- C++ -*-===// +// +// Under-Constrained Symbolic Sanitizer Runtime +// +// UCSan focuses on: +// - Pointer shadow tracking (object membership, bounds) +// - Lazy object initialization and management +// - Pseudo-pointer resolution +// +// UCSan uses 16-bit labels (separate from SymSan's 32-bit labels). +// This is separate from SymSan which tracks symbolic expressions. +// +//===----------------------------------------------------------------------===// + +#ifndef UCSAN_H +#define UCSAN_H + +#include "ucsan_platform.h" +#include "ucsan_exit_reason.h" +#include "sanitizer_common/sanitizer_internal_defs.h" +#include "sanitizer_common/sanitizer_common.h" +#include "sanitizer_common/sanitizer_atomic.h" + +#include +#include + +using __sanitizer::uptr; +using __sanitizer::atomic_uint16_t; +using __sanitizer::atomic_uint32_t; +using __sanitizer::atomic_uint64_t; +using __sanitizer::memory_order_relaxed; + +typedef uint32_t dfsan_label; + +//===----------------------------------------------------------------------===// +// UCSan Configuration +//===----------------------------------------------------------------------===// + +#define UCSAN_SAFE_GAP 0x1000 +#define UCSAN_ROUNDUPGAP(x) (((x) + UCSAN_SAFE_GAP - 1) & ~(UCSAN_SAFE_GAP - 1)) +#define UCSAN_OBJECT_SIZE_LIMIT (4096 * 10) +#define UCSAN_MAX_SAVED_STACK_ENTRIES 1024 + +namespace __ucsan { + +//===----------------------------------------------------------------------===// +// Special Labels (16-bit) +//===----------------------------------------------------------------------===// + +const ucsan_label kUninitializedLabel = (ucsan_label)0xFFFF; + +//===----------------------------------------------------------------------===// +// Pointer State +//===----------------------------------------------------------------------===// + +enum PTR_STATE : uint16_t { + PTR_UNINITIALIZED = 0, // uninitialized pointer (needs lazy init) + PTR_INITIALIZED = 1, // lazy-initialized pointer (has object) + PTR_ALLOCATED = 2, // allocated by malloc (may not be symbolized) + PTR_EXTERNAL = 3, // external pointer (from input) +}; + +//===----------------------------------------------------------------------===// +// UCSan Label Info Structures (designed for pointer/object tracking) +// +// All stored in the same union table, so must be the same size. +// Use 'op' field to distinguish type. +//===----------------------------------------------------------------------===// + +// Byte info - tracks individual bytes within objects (op == OP_NONE) +// Shadow memory points to these for each byte in a lazy-initialized object +struct ucsan_byte_info { + uint16_t op; // OP_NONE + uint16_t _reserved; + uint32_t object_id; // which object this byte belongs to + int64_t offset; // byte's offset within the object + uint64_t _padding; // padding to match other structs +} __attribute__((aligned(8))); + +// Pointer info - tracks symbolic/external pointers (op == OP_EXTERNAL) +// Offset is computed dynamically as (current_ptr - pseudo_base) +struct ucsan_ptr_info { + uint16_t op; // OP_EXTERNAL, OP_NONE, etc. + uint16_t status; // PTR_STATE + ucsan_label obj_label; // label pointing to backing ucsan_obj_info + uint16_t _reserved; + void* pseudo_base; // original symbolic pointer value + uint64_t _padding; // padding to match other structs +} __attribute__((aligned(8))); + +// Object info - tracks lazy-initialized objects (op == OP_RESERVED_OBJ) +struct ucsan_obj_info { + uint16_t op; // OP_RESERVED_OBJ or OP_ALLOCA + uint16_t type_id; // compile-time type ID from type table (0 = unknown) + uint32_t object_id; // unique object ID (index into objects array) + void* real_ptr; // actual memory (points to base within allocated region) + uint32_t lower_bound; // bytes before base (for container_of) + uint32_t upper_bound; // bytes after base +} __attribute__((aligned(8))); + +// Union of all label info types for the label table +union ucsan_label_info { + struct { uint16_t op; } common; // common header to check type + ucsan_byte_info byte; + ucsan_ptr_info ptr; + ucsan_obj_info obj; +}; + +// Static assertions - all info structs must be same size (24 bytes) +static_assert(sizeof(ucsan_byte_info) == 24, "ucsan_byte_info should be 24 bytes"); +static_assert(sizeof(ucsan_ptr_info) == 24, "ucsan_ptr_info should be 24 bytes"); +static_assert(sizeof(ucsan_obj_info) == 24, "ucsan_obj_info should be 24 bytes"); +static_assert(sizeof(ucsan_label_info) == 24, "ucsan_label_info should be 24 bytes"); + +// Size of each entry in the union table +static const size_t kLabelInfoSize = sizeof(ucsan_label_info); + +//===----------------------------------------------------------------------===// +// Label Info Accessors +//===----------------------------------------------------------------------===// + +// Convert label_info to specific types (for convenience) +inline ucsan_ptr_info* to_ptr_info(ucsan_label_info *info) { + return &info->ptr; +} + +inline ucsan_obj_info* to_obj_info(ucsan_label_info *info) { + return &info->obj; +} + +inline ucsan_byte_info* to_byte_info(ucsan_label_info *info) { + return &info->byte; +} + +//===----------------------------------------------------------------------===// +// Object Tracking Structures +//===----------------------------------------------------------------------===// + +// Metadata about where an object originated +struct ObjectOrigin { + uint32_t obj_id; // parent object ID (0 = super object) + int32_t offset; // offset within parent object + uint32_t target_offset; // offset within this object the pointer points to +}; + +//===----------------------------------------------------------------------===// +// Simple dynamic buffer (replaces std::vector) +//===----------------------------------------------------------------------===// +struct ByteBuffer { + unsigned char *buf; + uint32_t len; + uint32_t cap; + + void init() { buf = nullptr; len = 0; cap = 0; } + void destroy(); + uint32_t size() const { return len; } + unsigned char *data() { return buf; } + const unsigned char *data() const { return buf; } + unsigned char& operator[](uint32_t i) { return buf[i]; } + const unsigned char& operator[](uint32_t i) const { return buf[i]; } + unsigned char& at(uint32_t i) { return buf[i]; } + const unsigned char& at(uint32_t i) const { return buf[i]; } + void resize(uint32_t new_size); + void clear() { len = 0; } + void assign(const unsigned char *src, uint32_t n); +}; + +// An object tracked by UCSan +struct UCSanObject { + uint32_t offset; // offset within object (for sub-objects) + ByteBuffer data; // concrete data + ObjectOrigin origin; // where this object came from +}; + +//===----------------------------------------------------------------------===// +// Simple dynamic array for objects (replaces std::vector) +//===----------------------------------------------------------------------===// +struct ObjectStorage { + UCSanObject *items; + uint32_t len; + uint32_t cap; + + void init() { items = nullptr; len = 0; cap = 0; } + void destroy(); + uint32_t size() const { return len; } + UCSanObject& operator[](uint32_t i) { return items[i]; } + const UCSanObject& operator[](uint32_t i) const { return items[i]; } + UCSanObject& at(uint32_t i) { return items[i]; } + const UCSanObject& at(uint32_t i) const { return items[i]; } + void resize(uint32_t new_size); + void clear(); + void push_back(const UCSanObject &obj); + UCSanObject& emplace_back(const UCSanObject &obj); +}; + +//===----------------------------------------------------------------------===// +// Simple open-addressing hash map (replaces std::map) +//===----------------------------------------------------------------------===// +struct ObjectMapKey { + uint32_t first; // parent_obj_id + int32_t second; // offset +}; + +struct ObjectMapEntry { + ObjectMapKey key; + uint32_t value; + uint8_t occupied; +}; + +struct ObjectMap { + ObjectMapEntry *entries; + uint32_t cap; + uint32_t count; + + void init(uint32_t initial_cap = 64); + void destroy(); + uint32_t size() const { return count; } + + // Returns pointer to value if found, nullptr if not + uint32_t *find_val(uint32_t parent_id, int32_t offset); + + // Insert or update + void insert(uint32_t parent_id, int32_t offset, uint32_t value); + +private: + uint32_t hash(uint32_t parent_id, int32_t offset) const; + void grow(); +}; + +//===----------------------------------------------------------------------===// +// UCSan Taint Source (Input Tracking) +//===----------------------------------------------------------------------===// + +struct ucsan_input { + char filename[4096]; // input file path + int fd; // file descriptor + off_t offset; // current offset + ucsan_label offset_label; // label for offset (if symbolic) + ucsan_label label; // base label + off_t size; // input size + uint8_t is_stdin; // is stdin input + uint8_t is_utmp; // is utmp input + const char *buf; // mapped input buffer + uptr buf_size; // buffer size + + // Object management (same pattern as dfsan) + // All pointers to avoid C++ constructor ordering issues + ObjectStorage *objects; // pointer - allocated dynamically + ObjectMap *obj_map; // pointer - allocated dynamically + + // Super object offset tracking + atomic_uint64_t arg_used; // bytes used from super object + + // NOTE: No constructors - use ucsan_init_tainted() for C-style init + // to avoid preinit_array ordering issues + + // Serialize objects to buffer + char* dump(); + + // Deserialize objects from buffer + uint64_t load(const char *buf, size_t file_size); + + // Build object map from objects + void build_obj_map(); +}; + +// NOTE: ucsan_input initialization is handled internally by ucsan_init() +// via ucsan_init_input_struct() - a C-style init to avoid preinit_array issues + +//===----------------------------------------------------------------------===// +// UCSan Event Types (for tracing) +//===----------------------------------------------------------------------===// + +enum ucsan_event_type { + EVENT_LAZY_INIT = 100, + EVENT_USAGE_CITE = 101, + EVENT_EXTENSION = 102, + EVENT_ASSERTION = 103, + EVENT_TYPE_BIND = 104, + EVENT_COPY_OVERFLOW = 105, +}; + +enum ucsan_assertion_type { + ASSERTION_NONE = 0, + ASSERTION_NONE_SYMBOLIC = 1, + ASSERTION_ALLOCATED_FAILED = 2, + ASSERTION_ALLOCATED_SUCCESS = 3, + ASSERTION_FREED_FAILED = 4, + ASSERTION_FREED_SUCCESS = 5, + ASSERTION_INIT_FAILED = 6, + ASSERTION_INIT_SUCCESS = 7, + ASSERTION_COND_FAILED = 8, + ASSERTION_COND_SUCCESS = 9, + ASSUMPTION_CONTRACTION = 10, +}; + +//===----------------------------------------------------------------------===// +// UCSan Operations (subset of LLVM opcodes needed for pointer tracking) +//===----------------------------------------------------------------------===// + +enum ucsan_op : uint16_t { + OP_NONE = 0, + OP_LOAD = 32, + OP_STORE = 33, + OP_GEP = 34, + OP_BITCAST = 47, + OP_INTTOPTR = 50, + OP_PTRTOINT = 51, + + // UCSan-specific operations + OP_FREE = 100, + OP_ALLOCA = 101, + OP_EXTERNAL = 102, // external/symbolic pointer + OP_INITP = 103, // initialized pointer + OP_RESERVED_OBJ = 104, // reserved object label +}; + +//===----------------------------------------------------------------------===// +// UCSan Runtime State +//===----------------------------------------------------------------------===// + +// UCSan configuration flags (defined via ucsan_flags.inc) +struct UCSanFlags { +#define UCSAN_FLAG(Type, Name, DefaultValue, Description) Type Name; +#include "ucsan_flags.inc" +#undef UCSAN_FLAG + + void SetDefaults(); +}; + +// Global UCSan state +extern ucsan_input ucsan_tainted; +extern UCSanFlags ucsan_flags_data; +extern bool ucsan_debug; + +// Access flags +inline UCSanFlags& ucsan_flags() { + return ucsan_flags_data; +} + +inline uint64_t ucsan_object_size_limit() { + return ucsan_flags().max_obj_size > 0 + ? static_cast(ucsan_flags().max_obj_size) + : static_cast(UCSAN_OBJECT_SIZE_LIMIT); +} + +//===----------------------------------------------------------------------===// +// UCSan Label Management +//===----------------------------------------------------------------------===// + +// Get label info for a given label +ucsan_label_info* get_label_info(ucsan_label label); + +// Allocate a new label +ucsan_label allocate_label(); + +// Check if label is valid +void check_label(ucsan_label label); + +//===----------------------------------------------------------------------===// +// UCSan Helper Functions +//===----------------------------------------------------------------------===// + +// Object lookup result +struct object_info { + ucsan_label label; + uint64_t offset; +}; + +// Look up or create an object for a given pointer label +UCSanObject& lookup_object(ucsan_label label, uint64_t offset, void* return_addr, + uint32_t *ret_object_id = nullptr, uint32_t type_id = 0, + uint32_t size = 0); + +// Create a label from the super object (object 0) +object_info create_label_from_super_object(size_t size, bool is_pointer = false); + +// Allocate memory with safe gap for pseudo-pointer resolution +void* customized_malloc(uint64_t size); + +// Check if pointer is within data/bss section +bool in_data_section(void *p); +bool in_bss_section(void *p); + +} // namespace __ucsan + +//===----------------------------------------------------------------------===// +// UCSan Public Interface (C linkage) +//===----------------------------------------------------------------------===// + +extern "C" { + +// UCSan TLS variables for argument/return value passing +#define UCSAN_TLS_SIZE 800 +extern SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL __ucsan::ucsan_label + __ucsan_retval_tls[UCSAN_TLS_SIZE / sizeof(__ucsan::ucsan_label)]; +extern SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL __ucsan::ucsan_label + __ucsan_arg_tls[UCSAN_TLS_SIZE / sizeof(__ucsan::ucsan_label)]; +extern SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint8_t + __ucsan_wrapped_return_tls[UCSAN_TLS_SIZE]; + +// Flag for null dereference checking +extern void* __ucsan_null_deref_flag; + +//===----------------------------------------------------------------------===// +// Core UCSan Functions +//===----------------------------------------------------------------------===// + +// Check and resolve a pointer (main UCSan entry point) +// Handles lazy object initialization and pseudo-pointer resolution +// @param p: the pointer to check +// @param label: the ucsan label for the pointer +// @param size: access size in bytes +// @param dereferencing: true if this is a dereference (load/store) +// @param type_id: compile-time type ID from type table (0 = unknown) +// @return: resolved real pointer (may differ from p for pseudo-pointers) +SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_check_pointer(void* p, __ucsan::ucsan_label label, size_t size, bool dereferencing, uint32_t type_id); + +// Inverse of ucsan_check_pointer: translate a real (materialized) pointer back +// into the UC pseudo-pointer space, using the pointer's label to find the +// backing object. Like phys_to_virt: no allocation, just offset arithmetic. +// Functions whose result aliases an input UC pointer (e.g. strchr/memchr return +// haystack+index in the materialized buffer) must convert the result back so +// that pointer arithmetic against the original UC pointer (e.g. p - c) is +// preserved. Returns real_ptr unchanged if label is not an initialized UC +// pointer (e.g. file-mode pointers), or if real_ptr is null. +SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_uncheck_pointer(void* real_ptr, __ucsan::ucsan_label label); + +// Check pointer argument for use-before-initialization +SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_check_ptr_arg(__ucsan::ucsan_label *label, uint32_t arg_index, void* ret_addr); + +// Load shadow for pointer type +SANITIZER_INTERFACE_ATTRIBUTE +__ucsan::ucsan_label ucsan_load_pointer_shadow(__ucsan::ucsan_label *ls, uint64_t n, bool is_pointer, + void *concrete_addr); + +// Store shadow for pointer type +SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_store_pointer_shadow(__ucsan::ucsan_label l, __ucsan::ucsan_label *ls, uint64_t n); + +// Set label for a memory region +// @param label: the label to set +// @param addr: start address of the memory region +// @param size: size of the memory region in bytes +SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_set_label(__ucsan::ucsan_label label, void *addr, uint64_t size); + +// Set symbolic label for function arguments at entry point +// @param index: argument index +// @param size_in_bits: size of argument in bits +// @param is_pointer: true if argument is a pointer +// @param given: concrete value if no seed data available +// @return: concrete value to use (from seed or given) +SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_set_label_for_args(uint32_t index, uint32_t size_in_bits, uint8_t is_pointer, uint64_t given); + +// Resign shadow for an object +// Called when passing pointer to external/uninstrumented code +// @param ptr: pointer to the object +// @param orig_label: pointer to the label for ptr +// @param n: size of the object in bytes +// @param ret_addr: return address for tracing +// @return: label representing the object +SANITIZER_INTERFACE_ATTRIBUTE +__ucsan::ucsan_label ucsan_resign_shadow(void *ptr, __ucsan::ucsan_label *orig_label, uint64_t n, void *ret_addr); + +// Wrap return value with symbolic label +// @param size: size in bits +// @param ret_label: output label for return value +// @param is_ptr: true if return value is a pointer +// @param ret_addr: return address for tracing +// @return: pointer to buffer with concrete return value +SANITIZER_INTERFACE_ATTRIBUTE +void* ucsan_wrap_retval(uint64_t size, __ucsan::ucsan_label *ret_label, bool is_ptr, void* ret_addr); + +//===----------------------------------------------------------------------===// +// UCSan Initialization +//===----------------------------------------------------------------------===// + +// Initialize UCSan runtime +SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_init(); + +// Finalize UCSan runtime +SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_fini(); + +// Initialize UCSan input from file +SANITIZER_INTERFACE_ATTRIBUTE +void ucsan_init_input(const char *filename); + +} // extern "C" + +//===----------------------------------------------------------------------===// +// Debug Output Macro +//===----------------------------------------------------------------------===// + +#define UCSAN_OUT(...) \ + do { \ + if (__ucsan::ucsan_debug) { \ + Printf("[UCSAN] (%s:%d) ", __FUNCTION__, __LINE__); \ + Printf(__VA_ARGS__); \ + } \ + } while(false) + +#endif // UCSAN_H diff --git a/runtime/dfsan/ucsan_abilist.txt b/runtime/dfsan/ucsan_abilist.txt new file mode 100644 index 00000000..a7483a28 --- /dev/null +++ b/runtime/dfsan/ucsan_abilist.txt @@ -0,0 +1,184 @@ +# UCSan ABI list - custom functions for under-constrained execution + +# Memory allocation functions (need to track shadow) +fun:aligned_alloc=custom +fun:calloc=custom +fun:free=custom +fun:malloc=custom +fun:memalign=custom +fun:posix_memalign=custom +fun:pvalloc=custom +fun:realloc=custom +fun:reallocarray=custom +fun:valloc=custom + +# libc internal versions +fun:__libc_calloc=custom +fun:__libc_free=custom +fun:__libc_malloc=custom +fun:__libc_memalign=custom +fun:__libc_pvalloc=custom +fun:__libc_realloc=custom +fun:__libc_reallocarray=custom +fun:__libc_valloc=custom + +# String and memory functions +fun:memcpy=custom +fun:memmove=custom +fun:memset=custom + +fun:stpcpy=taint +fun:strcat=taint +fun:strcpy=taint +fun:strdup=taint +fun:strncat=taint +fun:strncpy=taint +fun:strndup=taint + +fun:bcmp=taint +fun:memchr=taint +fun:memcmp=taint +fun:memrchr=taint +fun:strcasecmp=taint +fun:strchr=taint +fun:strcmp=taint +fun:strlen=taint +fun:strncasecmp=taint +fun:strncmp=taint +fun:strpbrk=taint +fun:strrchr=taint +fun:strstr=taint +fun:strnstr=taint +fun:memmem=taint + +fun:htonl=taint +fun:htons=taint +fun:ntohl=taint +fun:ntohs=taint +fun:__bswap_16=taint +fun:__bswap_32=taint +fun:__bswap_64=taint +fun:__bswapsi2=taint +fun:__bswapdi2=taint + +# custom assertions +fun:assert_cond=taint +fun:assume_cond=taint +fun:assert_init=custom +fun:assert_allocated=custom +fun:assert_freed=custom +fun:assume_init=custom +fun:assume_allocated=custom +fun:assume_freed=custom + +# kernel +fun:kmalloc_large=custom +fun:__kmalloc=custom +fun:kmalloc=custom +fun:panic=custom +fun:abort=custom +fun:__assert_fail=custom +fun:_copy_from_user=custom +fun:printk=discard +fun:_copy_to_user=discard + +# copy semantics: unbounded (size=strlen(src)) — needs check_copy_bounds +# bounded copies (memcpy/memmove/strncpy/strncat) get the size via sizeN +# below, which check_pointer uses for lazy enlargement. +fun:strcpy=copystr +fun:stpcpy=copystr +fun:strcat=copystr +fun:strdup=copystr + +# I/O open/close (fake handles, no real libc I/O) +fun:open=custom +fun:openat=custom +fun:close=custom +fun:fopen=custom +fun:fopen64=custom +fun:freopen=custom +fun:fclose=custom +fun:fileno=custom +fun:fileno_unlocked=custom + +# I/O seek/tell +fun:fseek=custom +fun:fseeko=custom +fun:lseek=custom +fun:rewind=custom +fun:ftell=custom + +# I/O read functions +fun:read=custom +fun:pread=custom +fun:pread64=custom +fun:fread=custom +fun:fread_unlocked=custom + +# I/O single-byte read functions +fun:fgetc=custom +fun:fgetc_unlocked=custom +fun:getc=custom +fun:getc_unlocked=custom +fun:_IO_getc=custom +fun:getchar=custom + +# ignore I/O writes +fun:write=ignore +fun:writev=ignore +fun:pwrite=ignore +fun:pwrite64=ignore +fun:pwritev=ignore +fun:pwritev64=ignore +fun:fprintf=ignore +fun:vfprintf=ignore +fun:printf=ignore +fun:vprintf=ignore +fun:snprintf=ignore +fun:vsnprintf=ignore +fun:sprintf=ignore +fun:vsprintf=ignore +fun:fwrite=ignore +fun:fwrite_unlocked=ignore +fun:fflush=ignore +fun:fflush_unlocked=ignore +fun:fputc=ignore +fun:fputc_unlocked=ignore +fun:putc=ignore +fun:putc_unlocked=ignore +fun:putchar=ignore +fun:putchar_unlocked=ignore +fun:fputs=ignore +fun:puts=ignore + +# buffer size (sizeN means arg index N (0-indexed) holds the byte count) +fun:_copy_from_user=size2 + +# explicit byte-count args for taint comparison/search/copy functions +fun:memcpy=size2 +fun:memmove=size2 +fun:memcmp=size2 +fun:bcmp=size2 +fun:strncmp=size2 +fun:strncasecmp=size2 +fun:memchr=size2 +fun:memrchr=size2 +fun:strnstr=size2 +fun:strncpy=size2 +fun:strncat=size2 +fun:strndup=size1 + +# functions whose return value is a pointer into a pointer argument +# (retptrN means the result aliases arg N). Under UC the result must be +# translated from the materialized buffer back into the caller's pseudo +# pointer space (ucsan_uncheck_pointer) so that p - arg == index holds. +# Their __dfsw_ wrappers also stash the haystack pointer label in op2's high +# bits (low 8 bits = needle char) so the solver can constrain the base pointer +# non-null and simplify (ptr - base) to the index. This is limited to the +# single-char search ops, whose op2 is free; strstr/strpbrk/memmem keep a +# needle/accept pointer in op2 (used for concrete content), so they have no +# room to carry the pointer label and are not listed here. +fun:strchr=retptr0 +fun:strrchr=retptr0 +fun:memchr=retptr0 +fun:memrchr=retptr0 diff --git a/runtime/dfsan/ucsan_containers.h b/runtime/dfsan/ucsan_containers.h new file mode 100644 index 00000000..e0738ac8 --- /dev/null +++ b/runtime/dfsan/ucsan_containers.h @@ -0,0 +1,147 @@ +//===-- ucsan_containers.h - UCSan Container Types -------------*- C++ -*-===// +// +// Container implementations for UCSan runtime. +// Separated from ucsan.h to keep type definitions focused. +// +//===----------------------------------------------------------------------===// + +#ifndef UCSAN_CONTAINERS_H +#define UCSAN_CONTAINERS_H + +#include "sanitizer_common/sanitizer_internal_defs.h" +#include "sanitizer_common/sanitizer_common.h" + +#include +#include + +namespace __ucsan { + +//===----------------------------------------------------------------------===// +// AddrRangeMap - sorted array for address range → object ID lookup +//===----------------------------------------------------------------------===// +// Supports container_of/list_entry patterns where pointers point into +// the middle of objects. Uses binary search for O(log n) range queries. + +using ucsan_label = uint16_t; + +struct AddrRangeEntry { + uintptr_t start; + size_t size; + uint32_t object_id; + ucsan_label ptr_label; // ucsan label of the pointer to this object +}; + +struct AddrRangeMap { + AddrRangeEntry *entries; + uint32_t len; + uint32_t cap; + + void init(uint32_t initial_cap = 64) { + cap = initial_cap; + len = 0; + uptr alloc_size = __sanitizer::RoundUpTo( + cap * sizeof(AddrRangeEntry), __sanitizer::GetPageSizeCached()); + entries = (AddrRangeEntry *)__sanitizer::MmapOrDie( + alloc_size, "AddrRangeMap"); + } + + void destroy() { + if (entries) { + uptr alloc_size = __sanitizer::RoundUpTo( + cap * sizeof(AddrRangeEntry), __sanitizer::GetPageSizeCached()); + __sanitizer::UnmapOrDie(entries, alloc_size); + entries = nullptr; + } + len = cap = 0; + } + + // Insert maintaining sorted order by start address. + void insert(void *addr, size_t size, uint32_t object_id, + ucsan_label ptr_label) { + if (len >= cap) grow(); + + uintptr_t start = (uintptr_t)addr; + + // Binary search for insertion point + uint32_t lo = 0, hi = len; + while (lo < hi) { + uint32_t mid = lo + (hi - lo) / 2; + if (entries[mid].start < start) + lo = mid + 1; + else + hi = mid; + } + + // Shift elements to make room + for (uint32_t i = len; i > lo; --i) + entries[i] = entries[i - 1]; + + entries[lo] = {start, size, object_id, ptr_label}; + len++; + } + + // Find which registered object contains the given address. + // Returns true if found; sets out_object_id, out_offset (within object), + // and out_ptr_label. + bool find(void *addr, uint32_t *out_object_id, uint32_t *out_offset, + ucsan_label *out_ptr_label) const { + if (len == 0) return false; + + uintptr_t target = (uintptr_t)addr; + + // Binary search: find largest start <= target + uint32_t lo = 0, hi = len; + while (lo < hi) { + uint32_t mid = lo + (hi - lo) / 2; + if (entries[mid].start <= target) + lo = mid + 1; + else + hi = mid; + } + + // lo is now one past the last entry with start <= target + if (lo == 0) return false; + + const AddrRangeEntry &e = entries[lo - 1]; + if (target >= e.start && target < e.start + e.size) { + if (out_object_id) *out_object_id = e.object_id; + if (out_offset) *out_offset = (uint32_t)(target - e.start); + if (out_ptr_label) *out_ptr_label = e.ptr_label; + return true; + } + + return false; + } + + // Iteration over all entries in sorted (start address) order. + // Entries are stored contiguously, so plain pointers serve as iterators. + typedef AddrRangeEntry *iterator; + typedef const AddrRangeEntry *const_iterator; + + iterator begin() { return entries; } + iterator end() { return entries + len; } + const_iterator begin() const { return entries; } + const_iterator end() const { return entries + len; } + + uint32_t size() const { return len; } + bool empty() const { return len == 0; } + +private: + void grow() { + uint32_t old_cap = cap; + AddrRangeEntry *old = entries; + cap *= 2; + uptr alloc_size = __sanitizer::RoundUpTo( + cap * sizeof(AddrRangeEntry), __sanitizer::GetPageSizeCached()); + entries = (AddrRangeEntry *)__sanitizer::MmapOrDie( + alloc_size, "AddrRangeMap"); + __sanitizer::internal_memcpy(entries, old, len * sizeof(AddrRangeEntry)); + uptr old_size = __sanitizer::RoundUpTo( + old_cap * sizeof(AddrRangeEntry), __sanitizer::GetPageSizeCached()); + __sanitizer::UnmapOrDie(old, old_size); + } +}; + +} // namespace __ucsan + +#endif // UCSAN_CONTAINERS_H diff --git a/runtime/dfsan/ucsan_custom.cpp b/runtime/dfsan/ucsan_custom.cpp new file mode 100644 index 00000000..df346d9b --- /dev/null +++ b/runtime/dfsan/ucsan_custom.cpp @@ -0,0 +1,1155 @@ +// UCSan custom function wrappers for standalone mode +// Provides minimal malloc/free wrappers with bounds tracking + +#include "sanitizer_common/sanitizer_internal_defs.h" +#include "sanitizer_common/sanitizer_common.h" +#include "ucsan_platform.h" +#include "ucsan.h" + +#include +#include +#include +#include +#include + +using namespace __sanitizer; +using namespace __ucsan; + +// External symbols for real libc functions +extern "C" { + void *__libc_malloc(size_t size); + void *__libc_calloc(size_t nmemb, size_t size); + void __libc_free(void *ptr); + // [[deprecated]] void *memalign(size_t alignment, size_t size); + // [[deprecated]] void *valloc(size_t size); + // [[deprecated]] void *pvalloc(size_t size); + void *aligned_alloc(size_t alignment, size_t size); + int posix_memalign(void **memptr, size_t alignment, size_t size); + + // SymSan bridge functions (weak stubs in ucsan.cpp, strong in dfsan.cpp) + dfsan_label __taint_create_label(uint32_t object_id, uint64_t offset, + uint32_t size_in_bytes); + void __taint_copy_shadow(void *dst, void *src, u64 size); + void __taint_move_shadow(void *dst, void *src, u64 size); + void __taint_set_label(u32 label, void *addr, u64 size); + dfsan_label __taint_get_ptr_bounds_label(void *ptr, u64 lower, u64 upper); + void __taint_set_retval_tls(u32 index, dfsan_label label, u32 size_in_bits); + dfsan_label __taint_extend_label(dfsan_label label, bool sign_extend, + uint16_t new_size_in_bits); + + // event + void __taint_trace_event_addr(__ucsan::ucsan_label label, uint32_t event_id, + uint64_t info, void* addr, uint32_t info2); +} + +// Simple passthrough wrappers for malloc family +// In standalone UCSan mode, we don't track taint, just provide the symbols + +extern "C" { + +// Helper to create an alloca-style bounds label for heap allocations +// Also sets shadow memory to kUninitializedLabel for UBI detection +static ucsan_label create_alloca_label(void *ptr, size_t size, bool set_uninitialized) { + if (ptr == nullptr) { + // clear retval TLS for null pointer (no label, no bounds) + __taint_set_retval_tls(0, 0, sizeof(void*) * 8); + return UCSAN_CONST_LABEL; + } + + ucsan_label label = allocate_label(); + check_label(label); + + ucsan_label_info *info = get_label_info(label); + ucsan_obj_info *obj = to_obj_info(info); + + obj->op = OP_ALLOCA; + obj->type_id = 0; + obj->object_id = 0; // Not tracked in objects array + obj->real_ptr = ptr; + obj->lower_bound = 0; // No bytes before base + obj->upper_bound = (u32)size; // Size in bytes + + // Set shadow memory to kUninitializedLabel for UBI detection + if (set_uninitialized) { + ucsan_label *shadow = ucsan_shadow_for(ptr); + for (size_t i = 0; i < size; i++) { + shadow[i] = kUninitializedLabel; + } + } + + // Bridge to SymSan: create Alloca bounds label and set via retval TLS + dfsan_label bounds = __taint_get_ptr_bounds_label(nullptr, (u64)ptr, (u64)ptr + size); + __taint_set_retval_tls(0, bounds, sizeof(void*) * 8); + + return label; +} + +__attribute__((visibility("default"))) +void *__dfsw_malloc(size_t size, ucsan_label size_label, ucsan_label *ret_label) { + void *ret = nullptr; + if (size != 0 && size < UINT32_MAX) { + ret = __libc_malloc(size); + } + UCSAN_OUT("__dfsw_malloc(%zu) = %p\n", size, ret); + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + *ret_label = create_alloca_label(ret, size, true); + return ret; +} + +__attribute__((visibility("default"))) +void *__dfsw_kmalloc_large(size_t size, unsigned int flags, + ucsan_label size_label, ucsan_label flags_label, + ucsan_label *ret_label) { + return __dfsw_malloc(size, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___kmalloc(size_t size, unsigned int flags, + ucsan_label size_label, ucsan_label flags_label, + ucsan_label *ret_label) { + return __dfsw_malloc(size, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw_kmalloc(size_t size, unsigned int flags, ucsan_label size_label, + ucsan_label flags_label, ucsan_label *ret_label) { + return __dfsw_malloc(size, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw_calloc(size_t nmemb, size_t size, ucsan_label nmemb_label, + ucsan_label size_label, ucsan_label *ret_label) { + void *ret = nullptr; + size_t total_size = nmemb * size; + if (total_size != 0 && total_size < UINT32_MAX) { + ret = __libc_calloc(nmemb, size); + } + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + // (calloc zeros memory, but we still want to detect reads before writes) + *ret_label = create_alloca_label(ret, total_size, false); + return ret; +} + +__attribute__((visibility("default"))) +void *__dfsw_realloc(void *ptr, size_t size, ucsan_label ptr_label, + ucsan_label size_label, ucsan_label *ret_label) { + // If ptr has an alloca label, mark it as freed + size_t old_size = 0; + if (ptr) { + if (ptr_label > UCSAN_CONST_LABEL) { + ucsan_label_info *old_info = get_label_info(ptr_label); + if (old_info->common.op == OP_ALLOCA) { + ucsan_obj_info *old_obj = to_obj_info(old_info); + if (old_obj->op == OP_ALLOCA) { + old_obj->op = OP_FREE; // Mark old buffer as freed + } + old_size = old_obj->upper_bound - old_obj->lower_bound; + if (ptr != old_obj->real_ptr) { + UCSAN_OUT("WARNING: realloc ptr %p does not match real_ptr %p\n", ptr, old_obj->real_ptr); + } + } else if (old_info->common.op == OP_EXTERNAL) { + ucsan_ptr_info *ptr_info = to_ptr_info(old_info); + // If ptr is an external pointer, size is in obj + if (ptr_info->status == PTR_INITIALIZED) { + ucsan_obj_info *obj_info = to_obj_info(get_label_info(ptr_info->obj_label)); + old_size = obj_info->lower_bound + obj_info->upper_bound; + } else { + UCSAN_OUT("WARNING: realloc external ptr label %d is not initialized\n", ptr_label); + } + } else if (old_info->common.op == OP_FREE) { + UCSAN_OUT("WARNING: realloc ptr_label %d is already freed\n", ptr_label); + } else { + UCSAN_OUT("WARNING: realloc ptr_label %d is not alloca, op %d\n", ptr_label, old_info->common.op); + } + } else { + old_size = malloc_usable_size(ptr); + } + } + + // don't actually free the buffer + void *ret = nullptr; + if (size != 0 && size < UINT32_MAX) { + ret = __libc_malloc(size); + } + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + *ret_label = create_alloca_label(ret, size, true); + + UCSAN_OUT("__dfsw_realloc(%p, %zu) = %p, old_size: %zu", ptr, size, ret, old_size); + if (ret) { + // Copy data from old buffer to new buffer (up to min of old/new size) + size_t copy_size = old_size < size ? old_size : size; + internal_memcpy(ret, ptr, copy_size); + // Copy UCSan shadow memory from old to new + ucsan_label *sdest = ucsan_shadow_for(ret); + const ucsan_label *ssrc = ucsan_shadow_for(ptr); + internal_memcpy((void *)sdest, (const void *)ssrc, copy_size * sizeof(ucsan_label)); + // Bridge to SymSan: copy SymSan shadow memory + __taint_copy_shadow(ret, (void *)ptr, copy_size); + } + return ret; +} + +__attribute__((visibility("default"))) +void *__dfsw_reallocarray(void *ptr, size_t nmemb, size_t size, + ucsan_label ptr_label, ucsan_label nmemb_label, + ucsan_label size_label, ucsan_label *ret_label) { + size_t total_size = nmemb * size; + + return __dfsw_realloc(ptr, total_size, ptr_label, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void __dfsw_free(void *ptr, ucsan_label ptr_label) { + // Mark buffer as freed for UAF detection + if (ptr && ptr_label > UCSAN_CONST_LABEL) { + ucsan_label_info *info = get_label_info(ptr_label); + if (info->common.op == OP_ALLOCA || info->common.op == OP_EXTERNAL) { + info->common.op = OP_FREE; + } else if (info->common.op == OP_FREE) { + UCSAN_OUT("WARNING: double free detected for ptr %p, label %d\n", ptr, ptr_label); + __taint_trace_event_addr(ptr_label, EVENT_DOUBLE_FREE, 0, __builtin_return_address(0), 0); + exit(exit_reason::EVENT_DOUBLE_FREE); + } else { + UCSAN_OUT("WARNING: free ptr_label %d with unknown op %d\n", ptr_label, info->common.op); + } + } + // don't actually free the buffer, to capture UAFs + // __libc_free(ptr); +} + +__attribute__((visibility("default"))) +void *__dfsw_memalign(size_t alignment, size_t size, + ucsan_label alignment_label, ucsan_label size_label, + ucsan_label *ret_label) { + void *ret = nullptr; + if (size > 0 && size < UINT32_MAX) { + ret = aligned_alloc(alignment, size); + } + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + *ret_label = create_alloca_label(ret, size, true); + return ret; +} + +__attribute__((visibility("default"))) +void *__dfsw_aligned_alloc(size_t alignment, size_t size, + ucsan_label alignment_label, ucsan_label size_label, + ucsan_label *ret_label) { + void *ret = nullptr; + if (size > 0 && size < UINT32_MAX) { + ret = aligned_alloc(alignment, size); + } + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + *ret_label = create_alloca_label(ret, size, true); + return ret; +} + +#define PAGE_SIZE 4096 + +__attribute__((visibility("default"))) +void *__dfsw_valloc(size_t size, ucsan_label size_label, ucsan_label *ret_label) { + void *ret = nullptr; + if (size > 0 && size < UINT32_MAX) { + ret = aligned_alloc(PAGE_SIZE, size); + } + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + *ret_label = create_alloca_label(ret, size, true); + return ret; +} + +__attribute__((visibility("default"))) +void *__dfsw_pvalloc(size_t size, ucsan_label size_label, ucsan_label *ret_label) { + void *ret = nullptr; + if (size > 0 && size < UINT32_MAX) { + ret = aligned_alloc(PAGE_SIZE, size); + } + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + *ret_label = create_alloca_label(ret, size, true); + return ret; +} + +__attribute__((visibility("default"))) +int __dfsw_posix_memalign(void **memptr, size_t alignment, size_t size, + ucsan_label memptr_label, ucsan_label alignment_label, + ucsan_label size_label, ucsan_label *ret_label) { + int result = posix_memalign(memptr, alignment, size); + if (result == 0) { + *ret_label = UCSAN_CONST_LABEL; // Return value is int, no pointer label + // Create bounds label (sets shadow to kUninitializedLabel for UBI) + ucsan_label alloca_label = create_alloca_label(*memptr, size, true); + ucsan_label *shadow = ucsan_shadow_for(memptr); + *shadow = alloca_label; // Label for the pointer itself + } else { + *ret_label = UCSAN_CONST_LABEL; + } + return result; +} + +// Wrappers for __libc versions - delegate to main wrappers for bounds tracking + +__attribute__((visibility("default"))) +void *__dfsw___libc_malloc(size_t size, ucsan_label size_label, ucsan_label *ret_label) { + return __dfsw_malloc(size, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___libc_calloc(size_t nmemb, size_t size, ucsan_label nmemb_label, + ucsan_label size_label, ucsan_label *ret_label) { + return __dfsw_calloc(nmemb, size, nmemb_label, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___libc_realloc(void *ptr, size_t size, ucsan_label ptr_label, + ucsan_label size_label, ucsan_label *ret_label) { + return __dfsw_realloc(ptr, size, ptr_label, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___libc_reallocarray(void *ptr, size_t nmemb, size_t size, + ucsan_label ptr_label, ucsan_label nmemb_label, + ucsan_label size_label, ucsan_label *ret_label) { + return __dfsw_reallocarray(ptr, nmemb, size, ptr_label, nmemb_label, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void __dfsw___libc_free(void *ptr, ucsan_label ptr_label) { + __dfsw_free(ptr, ptr_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___libc_memalign(size_t alignment, size_t size, + ucsan_label alignment_label, ucsan_label size_label, + ucsan_label *ret_label) { + return __dfsw_memalign(alignment, size, alignment_label, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___libc_valloc(size_t size, ucsan_label size_label, ucsan_label *ret_label) { + return __dfsw_valloc(size, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw___libc_pvalloc(size_t size, ucsan_label size_label, ucsan_label *ret_label) { + return __dfsw_pvalloc(size, size_label, ret_label); +} + +__attribute__((visibility("default"))) +void *__dfsw_memcpy(void *dest, const void *src, size_t n, + ucsan_label dest_label, ucsan_label src_label, + ucsan_label n_label, ucsan_label *ret_label) { + *ret_label = dest_label; + // Copy UCSan shadow memory from src to dest + ucsan_label *sdest = ucsan_shadow_for(dest); + const ucsan_label *ssrc = ucsan_shadow_for(src); + internal_memcpy((void *)sdest, (const void *)ssrc, n * sizeof(ucsan_label)); + // Bridge to SymSan: copy SymSan shadow memory + __taint_copy_shadow(dest, (void *)src, n); + // Copy actual data + return internal_memcpy(dest, src, n); +} + +__attribute__((visibility("default"))) +void *__dfsw_memmove(void *dest, const void *src, size_t n, + ucsan_label dest_label, ucsan_label src_label, + ucsan_label n_label, ucsan_label *ret_label) { + *ret_label = dest_label; + ucsan_label *sdest = ucsan_shadow_for(dest); + const ucsan_label *ssrc = ucsan_shadow_for(src); + internal_memmove((void *)sdest, (const void *)ssrc, n * sizeof(ucsan_label)); + // Bridge to SymSan: move SymSan shadow memory (handles overlapping regions) + __taint_move_shadow(dest, (void *)src, n); + void *ret = internal_memmove(dest, src, n); + return ret; +} + +__attribute__((visibility("default"))) +void *__dfsw_memset(void *s, int c, size_t n, + ucsan_label s_label, ucsan_label c_label, + ucsan_label n_label, ucsan_label *ret_label) { + *ret_label = s_label; + // Set actual memory + internal_memset(s, c, n); + // Set UCSan shadow memory - propagate c_label to all bytes + ucsan_label *shadow = ucsan_shadow_for(s); + for (size_t i = 0; i < n; i++) { + shadow[i] = c_label; + } + // Bridge to SymSan: set SymSan shadow memory + __taint_set_label(c_label, s, n); + return s; +} + +// Kernel-specific stubs for __get_user and __put_user inline asm helpers +SANITIZER_INTERFACE_WEAK_DEF(int, __get_user_1, void) { return 0; } +SANITIZER_INTERFACE_WEAK_DEF(int, __get_user_2, void) { return 0; } +SANITIZER_INTERFACE_WEAK_DEF(int, __get_user_4, void) { return 0; } +SANITIZER_INTERFACE_WEAK_DEF(int, __get_user_8, void) { return 0; } +SANITIZER_INTERFACE_WEAK_DEF(void, __put_user_1, void) { } +SANITIZER_INTERFACE_WEAK_DEF(void, __put_user_2, void) { } +SANITIZER_INTERFACE_WEAK_DEF(void, __put_user_4, void) { } +SANITIZER_INTERFACE_WEAK_DEF(void, __put_user_8, void) { } + +// _copy_from_user: copy n bytes from user-space pointer to kernel buffer +// Propagates shadow/labels from source to destination +__attribute__((visibility("default"))) +unsigned long __dfsw__copy_from_user(void *to, const void *from, unsigned long n, + ucsan_label to_label, ucsan_label from_label, + ucsan_label n_label, ucsan_label *ret_label) { + UCSAN_OUT("copy_from_user(%u(%p), %u(%p), %lu)\n", to_label, to, from_label, from, n); + if (from_label) { + // assume from ptr has been checked/initialized before calling + ucsan_ptr_info *ptr_info = to_ptr_info(get_label_info(from_label)); + if (ptr_info->op != OP_EXTERNAL) { + UCSAN_OUT("WARNING: copy_from_user: from_label %d is not external, op %d\n", from_label, ptr_info->op); + } + ucsan_label *sdest = ucsan_shadow_for(to); + const ucsan_label *ssrc = ucsan_shadow_for(from); + internal_memcpy((void *)sdest, (const void *)ssrc, n * sizeof(ucsan_label)); + // Bridge to SymSan + __taint_copy_shadow(to, (void *)from, n); + } + // Copy actual data + internal_memcpy(to, from, n); + *ret_label = 0; + return 0; +} + +/// assertion and assumption interfaces + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_assert_allocated(void *ptr, size_t size, uint64_t id, ucsan_label ptr_label, + ucsan_label size_label, ucsan_label id_label) { + if (ptr_label == 0) { + if (ptr == nullptr) { + UCSAN_OUT("ERROR: assertion %lu failure null pointer\n", id); + __taint_trace_event_addr(0, EVENT_ASSERTION, id, ptr, ASSERTION_ALLOCATED_FAILED); + } else { + UCSAN_OUT("WARNING: assertion %lu non-symbolic label: ptr %p, label %d\n", id, ptr, ptr_label); + __taint_trace_event_addr(0, EVENT_ASSERTION, id, ptr, ASSERTION_ALLOCATED_SUCCESS); + } + return; + } + ucsan_label_info *info = get_label_info(ptr_label); + ucsan_obj_info *obj = to_obj_info(info); + if (obj->op != OP_ALLOCA) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p, label %d, op %d\n", id, ptr, ptr_label, obj->op); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_ALLOCATED_FAILED); + return; + } + if (ptr != obj->real_ptr) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p does not match real_ptr %p\n", id, ptr, obj->real_ptr); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_ALLOCATED_FAILED); + return; + } + if (size > obj->upper_bound) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p, label %d, size %lu exceeds upper bound %u\n", + id, ptr, ptr_label, size, obj->upper_bound); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_ALLOCATED_FAILED); + } + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_ALLOCATED_SUCCESS); +} + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_assert_freed(void *ptr, uint64_t id, ucsan_label ptr_label, ucsan_label id_label) { + if (ptr_label == 0) { + if (ptr != nullptr) { + UCSAN_OUT("ERROR: assertion %lu failure non-symbolic label: ptr %p, label %d\n", id, ptr, ptr_label); + __taint_trace_event_addr(0, EVENT_ASSERTION, id, ptr, ASSERTION_NONE_SYMBOLIC); + } else { + __taint_trace_event_addr(0, EVENT_ASSERTION, id, ptr, ASSERTION_FREED_SUCCESS); + } + return; + } + ucsan_label_info *info = get_label_info(ptr_label); + ucsan_obj_info *obj = to_obj_info(info); + if (obj->op != OP_FREE) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p, label %d, op %d\n", id, ptr, ptr_label, obj->op); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_FREED_FAILED); + return; + } + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_FREED_SUCCESS); +} + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_assert_init(void *ptr, size_t size, uint64_t id, ucsan_label ptr_label, + ucsan_label size_label, ucsan_label id_label) { + // check ptr is allocated and the size is in bound + if (ptr_label == 0) { + if (ptr == nullptr) { + UCSAN_OUT("ERROR: assertion %lu failure null pointer\n", id); + __taint_trace_event_addr(0, EVENT_ASSERTION, id, ptr, ASSERTION_INIT_FAILED); + return; + } else { + UCSAN_OUT("WARNING: assertion %lu non-symbolic label: ptr %p, label %d\n", id, ptr, ptr_label); + } + } else { + ucsan_label_info *info = get_label_info(ptr_label); + ucsan_obj_info *obj = to_obj_info(info); + if (obj->op != OP_ALLOCA) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p, label %d, op %d\n", id, ptr, ptr_label, obj->op); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_INIT_FAILED); + return; + } + if (size > obj->upper_bound) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p, label %d, size %lu exceeds upper bound %u\n", + id, ptr, ptr_label, size, obj->upper_bound); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_INIT_FAILED); + return; + } + } + // fall through to scan shadow + bool success = true; + ucsan_label *shadow = ucsan_shadow_for(ptr); + for (size_t i = 0; i < size; i++) { + if (shadow[i] == kUninitializedLabel) { + UCSAN_OUT("ERROR: assertion %lu failure: ptr %p, index %lu, label %d\n", id, ptr, i, shadow[i]); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, (char *)ptr + i, ASSERTION_INIT_FAILED); + success = false; + } + } + if (success) { + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSERTION_INIT_SUCCESS); + } +} + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_assume_init(void *ptr, size_t size, uint64_t id, ucsan_label ptr_label, + ucsan_label size_label, uint64_t id_label) { + if (ptr_label == 0) { + UCSAN_OUT("WARNING: assumption %lu non-symbolic label: ptr %p\n", id, ptr); + return; + } + ucsan_label_info *info = get_label_info(ptr_label); + ucsan_obj_info *obj = to_obj_info(info); + if (obj->op != OP_ALLOCA) { + UCSAN_OUT("WARNING: assumption %lu failure: ptr %p, label %d, op %d\n", id, ptr, ptr_label, obj->op); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSUMPTION_CONTRACTION); + exit(exit_reason::EVENT_CONTRACTION); + } + if (size > obj->upper_bound) { + UCSAN_OUT("WARNING: assumption %lu failure: ptr %p, label %d, size %lu exceeds upper bound %u\n", + id, ptr, ptr_label, size, obj->upper_bound); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSUMPTION_CONTRACTION); + exit(exit_reason::EVENT_CONTRACTION); + } + if (size > ucsan_object_size_limit()) { + UCSAN_OUT("WARNING: alloca resign size %lu exceeds limit %lu, capping\n", + size, ucsan_object_size_limit()); + size = ucsan_object_size_limit(); + } + ucsan_label *shadow = ucsan_shadow_for(ptr); + char *obj_ptr = (char *)ptr; + for (size_t i = 0; i < size; i++) { + // do the same as ucsan_resign_shadow + if (shadow[i] == kUninitializedLabel) { + // Allocate a byte from super object + auto ret = create_label_from_super_object(1, false); + UCSAN_OUT("resign alloca ret: %u %lu\n", ret.label, ret.offset); + shadow[i] = ret.label; + + // Bridge to SymSan: create symbolic label for this byte + dfsan_label symsan_label = __taint_create_label(0, ret.offset, 1); + __taint_set_label(symsan_label, obj_ptr + i, 1); + + if (ucsan_tainted.objects->size() && ret.offset < ucsan_tainted.objects->at(0).data.size()) { + UCSAN_OUT("resign alloca super object: %u\n", ucsan_tainted.objects->at(0).data.at(ret.offset)); + obj_ptr[i] = ucsan_tainted.objects->at(0).data.at(ret.offset); + } else { + obj_ptr[i] = 0; + } + } + } +} + +SANITIZER_INTERFACE_ATTRIBUTE void* +__dfsw_assume_allocated(void *ptr, size_t size, uint64_t id, ucsan_label ptr_label, + ucsan_label size_label, ucsan_label id_label, + ucsan_label *ret_label) { + if (ptr_label == 0) { + // allocate a new buffer + char *new_ptr = (char *)__dfsw_malloc(size, size_label, ret_label); + // symbolize the new buffer + if (new_ptr) { + ucsan_label *shadow = ucsan_shadow_for(new_ptr); + for (size_t i = 0; i < size; i++) { + auto ret = create_label_from_super_object(1, false); + shadow[i] = ret.label; + dfsan_label symsan_label = __taint_create_label(0, ret.offset, 1); + __taint_set_label(symsan_label, (char *)new_ptr + i, 1); + + // Initialize from seed data if available + if (ucsan_tainted.objects->size() && ret.offset < ucsan_tainted.objects->at(0).data.size()) { + new_ptr[i] = ucsan_tainted.objects->at(0).data.at(ret.offset); + } else { + new_ptr[i] = 0; + } + } + } + return new_ptr; + } + // ptr has a label, just make sure size is right + ucsan_label_info *info = get_label_info(ptr_label); + if (info->common.op == OP_ALLOCA) { + ucsan_obj_info *obj = to_obj_info(info); + if (size > obj->upper_bound) { + void *new_ptr = __dfsw_realloc(ptr, size, ptr_label, size_label, ret_label); + return new_ptr; + } + *ret_label = ptr_label; + } else if (info->common.op == OP_FREE) { + // should not happen, contradiction + UCSAN_OUT("WARNING: assume_allocated id=%lu, ptr=%p, label=%d is already freed\n", + id, ptr, ptr_label); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSUMPTION_CONTRACTION); + exit(exit_reason::EVENT_CONTRACTION); + } else { + // external ptr + ucsan_label_info *info = get_label_info(ptr_label); + ucsan_ptr_info *ptr_info = to_ptr_info(info); + // for external ptr, we need to convert it back to pseudo ptr + ptr = ptr_info->pseudo_base; + if (size_label == 0) { + // size is concrete, we set a concrete bound by malloc + void *new_ptr = __dfsw_malloc(size, size_label, ret_label); + if (new_ptr) { + void *old_ptr = ucsan_check_pointer(ptr, ptr_label, size, true, 0); + ucsan_label dummy; + __dfsw_memcpy(new_ptr, old_ptr, size, *ret_label, ptr_label, size_label, &dummy); + return new_ptr; + } + } else { + // symbolic size, keep it unbounded? + *ret_label = ptr_label; + } + } + + return ptr; +} + +SANITIZER_INTERFACE_ATTRIBUTE void* +__dfsw_assume_freed(void *ptr, uint64_t id, ucsan_label ptr_label, + ucsan_label id_label, ucsan_label *ret_label) { + if (ptr_label == 0) { + ptr_label = allocate_label(); + check_label(ptr_label); + } + ucsan_label_info *info = get_label_info(ptr_label); + // check for contradictions + if (info->common.op == OP_ALLOCA) { + UCSAN_OUT("WARNING: assume_freed id=%lu, ptr=%p, label=%d is still allocated\n", + id, ptr, ptr_label); + __taint_trace_event_addr(ptr_label, EVENT_ASSERTION, id, ptr, ASSUMPTION_CONTRACTION); + exit(exit_reason::EVENT_CONTRACTION); + } + // otherwise we just mark it as freed + // internal_memset(info, 0, sizeof(*info)); + info->common.op = OP_FREE; + *ret_label = ptr_label; + return ptr; +} + +SANITIZER_INTERFACE_ATTRIBUTE void +__dfsw_panic(char *reason, ucsan_label reason_label) { + UCSAN_OUT("PANIC: %s\n", reason); + _exit(EVENT_PANIC); +} + +SANITIZER_INTERFACE_ATTRIBUTE __attribute__((noreturn)) void +__dfsw_abort(void) { + UCSAN_OUT("ABORT\n"); + _exit(EVENT_PANIC); +} + +SANITIZER_INTERFACE_ATTRIBUTE __attribute__((noreturn)) void +__dfsw___assert_fail(const char *assertion, const char *file, + unsigned int line, const char *function, + ucsan_label assertion_label, ucsan_label file_label, + ucsan_label line_label, ucsan_label function_label) { + UCSAN_OUT("ASSERT FAILED: %s at %s:%u (%s)\n", assertion, file, line, function); + _exit(EVENT_PANIC); +} + +//===----------------------------------------------------------------------===// +// File Simulator +// +// open()/fopen() are not forwarded to libc — we return a unique opaque +// handle and label it via create_label_from_super_object so the handle's +// label points to (super_obj=0, super_offset). The first read on that +// handle materializes a UC object via lookup_object(handle_label), which +// registers the (parent=0, offset=super_offset) -> file_obj_id mapping in +// obj_map. Re-runs see the same super_offset and resolve to the same +// file_obj_id — so the solver can feed back file content by populating +// obj.data for that id in the seed. +// +// Reads grow the file UCSanObject's data on demand (similar to how +// check_pointer extends an under-constrained object), and tag each +// byte's shadow with an OP_NONE label pointing to (file_obj_id, offset+i). +//===----------------------------------------------------------------------===// + +// UCSan tests one file at a time (SymSan likewise only symbolizes a single +// input file), so we keep one global state for "the file" instead of a +// per-handle table. open/fopen return distinct fake handles purely so +// callers' null-checks behave; all reads route to this single state. +struct ucsan_file_state { + ucsan_label label; // label rooting the file object in the super obj + uint32_t object_id; // cached file UC object id (0 = not yet resolved) + off_t offset; // current sequential read position + off_t size; // concrete backing-file size, or -1 if unknown + int fd; // current fake fd + FILE *stream; // current fake FILE* +}; +static ucsan_file_state __ucsan_file = {0, 0, 0, -1, -1, nullptr}; + +// Counter for synthetic fd/FILE* values returned by our open/fopen wrappers. +// Start above the usual stdio fds and at a recognizable bit pattern for +// FILE* so accidental dereferences fault loudly instead of returning data. +static int __ucsan_fake_fd_counter = 1000; +static uintptr_t __ucsan_fake_file_counter = 0xFAFE0000; + +static off_t concrete_file_size(const char *path) { + if (!path) return -1; + struct stat st; + if (stat(path, &st) == 0 && S_ISREG(st.st_mode)) + return st.st_size; + return -1; +} + +static void materialize_file_object(ucsan_file_state *state) { + if (!state || state->object_id != 0) return; + + uint32_t obj_id = 0; + lookup_object(state->label, 0, __builtin_return_address(0), &obj_id, 0, 0); + state->object_id = obj_id; + UCSAN_OUT("file simulator: resolved file_obj_id=%u (label=%u)\n", + obj_id, state->label); +} + +// Each open/fopen creates an anchor pointer in the super object. The file +// content object is then discovered through lookup_object({obj0, anchor_off}) +// so replay can rebuild obj_map from the seed metadata. +static ucsan_file_state *open_file_state(off_t size = -1) { + auto lbl = create_label_from_super_object(sizeof(void*), true); + __ucsan_file.label = lbl.label; + __ucsan_file.object_id = 0; + __ucsan_file.offset = 0; + __ucsan_file.size = size; + UCSAN_OUT("file simulator: opened anchor label=%u offset=%lu\n", + __ucsan_file.label, lbl.offset); + materialize_file_object(&__ucsan_file); + return &__ucsan_file; +} + +// Fallback for read/getchar-style use without a prior open wrapper. +static ucsan_file_state *get_file_state() { + if (__ucsan_file.label == 0) { + return open_file_state(); + } + return &__ucsan_file; +} + +static off_t simulated_file_size(ucsan_file_state *s) { + if (!s) return 0; + if (s->size >= 0) return s->size; + materialize_file_object(s); + if (s->object_id != 0 && s->object_id < ucsan_tainted.objects->size()) + return (off_t)ucsan_tainted.objects->at(s->object_id).data.size(); + return s->offset; +} + +// Symbolize n bytes into ptr from the file UC object at position pos. +// Grows obj.data to cover [pos, pos+n) on first touch of those bytes; +// existing bytes (e.g. populated from seed) are preserved. +static size_t simulate_file_read(ucsan_file_state *state, void *ptr, size_t n, off_t pos) { + if (!state || !ptr || n == 0) return 0; + + materialize_file_object(state); + if (state->object_id == 0) return 0; + + // Clamp at the per-object size limit. Anything past the cap returns + // short (caller treats as EOF) rather than silently wrapping. + off_t end = pos + (off_t)n; + if (end > (off_t)ucsan_object_size_limit()) { + UCSAN_OUT("WARNING: file read end %ld exceeds object limit %lu, capping\n", + (long)end, ucsan_object_size_limit()); + end = ucsan_object_size_limit(); + if (pos >= end) return 0; + n = (size_t)(end - pos); + } + + // Grow the file object's data to cover the read window. Seeded bytes + // (from solver feedback in re-runs) are preserved by ByteBuffer::resize. + UCSanObject &obj = ucsan_tainted.objects->at(state->object_id); + if (obj.data.size() < (uint32_t)end) { + obj.data.resize((uint32_t)end); + } + + ucsan_label *shadow = ucsan_shadow_for(ptr); + for (size_t i = 0; i < n; i++) { + off_t byte_off = pos + (off_t)i; + + ucsan_label byte_label = allocate_label(); + check_label(byte_label); + + ucsan_label_info *info = get_label_info(byte_label); + ucsan_byte_info *byte = to_byte_info(info); + byte->op = OP_NONE; + byte->object_id = state->object_id; + byte->offset = byte_off; + + shadow[i] = byte_label; + + // Bridge to SymSan: a symbolic 1-byte label rooted at the file object. + void *byte_addr = (void *)((uintptr_t)ptr + i); + dfsan_label symsan_label = __taint_create_label(state->object_id, (uint64_t)byte_off, 1); + __taint_set_label(symsan_label, byte_addr, 1); + + // Copy concrete byte from obj.data (zero on first run, seed value on re-runs). + ((uint8_t *)ptr)[i] = obj.data.at((uint32_t)byte_off); + } + + return n; +} + +// ===== open/fopen wrappers ===== +// These do NOT forward to libc — we return a unique opaque handle so +// reads can be fully simulated. Other libc operations on the returned +// handle (fstat, ferror, ...) are not supported. + +// SymSan retval TLS is sticky: handleUCSanCall always emits a load from +// __dfsan_retval_tls after each __dfsw_* call, so any wrapper that doesn't +// store a meaningful label there will read whatever the previous call left +// behind (notably, fgetc's byte label). Clear retval slot 0 explicitly in +// wrappers whose return value carries no symbolic content. +__attribute__((visibility("default"))) +int __dfsw_open(const char *path, int oflags, ucsan_label path_label, + ucsan_label oflags_label, ucsan_label *va_labels, + ucsan_label *ret_label, ...) { + int fake_fd = __ucsan_fake_fd_counter++; + UCSAN_OUT("__dfsw_open(path=%s, oflags=%d) = fake fd %d\n", + path ? path : "(null)", oflags, fake_fd); + // The returned fd is just an opaque concrete handle; symbolic content comes + // from the file object anchored in obj0 at this open call. + ucsan_file_state *s = open_file_state(concrete_file_size(path)); + s->fd = fake_fd; + s->stream = nullptr; + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + return fake_fd; +} + +__attribute__((visibility("default"))) +int __dfsw_openat(int dirfd, const char *path, int oflags, + ucsan_label dirfd_label, ucsan_label path_label, + ucsan_label oflags_label, ucsan_label *va_labels, + ucsan_label *ret_label, ...) { + int fake_fd = __ucsan_fake_fd_counter++; + UCSAN_OUT("__dfsw_openat(dirfd=%d, path=%s, oflags=%d) = fake fd %d\n", + dirfd, path ? path : "(null)", oflags, fake_fd); + ucsan_file_state *s = open_file_state(concrete_file_size(path)); + s->fd = fake_fd; + s->stream = nullptr; + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + return fake_fd; +} + +__attribute__((visibility("default"))) +FILE *__dfsw_fopen(const char *filename, const char *mode, + ucsan_label filename_label, ucsan_label mode_label, + ucsan_label *ret_label) { + FILE *fake = (FILE *)(__ucsan_fake_file_counter++); + UCSAN_OUT("__dfsw_fopen(filename=%s, mode=%s) = fake FILE* %p\n", + filename ? filename : "(null)", mode ? mode : "(null)", fake); + ucsan_file_state *s = open_file_state(concrete_file_size(filename)); + s->fd = __ucsan_fake_fd_counter++; + s->stream = fake; + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(FILE*) * 8); + return fake; +} + +__attribute__((visibility("default"))) +FILE *__dfsw_fopen64(const char *filename, const char *mode, + ucsan_label filename_label, ucsan_label mode_label, + ucsan_label *ret_label) { + return __dfsw_fopen(filename, mode, filename_label, mode_label, ret_label); +} + +__attribute__((visibility("default"))) +FILE *__dfsw_freopen(const char *filename, const char *mode, FILE *stream, + ucsan_label filename_label, ucsan_label mode_label, + ucsan_label stream_label, ucsan_label *ret_label) { + // Single-file simulator: reset position; reissue a fresh fake FILE*. + __ucsan_file.offset = 0; + return __dfsw_fopen(filename, mode, filename_label, mode_label, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_close(int fd, ucsan_label fd_label, ucsan_label *ret_label) { + // Single-file simulator: no per-handle state to tear down. Reset offset + // so a later open()+read() starts at byte 0 like a fresh handle would. + __ucsan_file.offset = 0; + __ucsan_file.fd = -1; + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + return 0; +} + +__attribute__((visibility("default"))) +int __dfsw_fclose(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + __ucsan_file.offset = 0; + __ucsan_file.fd = -1; + __ucsan_file.stream = nullptr; + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + return 0; +} + +__attribute__((visibility("default"))) +int __dfsw_fileno(FILE *stream, ucsan_label stream_label, + ucsan_label *ret_label) { + ucsan_file_state *s = get_file_state(); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + if (stream == s->stream) + return s->fd; + return s->fd >= 0 ? s->fd : 0; +} + +__attribute__((visibility("default"))) +int __dfsw_fileno_unlocked(FILE *stream, ucsan_label stream_label, + ucsan_label *ret_label) { + return __dfsw_fileno(stream, stream_label, ret_label); +} + +static int simulate_fstat(int fd, struct stat *buf, ucsan_label *ret_label) { + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + ucsan_file_state *s = get_file_state(); + if (!buf) + return -1; + internal_memset(buf, 0, sizeof(struct stat)); + __taint_set_label(0, buf, sizeof(struct stat)); + buf->st_mode = S_IFREG | 0600; + buf->st_nlink = 1; + buf->st_size = simulated_file_size(s); + UCSAN_OUT("__dfsw_fstat(fd=%d) simulated st_size=%ld\n", + fd, (long)buf->st_size); + return 0; +} + +__attribute__((visibility("default"))) +int __dfsw_ucsan_fstat(int fd, struct stat *buf, ucsan_label fd_label, + ucsan_label buf_label, ucsan_label *ret_label) { + return simulate_fstat(fd, buf, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_ucsan_fstat64(int fd, struct stat *buf, ucsan_label fd_label, + ucsan_label buf_label, ucsan_label *ret_label) { + return simulate_fstat(fd, buf, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_ucsan_fxstat(int vers, int fd, struct stat *buf, + ucsan_label vers_label, ucsan_label fd_label, + ucsan_label buf_label, ucsan_label *ret_label) { + return simulate_fstat(fd, buf, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_ucsan_fxstat64(int vers, int fd, struct stat *buf, + ucsan_label vers_label, ucsan_label fd_label, + ucsan_label buf_label, ucsan_label *ret_label) { + return simulate_fstat(fd, buf, ret_label); +} + +// ===== seek wrappers — just update the simulator offset ===== + +static void file_seek(ucsan_file_state *s, off_t offset, int whence) { + if (!s) return; + off_t new_off; + switch (whence) { + case SEEK_SET: new_off = offset; break; + case SEEK_CUR: new_off = s->offset + offset; break; + case SEEK_END: + // Object grows on demand — current size is our best "end" estimate. + if (s->object_id != 0 && s->object_id < ucsan_tainted.objects->size()) { + new_off = (off_t)ucsan_tainted.objects->at(s->object_id).data.size() + offset; + } else { + new_off = offset; + } + break; + default: return; + } + if (new_off < 0) new_off = 0; + s->offset = new_off; +} + +__attribute__((visibility("default"))) +int __dfsw_fseek(FILE *stream, long offset, int whence, + ucsan_label stream_label, ucsan_label offset_label, + ucsan_label whence_label, ucsan_label *ret_label) { + file_seek(get_file_state(), (off_t)offset, whence); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + return 0; +} + +__attribute__((visibility("default"))) +int __dfsw_fseeko(FILE *stream, off_t offset, int whence, + ucsan_label stream_label, ucsan_label offset_label, + ucsan_label whence_label, ucsan_label *ret_label) { + file_seek(get_file_state(), offset, whence); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(int) * 8); + return 0; +} + +__attribute__((visibility("default"))) +off_t __dfsw_lseek(int fd, off_t offset, int whence, + ucsan_label fd_label, ucsan_label offset_label, + ucsan_label whence_label, ucsan_label *ret_label) { + ucsan_file_state *s = get_file_state(); + file_seek(s, offset, whence); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(off_t) * 8); + return s->offset; +} + +__attribute__((visibility("default"))) +void __dfsw_rewind(FILE *stream, ucsan_label stream_label) { + get_file_state()->offset = 0; +} + +__attribute__((visibility("default"))) +long __dfsw_ftell(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(long) * 8); + return (long)get_file_state()->offset; +} + +// ===== read wrappers — simulate via the file UC object ===== + +__attribute__((visibility("default"))) +ssize_t __dfsw_read(int fd, void *buf, size_t count, + ucsan_label fd_label, ucsan_label buf_label, + ucsan_label count_label, ucsan_label *ret_label) { + UCSAN_OUT("__dfsw_read(fd=%d, buf=%p, count=%zu)\n", fd, buf, count); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(ssize_t) * 8); + if (!buf || count == 0) return 0; + ucsan_file_state *s = get_file_state(); + size_t did = simulate_file_read(s, buf, count, s->offset); + s->offset += (off_t)did; + return (ssize_t)did; +} + +__attribute__((visibility("default"))) +ssize_t __dfsw_pread(int fd, void *buf, size_t count, off_t offset, + ucsan_label fd_label, ucsan_label buf_label, + ucsan_label count_label, ucsan_label offset_label, + ucsan_label *ret_label) { + UCSAN_OUT("__dfsw_pread(fd=%d, buf=%p, count=%zu, offset=%ld)\n", fd, buf, count, (long)offset); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(ssize_t) * 8); + if (!buf || count == 0) return 0; + // pread() does not advance the kernel file offset, and per the design + // it shouldn't advance ours either — just symbolize from offset. + ucsan_file_state *s = get_file_state(); + size_t did = simulate_file_read(s, buf, count, offset); + return (ssize_t)did; +} + +__attribute__((visibility("default"))) +ssize_t __dfsw_pread64(int fd, void *buf, size_t count, off_t offset, + ucsan_label fd_label, ucsan_label buf_label, + ucsan_label count_label, ucsan_label offset_label, + ucsan_label *ret_label) { + return __dfsw_pread(fd, buf, count, offset, fd_label, buf_label, + count_label, offset_label, ret_label); +} + +__attribute__((visibility("default"))) +size_t __dfsw_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, + ucsan_label ptr_label, ucsan_label size_label, + ucsan_label nmemb_label, ucsan_label stream_label, + ucsan_label *ret_label) { + UCSAN_OUT("__dfsw_fread(ptr=%p, size=%zu, nmemb=%zu, stream=%p)\n", + ptr, size, nmemb, stream); + *ret_label = 0; + __taint_set_retval_tls(0, 0, sizeof(size_t) * 8); + if (!ptr || size == 0 || nmemb == 0) return 0; + ucsan_file_state *s = get_file_state(); + size_t total = size * nmemb; + size_t did = simulate_file_read(s, ptr, total, s->offset); + s->offset += (off_t)did; + // fread returns count of full elements actually read. + return did / size; +} + +__attribute__((visibility("default"))) +size_t __dfsw_fread_unlocked(void *ptr, size_t size, size_t nmemb, FILE *stream, + ucsan_label ptr_label, ucsan_label size_label, + ucsan_label nmemb_label, ucsan_label stream_label, + ucsan_label *ret_label) { + return __dfsw_fread(ptr, size, nmemb, stream, ptr_label, size_label, + nmemb_label, stream_label, ret_label); +} + +// ===== fgetc / getc family — single-byte read returned as int ===== + +static int file_getc(ucsan_file_state *s, ucsan_label *ret_label) { + if (!s) { *ret_label = 0; return -1; } + uint8_t byte = 0; + size_t did = simulate_file_read(s, &byte, 1, s->offset); + if (did == 0) { *ret_label = 0; return -1; } + // The byte we just wrote into &byte carries its file-byte label in shadow. + // Lift that label into the int return value's ret_label slot. The int's + // upper bits are concrete-zero — only the low 8 bits are symbolic, which + // matches how the IR will zero-extend the byte into the int register. + ucsan_label *shadow = ucsan_shadow_for(&byte); + *ret_label = shadow[0]; + s->offset += 1; + + // Bridge to SymSan: create an 8-bit label for the byte, then ZExt it to 32 + // bits to match fgetc's int return type (C-standard: byte 0..255 zero-extended + // into int, EOF == -1 handled by the concrete return path above). Z3 errors + // on width mismatch between the loaded retval TLS shadow and the i32 return. + dfsan_label byte_label = __taint_create_label(s->object_id, (uint64_t)(s->offset - 1), 1); + dfsan_label int_label = __taint_extend_label(byte_label, /*sign_extend=*/false, 32); + __taint_set_retval_tls(0, int_label, 32); + + return (int)byte; +} + +__attribute__((visibility("default"))) +int __dfsw_fgetc(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + return file_getc(get_file_state(), ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_fgetc_unlocked(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + return __dfsw_fgetc(stream, stream_label, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_getc(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + return __dfsw_fgetc(stream, stream_label, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_getc_unlocked(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + return __dfsw_fgetc(stream, stream_label, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw__IO_getc(FILE *stream, ucsan_label stream_label, ucsan_label *ret_label) { + return __dfsw_fgetc(stream, stream_label, ret_label); +} + +__attribute__((visibility("default"))) +int __dfsw_getchar(ucsan_label *ret_label) { + return file_getc(get_file_state(), ret_label); +} + +} // extern "C" diff --git a/runtime/dfsan/ucsan_exit_reason.h b/runtime/dfsan/ucsan_exit_reason.h new file mode 100644 index 00000000..69ca3f24 --- /dev/null +++ b/runtime/dfsan/ucsan_exit_reason.h @@ -0,0 +1,23 @@ +enum exit_reason { + /* internal */ + REASON_LOOP_OOB = 123, + REASON_OBJ_OOB = 124, + REASON_STACK_OOB = 125, + + /* checker */ + EVENT_UBI = 150, + EVENT_UAF = 151, + EVENT_OOB = 152, + EVENT_NULL_DEREF = 153, + EVENT_DIV_BY_ZERO = 154, + EVENT_INT_OVERFLOW = 155, + EVENT_DOUBLE_FREE = 156, + EVENT_MEMLEAK = 157, + + EVENT_OOB_UPCAST = 161, + + EVENT_PANIC = 171, + + /* assumption assertion errors */ + EVENT_CONTRACTION = 201, +}; diff --git a/runtime/dfsan/ucsan_flags.inc b/runtime/dfsan/ucsan_flags.inc new file mode 100644 index 00000000..1321a01f --- /dev/null +++ b/runtime/dfsan/ucsan_flags.inc @@ -0,0 +1,44 @@ +//===-- ucsan_flags.inc -----------------------------------------*- C++ -*-===// +// +// UCSan runtime flags. +// +//===----------------------------------------------------------------------===// +// +// UCSan runtime flags are defined here using the UCSAN_FLAG macro. +// +//===----------------------------------------------------------------------===// + +#ifndef UCSAN_FLAG +#error "Define UCSAN_FLAG prior to including this file!" +#endif + +UCSAN_FLAG(bool, debug, false, + "Enable debug output for UCSan runtime.") + +UCSAN_FLAG(bool, trace_object, false, + "Trace object lazy initializations.") + +UCSAN_FLAG(bool, checker_nullderef, false, + "Check for null pointer dereferences.") + +UCSAN_FLAG(bool, no_upcast, false, + "Disallow upcast (negative offset in container_of).") + +UCSAN_FLAG(bool, trace_bounds, false, + "Enable bounds tracking and exit on OOB/UAF detection.") + +UCSAN_FLAG(bool, no_enlarge, false, + "Disallow object enlargement beyond initial size.") + +UCSAN_FLAG(int, max_obj_size, 0, + "Maximum object size for lazy initialization " + "(0 = use UCSAN_OBJECT_SIZE_LIMIT).") + +UCSAN_FLAG(const char *, input_file, "", + "Input file containing seed objects.") + +UCSAN_FLAG(bool, disable_loop_bounds, false, + "Disable loop unrolling bounds.") + +UCSAN_FLAG(bool, check_memleak, false, + "Check for memory leaks.") \ No newline at end of file diff --git a/runtime/dfsan/ucsan_platform.h b/runtime/dfsan/ucsan_platform.h new file mode 100644 index 00000000..5be8313b --- /dev/null +++ b/runtime/dfsan/ucsan_platform.h @@ -0,0 +1,93 @@ +//===-- ucsan_platform.h - UCSan Platform Definitions ----------*- C++ -*-===// +// +// UCSan shadow memory and platform-specific definitions (x86_64 only). +// +// UCSan uses 2-byte (16-bit) labels for pointer/object tracking. +// This is separate from SymSan/DFSan which uses 4-byte labels. +// +// Shadow computation: +// ucsan_shadow = ((ptr & ShadowMask) << 1) + kShadowBase +// +// Layout: shadow first, then union table (no hash table needed). +// +//===----------------------------------------------------------------------===// + +#ifndef UCSAN_PLATFORM_H +#define UCSAN_PLATFORM_H + +#include "defs.h" +#include "sanitizer_common/sanitizer_internal_defs.h" + +using __sanitizer::uptr; + +namespace __ucsan { + +//===----------------------------------------------------------------------===// +// UCSan Label Type (16-bit) +//===----------------------------------------------------------------------===// + +typedef u16 ucsan_label; + +#define UCSAN_LABEL_MAX 0xFFFF +#define UCSAN_CONST_LABEL 0 +#define UCSAN_CONST_OFFSET 1 + +//===----------------------------------------------------------------------===// +// Memory Layout for UCSan on Linux/x86_64 +//===----------------------------------------------------------------------===// +// +// SymSan/DFSan layout (unchanged): +// Shadow: 0x000000010000 (4-byte labels) +// HashTable: 0x400000000000 +// UnionTable: 0x400100000000 +// +// UCSan layout (separate, 2-byte labels): +// Shadow: 0x480000000000 (2TB, ends at 0x680000000000) +// UnionTable: 0x680000000000 +// +// App: 0x700000040000 - 0x800000000000 +// + +static const uptr kShadowBase = 0x480000000000ULL; +static const uptr kUnionTableAddr = 0x680000000000ULL; +static const uptr kAppAddr = 0x700000040000ULL; +static const uptr kAppBaseAddr = 0x700000000000ULL; +static const uptr kShadowMask = ~0x700000000000ULL; + +// With 16-bit labels, max 65536 labels +// Each label_info is 24 bytes, so max size = 65536 * 24 = 1.5MB +// Allocate 4MB for safety +static const uptr kUnionTableSize = 0x400000ULL; + +//===----------------------------------------------------------------------===// +// Accessors +//===----------------------------------------------------------------------===// + +inline uptr ShadowBase() { return kShadowBase; } +inline uptr UnionTableAddr() { return kUnionTableAddr; } +inline uptr AppAddr() { return kAppAddr; } +inline uptr AppBaseAddr() { return kAppBaseAddr; } +inline uptr ShadowMask() { return kShadowMask; } +inline uptr UnusedAddr() { return kUnionTableAddr + kUnionTableSize; } + +//===----------------------------------------------------------------------===// +// Shadow Memory Access (2-byte labels, shift by 1) +//===----------------------------------------------------------------------===// + +// ucsan_shadow = ((ptr & ShadowMask) << 1) + ShadowBase +inline ucsan_label* ucsan_shadow_for(void *ptr) { + return (ucsan_label*)(((((uptr)ptr) & kShadowMask) << 1) + kShadowBase); +} + +inline ucsan_label* ucsan_shadow_for(const void *ptr) { + return ucsan_shadow_for(const_cast(ptr)); +} + +inline void* ucsan_app_for(const ucsan_label *l) { + uptr shadow_offset = ((uptr)l) - kShadowBase; + return (void*)((shadow_offset >> 1) | kAppBaseAddr); +} + +} // namespace __ucsan + +#endif // UCSAN_PLATFORM_H diff --git a/solvers/z3-ts.cpp b/solvers/z3-ts.cpp index 2b9f9e15..4aae3b6e 100644 --- a/solvers/z3-ts.cpp +++ b/solvers/z3-ts.cpp @@ -3,15 +3,21 @@ #include "parse-z3.h" #include +#include #include #include #include #include +#include +#include + using namespace symsan; #define FILTER_WRONG_AST 1 +static const uint64_t MAX_STRLEN_EXTEND = 4096; + static const std::unordered_map OP_MAP { {__dfsan::Extract, "Extract"}, {__dfsan::Trunc, "Trunc"}, @@ -54,6 +60,7 @@ static const std::unordered_map OP_MAP { {__dfsan::fstrcat, "strcat"}, {__dfsan::fprefixof, "prefixof"}, {__dfsan::fsuffixof, "suffixof"}, + {__dfsan::flength, "length"}, }; static std::string get_op_name(uint32_t op) { @@ -120,18 +127,20 @@ static std::vector decode_z3_string(const std::string &str) { } void Z3AstParser::dump_value_cache(dfsan_label label) { - if (label >= value_cache_.size()) { - throw z3::exception("invalid label for value cache"); + if (label < CONST_OFFSET || label >= size_) { + fprintf(stderr, " label %u: out of range\n", label); + return; } dfsan_label_info *info = get_label_info(label); - fprintf(stderr, "label %u = l1: %u, l2: %u, op: %s, size: %u, op1: %lu, op2: %lu\n", - label, info->l1, info->l2, get_op_name(info->op).c_str(), info->size, - info->op1.i, info->op2.i); - fprintf(stderr, "recalcuated value: %lu = op1: %lu, op2: %lu\n", - value_cache_[label], value_cache_[info->l1], value_cache_[info->l2]); - if (info->l1 != 0) + fprintf(stderr, " label %u = (l1:%u, l2:%u, op:%s(0x%x), size:%u, op1:%lu, op2:%lu)", + label, info->l1, info->l2, get_op_name(info->op).c_str(), info->op, + info->size, info->op1.i, info->op2.i); + if (label < value_cache_.size()) + fprintf(stderr, " val:%lu", value_cache_[label]); + fprintf(stderr, "\n"); + if (info->l1 >= CONST_OFFSET) dump_value_cache(info->l1); - if (info->l2 != 0) + if (info->l2 >= CONST_OFFSET) dump_value_cache(info->l2); } @@ -140,13 +149,16 @@ Z3AstParser::Z3AstParser(void *base, size_t size, z3::context &context) input_name_format = "input-%u-%u"; atoi_name_format = "atoi-%u-%u-%d-%lu"; // input, offset, base, original_len strlen_name_format = "strlen-%u-%u-%lu-%u"; // input, offset, original_len, null_from_input + str_name_format = "str-%u-%u-%u"; // input, offset, length + int_name_format = "int-%u-%u-%u"; // input, offset, bits } -int Z3AstParser::restart(std::vector &inputs) { +int Z3AstParser::restart(std::vector &inputs, bool copy_input) { // reset caches memcmp_cache_.clear(); string_ranges_.clear(); + string_ranges_.resize(inputs.size()); // vector indexed by input_id tsize_cache_.clear(); tsize_cache_.resize(1); // reserve for CONST_OFFSET for (Z3_ast ast : expr_cache_) { @@ -162,19 +174,68 @@ int Z3AstParser::restart(std::vector &inputs) { value_cache_.clear(); value_cache_.resize(1); // reserve for CONST_OFFSET #endif + // Label-level tracking caches + is_label_bv_.clear(); + is_label_bv_.resize(1); // reserve for CONST_OFFSET + is_label_seq_.clear(); + is_label_seq_.resize(1); // reserve for CONST_OFFSET + + aux_constraints_.clear(); + minimize_hints_.clear(); string_info_cache_.clear(); + int_var_cache_.clear(); branch_deps_.clear(); + neg_branch_deps_.clear(); branch_deps_.resize(inputs.size()); - for (size_t i = 0; i < inputs.size(); i++) { auto &input = inputs[i]; -#if FILTER_WRONG_AST - inputs_cache_.emplace_back(input.first, input.second); -#endif - // resize branch_deps_ branch_deps_[i].resize(input.second); } + // Clear old cached data + inputs_cache_.clear(); + inputs_copy_.clear(); + +#if FILTER_WRONG_AST + if (copy_input) { + // Copy input data and point inputs_cache_ to our owned copies + inputs_copy_.resize(inputs.size()); + for (size_t i = 0; i < inputs.size(); i++) { + auto &input = inputs[i]; + inputs_copy_[i].assign(input.first, input.first + input.second); + inputs_cache_.emplace_back(inputs_copy_[i].data(), inputs_copy_[i].size()); + } + } else { + // Just store pointers (caller must keep data alive) + for (size_t i = 0; i < inputs.size(); i++) { + auto &input = inputs[i]; + inputs_cache_.emplace_back(input.first, input.second); + } + } +#endif + + return 0; +} + +int Z3AstParser::update_input(std::vector &inputs, bool copy_input) { +#if FILTER_WRONG_AST + inputs_cache_.clear(); + inputs_copy_.clear(); + + if (copy_input) { + inputs_copy_.resize(inputs.size()); + for (size_t i = 0; i < inputs.size(); i++) { + auto &input = inputs[i]; + inputs_copy_[i].assign(input.first, input.first + input.second); + inputs_cache_.emplace_back(inputs_copy_[i].data(), inputs_copy_[i].size()); + } + } else { + for (size_t i = 0; i < inputs.size(); i++) { + auto &input = inputs[i]; + inputs_cache_.emplace_back(input.first, input.second); + } + } +#endif return 0; } @@ -192,13 +253,16 @@ z3::expr Z3AstParser::read_concrete(dfsan_label label, uint16_t size) { } static z3::expr get_cmd(z3::expr const &lhs, z3::expr const &rhs, uint32_t predicate) { + // For Int operands, unsigned comparisons reduce to regular Int comparisons + // since Int variables are bounded to [0, 2^bits) by aux_constraints_ + bool is_int = lhs.get_sort().is_int(); switch (predicate) { case __dfsan::bveq: return lhs == rhs; case __dfsan::bvneq: return lhs != rhs; - case __dfsan::bvugt: return z3::ugt(lhs, rhs); - case __dfsan::bvuge: return z3::uge(lhs, rhs); - case __dfsan::bvult: return z3::ult(lhs, rhs); - case __dfsan::bvule: return z3::ule(lhs, rhs); + case __dfsan::bvugt: return is_int ? (lhs > rhs) : z3::ugt(lhs, rhs); + case __dfsan::bvuge: return is_int ? (lhs >= rhs) : z3::uge(lhs, rhs); + case __dfsan::bvult: return is_int ? (lhs < rhs) : z3::ult(lhs, rhs); + case __dfsan::bvule: return is_int ? (lhs <= rhs) : z3::ule(lhs, rhs); case __dfsan::bvsgt: return lhs > rhs; case __dfsan::bvsge: return lhs >= rhs; case __dfsan::bvslt: return lhs < rhs; @@ -219,42 +283,27 @@ static bool eval_icmp(uint16_t predicate, uint64_t val1, uint64_t val2, uint8_t case __dfsan::bvuge: return val1 >= val2; case __dfsan::bvult: return val1 < val2; case __dfsan::bvule: return val1 <= val2; - case __dfsan::bvsgt: - switch(bits) { - case 8: return (int8_t)val1 > (int8_t)val2; - case 16: return (int16_t)val1 > (int16_t)val2; - case 32: return (int32_t)val1 > (int32_t)val2; - case 64: return (int64_t)val1 > (int64_t)val2; - default: - throw z3::exception("unsupported bits for signed comparison"); - } - case __dfsan::bvsge: - switch(bits) { - case 8: return (int8_t)val1 >= (int8_t)val2; - case 16: return (int16_t)val1 >= (int16_t)val2; - case 32: return (int32_t)val1 >= (int32_t)val2; - case 64: return (int64_t)val1 >= (int64_t)val2; - default: - throw z3::exception("unsupported bits for signed comparison"); - } - case __dfsan::bvslt: - switch(bits) { - case 8: return (int8_t)val1 < (int8_t)val2; - case 16: return (int16_t)val1 < (int16_t)val2; - case 32: return (int32_t)val1 < (int32_t)val2; - case 64: return (int64_t)val1 < (int64_t)val2; - default: - throw z3::exception("unsupported bits for signed comparison"); - } - case __dfsan::bvsle: - switch(bits) { - case 8: return (int8_t)val1 <= (int8_t)val2; - case 16: return (int16_t)val1 <= (int16_t)val2; - case 32: return (int32_t)val1 <= (int32_t)val2; - case 64: return (int64_t)val1 <= (int64_t)val2; - default: - throw z3::exception("unsupported bits for signed comparison"); - } + case __dfsan::bvsgt: { + // sign-extend to 64-bit for arbitrary widths + int64_t s1 = (int64_t)(val1 << (64 - bits)) >> (64 - bits); + int64_t s2 = (int64_t)(val2 << (64 - bits)) >> (64 - bits); + return s1 > s2; + } + case __dfsan::bvsge: { + int64_t s1 = (int64_t)(val1 << (64 - bits)) >> (64 - bits); + int64_t s2 = (int64_t)(val2 << (64 - bits)) >> (64 - bits); + return s1 >= s2; + } + case __dfsan::bvslt: { + int64_t s1 = (int64_t)(val1 << (64 - bits)) >> (64 - bits); + int64_t s2 = (int64_t)(val2 << (64 - bits)) >> (64 - bits); + return s1 < s2; + } + case __dfsan::bvsle: { + int64_t s1 = (int64_t)(val1 << (64 - bits)) >> (64 - bits); + int64_t s2 = (int64_t)(val2 << (64 - bits)) >> (64 - bits); + return s1 <= s2; + } default: throw z3::exception("unsupported predicate"); return false; // unsupported predicate @@ -263,6 +312,55 @@ static bool eval_icmp(uint16_t predicate, uint64_t val1, uint64_t val2, uint8_t // std::unreachable(); } +uint64_t Z3AstParser::serialize_input(dfsan_label label, uint32_t input, uint32_t offset, + uint32_t bytes, input_dep_set_t &input_deps) { + char name[256]; + snprintf(name, sizeof(name), input_name_format, input, offset); + z3::symbol symbol = context_.str_symbol(name); + z3::sort sort = context_.bv_sort(8); + z3::expr first_byte = context_.constant(symbol, sort); + z3::expr out = first_byte; + { // for ucsan, due to lazy init, the input may be empty + if (input >= branch_deps_.size()) branch_deps_.resize(input + 1); + if (is_negative_offset(offset) && input >= neg_branch_deps_.size()) { + neg_branch_deps_.resize(input + 1); + } + } + // Load operation can load overlapping bytes, so we need to check + if (get_branch_dep({input, offset}) == nullptr) { + set_branch_dep({input, offset}, std::make_unique(first_byte)); + } + input_deps.insert(std::make_pair(input, offset)); + uint64_t val = 0; +#if FILTER_WRONG_AST + if (!is_negative_offset(offset) && inputs_cache_.size() > input && + inputs_cache_[input].second > offset) { + val = (uint64_t)inputs_cache_[input].first[offset]; + } +#endif + for (uint32_t i = 1; i < bytes; i++) { + snprintf(name, sizeof(name), input_name_format, input, offset + i); + symbol = context_.str_symbol(name); + z3::expr byte_expr = context_.constant(symbol, sort); + out = z3::concat(byte_expr, out); + if (get_branch_dep({input, offset + i}) == nullptr) { + set_branch_dep({input, offset + i}, std::make_unique(byte_expr)); + } + input_deps.insert(std::make_pair(input, offset + i)); +#if FILTER_WRONG_AST + if (!is_negative_offset(offset + i) && inputs_cache_.size() > input && + inputs_cache_[input].second > offset + i) { + val |= (uint64_t)inputs_cache_[input].first[offset + i] << (i * 8); + } +#endif + } + + tsize_cache_.emplace_back(1); + cache_expr(label, out); + + return val; +} + z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { if (label < CONST_OFFSET || label == __dfsan::kInitializingLabel) { throw z3::exception("invalid label"); @@ -277,6 +375,8 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { #if FILTER_WRONG_AST value_cache_.reserve(label + SIZE_INCREMENT); #endif + is_label_bv_.reserve(label + SIZE_INCREMENT); + is_label_seq_.reserve(label + SIZE_INCREMENT); } for (dfsan_label l = last_label + 1; l <= label; l++) { @@ -289,6 +389,28 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { do { } while (0) #endif +// Helper macros for tracking label types (bitvec vs string/seq) +// Most ops are bitvec; string ops are seq; And/Or/ICmp propagate from children +#define TRACK_LABEL_BV_ONLY() \ + is_label_bv_.emplace_back(true); \ + is_label_seq_.emplace_back(false) + +#define TRACK_LABEL_SEQ_ONLY() \ + is_label_bv_.emplace_back(false); \ + is_label_seq_.emplace_back(true) + +#define TRACK_LABEL_PROPAGATE_BOTH() \ + do { \ + bool bv = (info->l1 >= CONST_OFFSET) ? is_label_bv_[info->l1] : false; \ + bool seq = (info->l1 >= CONST_OFFSET) ? is_label_seq_[info->l1] : false; \ + if (info->l2 >= CONST_OFFSET) { \ + bv = bv || is_label_bv_[info->l2]; \ + seq = seq || is_label_seq_[info->l2]; \ + } \ + is_label_bv_.emplace_back(bv); \ + is_label_seq_.emplace_back(seq); \ + } while (0) + dfsan_label_info *info = get_label_info(l); // fprintf(stderr, "%u = (l1:%u, l2:%u, op:%s, size:%u, op1:%lu, op2:%lu)\n", // l, info->l1, info->l2, get_op_name(info->op).c_str(), @@ -301,37 +423,15 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // input uint32_t offset = info->op1.i; // legacy: offset in op1 uint32_t input = info->op2.i; - snprintf(name, sizeof(name), input_name_format, input, offset); - z3::symbol symbol = context_.str_symbol(name); - z3::sort sort = context_.bv_sort(8); - tsize_cache_.emplace_back(1); - input_deps.insert(std::make_pair(input, offset)); - // caching is not super helpful - cache_expr(l, context_.constant(symbol, sort)); - RECORD_VALUE(inputs_cache_[input].first[offset]); + uint64_t val = serialize_input(l, input, offset, info->size / 8, input_deps); + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(val); continue; } else if (info->op == __dfsan::Load) { uint32_t offset = get_label_info(info->l1)->op1.i; // legacy: offset in op1 uint32_t input = get_label_info(info->l1)->op2.i; - snprintf(name, sizeof(name), input_name_format, input, offset); - z3::symbol symbol = context_.str_symbol(name); - z3::sort sort = context_.bv_sort(8); - z3::expr out = context_.constant(symbol, sort); - input_deps.insert(std::make_pair(input, offset)); -#if FILTER_WRONG_AST - uint64_t val = inputs_cache_[input].first[offset]; -#endif - for (uint32_t i = 1; i < info->l2; i++) { - snprintf(name, sizeof(name), input_name_format, input, offset + i); - symbol = context_.str_symbol(name); - out = z3::concat(context_.constant(symbol, sort), out); - input_deps.insert(std::make_pair(input, offset + i)); -#if FILTER_WRONG_AST - val |= (uint64_t)inputs_cache_[input].first[offset + i] << (i * 8); -#endif - } - tsize_cache_.emplace_back(1); - cache_expr(l, out); + uint64_t val = serialize_input(l, input, offset, info->l2, input_deps); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(val); continue; } else if (info->op == __dfsan::ZExt) { @@ -342,6 +442,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { uint32_t base_size = base.get_sort().bv_size(); tsize_cache_.emplace_back(tsize_cache_[info->l1]); cache_expr(l, z3::zext(base, info->size - base_size)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(value_cache_[info->l1] & ((1UL << base_size) - 1)); continue; } else if (info->op == __dfsan::SExt) { @@ -349,18 +450,31 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { uint32_t base_size = base.get_sort().bv_size(); tsize_cache_.emplace_back(tsize_cache_[info->l1]); cache_expr(l, z3::sext(base, info->size - base_size)); - RECORD_VALUE((int64_t)(value_cache_[info->l1] & ((1UL << base_size) - 1))); + TRACK_LABEL_BV_ONLY(); + // Sign extend: shift left to put sign bit at MSB, then arithmetic shift right + uint64_t base_val = value_cache_[info->l1] & ((1UL << base_size) - 1); + RECORD_VALUE(((int64_t)(base_val << (64 - base_size))) >> (64 - base_size)); continue; } else if (info->op == __dfsan::Trunc) { z3::expr base = get_cached_expr(info->l1, input_deps); tsize_cache_.emplace_back(tsize_cache_[info->l1]); - cache_expr(l, base.extract(info->size - 1, 0)); + if (!base.is_bv()) { + fprintf(stderr, "WARNING: Trunc on non-BV (label=%u, l1=%u, sort=%s)\n", + l, info->l1, base.get_sort().to_string().c_str()); + dump_value_cache(l); + } + z3::expr trunc_expr = base.extract(info->size - 1, 0); + if (info->size == 1) + trunc_expr = (trunc_expr == context_.bv_val(1, 1)); + cache_expr(l, trunc_expr); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(value_cache_[info->l1] & ((1UL << info->size) - 1)); continue; } else if (info->op == __dfsan::IntToPtr) { z3::expr e = get_cached_expr(info->l1, input_deps); tsize_cache_.emplace_back(tsize_cache_[info->l1]); cache_expr(l, e); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(value_cache_[info->l1]); continue; } else if (info->op == __dfsan::PtrToInt) { @@ -369,12 +483,23 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { if (info->l1 >= CONST_OFFSET) { dfsan_label_info *src_info = get_label_info(info->l1); if (src_info->op >= __dfsan::fstr_op_start && src_info->op < __dfsan::fstr_op_end) { - // String op result - the "pointer" is semantically the index - // Convert the Int expression to a bitvector for downstream ops + // String op result (indexof) is Int sort — keep it as Int + // instead of int2bv which creates expensive mixed BV+string theory. + // Downstream Add/Sub/ICmp will detect Int operands and stay in Int domain, + // converting BV operands to fresh Int variables (intbyte-...) as needed. z3::expr idx = get_cached_expr(info->l1, input_deps); - z3::expr bv_idx = z3::int2bv(info->size, idx); + // String index ops (indexof, last_indexof) return position relative + // to the string variable. But PtrToInt should give the absolute + // position from the start of the input buffer. Add the string's + // starting offset to convert relative -> absolute. + dfsan_label content_label = src_info->l1; // haystack content label + auto si_it = string_info_cache_.find(content_label); + if (si_it != string_info_cache_.end() && si_it->second.offset > 0) { + idx = idx + context_.int_val(si_it->second.offset); + } tsize_cache_.emplace_back(tsize_cache_[info->l1]); - cache_expr(l, bv_idx); + cache_expr(l, idx); // keep Int sort + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(value_cache_[info->l1]); continue; } @@ -383,6 +508,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { z3::expr e = get_cached_expr(info->l1, input_deps); tsize_cache_.emplace_back(tsize_cache_[info->l1]); cache_expr(l, e); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(value_cache_[info->l1]); continue; } //FIXME: other casting ops (BitCast)? @@ -390,7 +516,13 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { else if (info->op == __dfsan::Extract) { z3::expr base = get_cached_expr(info->l1, input_deps); tsize_cache_.emplace_back(tsize_cache_[info->l1]); + if (!base.is_bv()) { + fprintf(stderr, "WARNING: Extract on non-BV (label=%u, l1=%u, sort=%s)\n", + l, info->l1, base.get_sort().to_string().c_str()); + dump_value_cache(l); + } cache_expr(l, base.extract((info->op2.i + info->size) - 1, info->op2.i)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE((value_cache_[info->l1] >> info->op2.i) & ((1UL << info->size) - 1)); continue; @@ -404,6 +536,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { throw z3::exception("Only LNot should be recorded"); } cache_expr(l, !e); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(!value_cache_[info->l2]); continue; } else if (info->op == __dfsan::Neg) { @@ -413,6 +546,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { z3::expr e = get_cached_expr(info->l2, input_deps); tsize_cache_.emplace_back(tsize_cache_[info->l2]); cache_expr(l, -e); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(-value_cache_[info->l2]); continue; } @@ -429,7 +563,8 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { z3::expr e = z3::ite(op1 == op2, context_.bv_val(0, 32), context_.bv_val(1, 32)); cache_expr(l, e); - RECORD_VALUE(0); // memcmp result is always 0 or 1 + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(1); // memcmp result is always 0 or 1 continue; } else if (info->op == __dfsan::fsize) { // file size @@ -446,6 +581,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } else { cache_expr(l, base); } + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(0); // FIXME: map to input size continue; } else if (info->op == __dfsan::fatoi) { @@ -464,6 +600,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { z3::symbol symbol = context_.str_symbol(name); z3::sort sort = context_.bv_sort(info->size); cache_expr(l, context_.constant(symbol, sort)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(0); // FIXME: map to atoi result? continue; } else if (info->op == __dfsan::fstrlen) { @@ -508,8 +645,19 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { z3::symbol symbol = context_.str_symbol(name); z3::sort sort = context_.bv_sort(info->size); cache_expr(l, context_.constant(symbol, sort)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(info->op2.i); // actual length for value cache continue; + } else if (info->op == __dfsan::flength) { + // length(str_var) -> Int sort + // l1 = content label, l2 = 0, op2 = concrete length + z3::expr str_var = build_string_from_label(info->l1, input_deps); + z3::expr len_expr(context_, Z3_mk_seq_length(context_, str_var)); + tsize_cache_.emplace_back(1); + cache_expr(l, len_expr); // Int sort + TRACK_LABEL_SEQ_ONLY(); + RECORD_VALUE(info->op2.i); + continue; } else if (info->op == __dfsan::fstrchr) { // strchr/memchr: find character in string // l1 = source pointer label (content bytes, fsubstr, or previous strchr for chaining) @@ -588,14 +736,26 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { c_expr = c_expr.extract(7, 0); } code = z3::bv2int(c_expr, false); + // Prefer non-zero needle: avoids trivial solutions where the solver + // picks '\0' which already exists in unmodified buffer regions + aux_constraints_.push_back(code != context_.int_val(0)); } // Use Z3_mk_string_from_code to convert int to single-char String z3::expr target_str(context_, Z3_mk_string_from_code(context_, code)); z3::expr idx = z3::indexof(haystack_str, target_str, start_offset); + // Under UC the haystack pointer is symbolic; its label is stashed in the + // high 32 bits of op2 (the low 8 bits hold the needle char). The search + // result is base+index, so a match at index 0 collapses to a NULL pointer + // when the pseudo base is 0. Constrain the base pointer non-null so the + // solver picks a non-zero pseudo base; serialize() also pulls the pointer + // bytes into this op's deps so the constraint reaches the search branch. + add_haystack_ptr_nonnull((dfsan_label)(info->op2.i >> 32), input_deps); + tsize_cache_.emplace_back(1); cache_expr(l, idx); // cache the index expression (Int sort) + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops continue; } else if (info->op == __dfsan::fstrrchr) { @@ -643,6 +803,9 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { c_expr = c_expr.extract(7, 0); } code = z3::bv2int(c_expr, false); + // Prefer non-zero needle: avoids trivial solutions where the solver + // picks '\0' which already exists in unmodified buffer regions + aux_constraints_.push_back(code != context_.int_val(0)); } // Use Z3_mk_string_from_code to convert int to single-char String z3::expr target_str(context_, Z3_mk_string_from_code(context_, code)); @@ -650,8 +813,13 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // For reverse search, find the last occurrence z3::expr idx = z3::last_indexof(haystack_str, target_str); + // UC: constrain the (symbolic) haystack base pointer non-null; see fstrchr. + // The pointer label is stashed in op2's high bits (low 8 bits = char). + add_haystack_ptr_nonnull((dfsan_label)(info->op2.i >> 32), input_deps); + tsize_cache_.emplace_back(1); cache_expr(l, idx); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops continue; } else if (info->op == __dfsan::fstrstr) { @@ -736,6 +904,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { tsize_cache_.emplace_back(1); cache_expr(l, idx); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops continue; } else if (info->op == __dfsan::fstrpbrk) { @@ -825,6 +994,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { tsize_cache_.emplace_back(1); cache_expr(l, idx.simplify()); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops continue; } else if (info->op == __dfsan::fsubstr) { @@ -869,9 +1039,42 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } else { // Prefix mode: substr(str, 0, len) z3::expr len_expr = context_.int_val((int64_t)info->op1.i); + if (info->l2 < CONST_OFFSET && info->l1 >= CONST_OFFSET) { + // Concrete length substr: pin parent string length so Z3 cannot + // shrink it to empty (which trivially satisfies extract/indexof). + // The parent represents a fixed-size input region. + auto si_it = string_info_cache_.find(info->l1); + if (si_it != string_info_cache_.end()) { + aux_constraints_.push_back(z3::expr(context_, + Z3_mk_seq_length(context_, full_str)) == + context_.int_val(si_it->second.length)); + } + } if (info->l2 >= CONST_OFFSET) { // l2 is the string op label - its cached value is the index/length len_expr = get_cached_expr(info->l2, input_deps); + // seq_extract needs Int sort; convert BV when n comes from input bytes + if (len_expr.get_sort().is_bv()) { + // For direct input labels, reuse the named Int variable (int-id-off-bits) + // which sort coercion may have already constrained. Using the same name + // avoids expanding bv2int into an expensive polynomial in str.substr. + dfsan_label_info *l2_info = get_label_info(info->l2); + if (l2_info->op == 0) { + char intname[256]; + snprintf(intname, sizeof(intname), int_name_format, + l2_info->op2.i, l2_info->op1.i, l2_info->size); + z3::symbol sym = context_.str_symbol(intname); + len_expr = context_.constant(sym, context_.int_sort()); + // Ensure bounds and BV linkage exist (idempotent if sort coercion already added them) + aux_constraints_.push_back(len_expr >= 0); + aux_constraints_.push_back(len_expr == z3::expr(context_, + Z3_mk_bv2int(context_, get_cached_expr(info->l2, input_deps), false))); + // Cache int-* variable for Int mirroring of BV comparisons + int_var_cache_.emplace(info->l2, len_expr); + } else { + len_expr = z3::expr(context_, Z3_mk_bv2int(context_, len_expr, false)); + } + } } substr_expr = z3::expr(context_, Z3_mk_seq_extract(context_, full_str, @@ -881,6 +1084,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { tsize_cache_.emplace_back(1); cache_expr(l, substr_expr); + TRACK_LABEL_SEQ_ONLY(); // The substr itself doesn't have a numeric value, but downstream ops will use it RECORD_VALUE(info->op1.i); continue; @@ -939,6 +1143,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { tsize_cache_.emplace_back(1); cache_expr(l, concat_result); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); continue; } else if (info->op == __dfsan::fstrcmp) { @@ -1000,6 +1205,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { context_.bv_val(1, 32)); tsize_cache_.emplace_back(1); cache_expr(l, eq); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); continue; } else if (info->op == __dfsan::fprefixof) { @@ -1052,6 +1258,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { context_.bv_val(0, 32)); tsize_cache_.emplace_back(1); cache_expr(l, result); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); continue; } else if (info->op == __dfsan::fsuffixof) { @@ -1104,6 +1311,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { context_.bv_val(0, 32)); tsize_cache_.emplace_back(1); cache_expr(l, result); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); continue; } else if (info->op == __dfsan::fstr_off) { @@ -1122,12 +1330,14 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { tsize_cache_.emplace_back(tsize_cache_[info->l1]); cache_expr(l, offset_idx); + TRACK_LABEL_SEQ_ONLY(); // Record the concrete position + offset RECORD_VALUE(value_cache_[info->l1] + gep_offset); } else { // No base label, just return zero tsize_cache_.emplace_back(0); cache_expr(l, context_.int_val(0)); + TRACK_LABEL_SEQ_ONLY(); RECORD_VALUE(0); } continue; @@ -1135,6 +1345,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // not expression, do nothing tsize_cache_.emplace_back(0); expr_cache_.emplace_back(nullptr); + TRACK_LABEL_BV_ONLY(); // placeholder, doesn't matter RECORD_VALUE(0); continue; } @@ -1149,6 +1360,54 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { uint16_t l2_op = info->l2 >= CONST_OFFSET ? get_label_info(info->l2)->op : 0; bool l1_is_strfunc = (l1_op >= __dfsan::fstr_op_start && l1_op < __dfsan::fstr_op_end); bool l2_is_strfunc = (l2_op >= __dfsan::fstr_op_start && l2_op < __dfsan::fstr_op_end); + bool l1_is_flength = (l1_op == __dfsan::flength); + bool l2_is_flength = (l2_op == __dfsan::flength); + + if (l1_is_flength || l2_is_flength) { + // Length comparison - produce Int sort comparison + uint16_t predicate = info->op >> 8; + z3::expr len_expr = l1_is_flength + ? get_cached_expr(info->l1, input_deps) + : get_cached_expr(info->l2, input_deps); + // Get the other operand as Int + z3::expr other(context_); + if (l1_is_flength && info->l2 == 0) { + other = context_.int_val((uint64_t)info->op2.i); + } else if (l2_is_flength && info->l1 == 0) { + other = context_.int_val((uint64_t)info->op1.i); + } else { + // Both symbolic - get other operand as Int + dfsan_label other_l = l1_is_flength ? info->l2 : info->l1; + other = get_cached_expr(other_l, input_deps); + if (other.is_bv()) { + other = z3::expr(context_, Z3_mk_bv2int(context_, other, false)); + } + // else already Int (from sort coercion), use as-is + } + z3::expr cmp_expr(context_); + switch (predicate) { + case __dfsan::bvuge: cmp_expr = len_expr >= other; break; + case __dfsan::bvugt: cmp_expr = len_expr > other; break; + case __dfsan::bvule: cmp_expr = len_expr <= other; break; + case __dfsan::bvult: cmp_expr = len_expr < other; break; + case __dfsan::bveq: cmp_expr = len_expr == other; break; + case __dfsan::bvneq: cmp_expr = len_expr != other; break; + case __dfsan::bvsge: cmp_expr = len_expr >= other; break; + case __dfsan::bvsgt: cmp_expr = len_expr > other; break; + case __dfsan::bvsle: cmp_expr = len_expr <= other; break; + case __dfsan::bvslt: cmp_expr = len_expr < other; break; + default: throw z3::exception("unsupported predicate for flength comparison"); + } + tsize_cache_.emplace_back(tsize_cache_[info->l1] + tsize_cache_[info->l2]); + cache_expr(l, cmp_expr); + TRACK_LABEL_SEQ_ONLY(); + { + uint64_t cmp_result = eval_icmp(predicate, + (uint64_t)info->op1.i, (uint64_t)info->op2.i, 64) ? 1 : 0; + RECORD_VALUE(cmp_result); + } + continue; + } if (l1_is_strfunc || l2_is_strfunc) { // String function comparison - convert index to found/not-found @@ -1191,6 +1450,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { tsize_cache_.emplace_back(tsize_cache_[info->l1] + tsize_cache_[info->l2]); cache_expr(l, cmp_expr); + TRACK_LABEL_SEQ_ONLY(); // string function comparison is purely seq #if FILTER_WRONG_AST // For string ops, calculate value based on found/not-found semantics bool cmp_result = (predicate == __dfsan::bvneq) ? found : !found; @@ -1251,6 +1511,50 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } else if (info->size == 1) { op2 = context_.bool_val(info->op2.i == 1); } + + // Sort coercion: when one operand is Int (from string ops via PtrToInt) + // and the other is BV, convert the BV side to Int to avoid expensive int2bv. + // Tier 1: direct input bytes get a fresh named Int variable (int-id-offset-bits) + // Tier 2: complex BV expressions use bv2int (still cheaper than int2bv of indexof) + bool op1_is_int = op1.get_sort().is_int(); + bool op2_is_int = op2.get_sort().is_int(); + if (op1_is_int != op2_is_int) { + auto convert_bv_to_int = [&](z3::expr &bv_op, dfsan_label lbl, uint64_t concrete_val) { + if (lbl < CONST_OFFSET) { + // Constant — just use int_val + bv_op = context_.int_val(concrete_val); + } else { + dfsan_label_info *lbl_info = get_label_info(lbl); + if (lbl_info->op == 0) { + // Direct input byte — create named Int variable with bounds + char intname[256]; + snprintf(intname, sizeof(intname), int_name_format, + lbl_info->op2.i, lbl_info->op1.i, lbl_info->size); + z3::symbol sym = context_.str_symbol(intname); + z3::expr int_var = context_.constant(sym, context_.int_sort()); + aux_constraints_.push_back(int_var >= 0); + if (lbl_info->size < 64) { + aux_constraints_.push_back(int_var < context_.int_val((uint64_t)(1ULL << lbl_info->size))); + } + // Link Int variable to BV so generate_solution produces consistent values + aux_constraints_.push_back(int_var == z3::expr(context_, + Z3_mk_bv2int(context_, bv_op, false))); + bv_op = int_var; + // Cache int-* variable for Int mirroring of BV comparisons + int_var_cache_.emplace(lbl, bv_op); + } else { + // Complex BV expression — use bv2int (unsigned) + bv_op = z3::expr(context_, Z3_mk_bv2int(context_, bv_op, false)); + } + } + }; + if (!op1_is_int && op2_is_int) { + convert_bv_to_int(op1, info->l1, info->op1.i); + } else { + convert_bv_to_int(op2, info->l2, info->op2.i); + } + } + // update tree_size tsize_cache_.emplace_back(tsize_cache_[info->l1] + tsize_cache_[info->l2]); @@ -1258,36 +1562,49 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // llvm doesn't distinguish between logical and bitwise and/or/xor case __dfsan::And: { cache_expr(l, info->size != 1 ? (op1 & op2) : (op1 && op2)); + TRACK_LABEL_PROPAGATE_BOTH(); RECORD_VALUE((info->size != 1) ? (val1 & val2) : (val1 && val2)); break; } case __dfsan::Or: { cache_expr(l, info->size != 1 ? (op1 | op2) : (op1 || op2)); + TRACK_LABEL_PROPAGATE_BOTH(); RECORD_VALUE((info->size != 1) ? (val1 | val2) : (val1 || val2)); break; } case __dfsan::Xor: { - cache_expr(l, op1 ^ op2); + cache_expr(l, info->size != 1 ? (op1 ^ op2) : (op1 != op2)); + TRACK_LABEL_PROPAGATE_BOTH(); RECORD_VALUE(val1 ^ val2); break; } case __dfsan::Shl: { cache_expr(l, z3::shl(op1, op2)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(val1 << (val2 % size)); break; } case __dfsan::LShr: { cache_expr(l, z3::lshr(op1, op2)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(val1 >> (val2 % size)); break; } case __dfsan::AShr: { cache_expr(l, z3::ashr(op1, op2)); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE((int64_t)val1 >> (val2 % size)); break; } case __dfsan::Add: { cache_expr(l, op1 + op2); + if (op1_is_int || op2_is_int) { + // After sort coercion, result is pure Int - no BV variables remain. + // SEQ_ONLY prevents spurious linking of string bytes as used_in_bv. + TRACK_LABEL_SEQ_ONLY(); + } else { + TRACK_LABEL_BV_ONLY(); + } RECORD_VALUE(val1 + val2); break; } @@ -1302,25 +1619,55 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { if (src_info->op >= __dfsan::fstr_op_start && src_info->op < __dfsan::fstr_op_end) { // This is (PtrToInt(string_op)) - base_addr = index - // The expression is just the index (op1 already contains int2bv(idx)) + // The expression is just the index (op1 already contains the idx) cache_expr(l, op1); + TRACK_LABEL_SEQ_ONLY(); // The value is just the index, not idx - base_addr RECORD_VALUE(val1); break; } } } + // UC variant: (PtrToInt(string_op) - PtrToInt(base)) = index, when the + // base pointer is the search's own (symbolic) haystack pointer. The + // haystack pointer label is stashed in the search op's op2 high bits. + // Without this the solver sees Sub(index, base_value) and can satisfy + // ==k by adjusting the (free) base value, picking the wrong index. + if (info->l1 >= CONST_OFFSET && info->l2 >= CONST_OFFSET) { + dfsan_label_info *l1_info = get_label_info(info->l1); + dfsan_label_info *l2_info = get_label_info(info->l2); + if (l1_info->op == __dfsan::PtrToInt && l1_info->l1 >= CONST_OFFSET && + l2_info->op == __dfsan::PtrToInt && l2_info->l1 >= CONST_OFFSET) { + dfsan_label_info *src_info = get_label_info(l1_info->l1); + if (src_info->op >= __dfsan::fstr_op_start && + src_info->op < __dfsan::fstr_op_end && + (dfsan_label)(src_info->op2.i >> 32) == l2_info->l1) { + // base matches the haystack pointer: ptr - base = index + cache_expr(l, op1); + TRACK_LABEL_SEQ_ONLY(); + RECORD_VALUE(val1); + break; + } + } + } cache_expr(l, op1 - op2); + if (op1_is_int || op2_is_int) { + TRACK_LABEL_SEQ_ONLY(); + } else { + TRACK_LABEL_BV_ONLY(); + } RECORD_VALUE(val1 - val2); break; } case __dfsan::Mul: { cache_expr(l, op1 * op2); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(val1 * val2); break; } case __dfsan::UDiv: { cache_expr(l, z3::udiv(op1, op2)); + TRACK_LABEL_BV_ONLY(); if (val2 == 0) { fprintf(stderr, "WARNING: division by zero for label %u\n", l); RECORD_VALUE(0); @@ -1330,6 +1677,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } case __dfsan::SDiv: { cache_expr(l, op1 / op2); + TRACK_LABEL_BV_ONLY(); if (val2 == 0) { fprintf(stderr, "WARNING: division by zero for label %u\n", l); RECORD_VALUE(0); @@ -1339,6 +1687,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } case __dfsan::URem: { cache_expr(l, z3::urem(op1, op2)); + TRACK_LABEL_BV_ONLY(); if (val2 == 0) { fprintf(stderr, "WARNING: division by zero for label %u\n", l); RECORD_VALUE(0); @@ -1348,6 +1697,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } case __dfsan::SRem: { cache_expr(l, z3::srem(op1, op2)); + TRACK_LABEL_BV_ONLY(); if (val2 == 0) { fprintf(stderr, "WARNING: division by zero for label %u\n", l); RECORD_VALUE(0); @@ -1371,7 +1721,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { if ((info->op1.i & valmask) != val1 || (info->op2.i & valmask) != val2) { fprintf(stderr, "DEBUG serialize ICmp: VALUE MISMATCH detected\n"); - // fprintf(stderr, "WARNING: value mismatch for label %u:" + // fprintf(stderr, "WARNING: value mismatch for label %u: " // "expected op1 %lu, got %lu, expected op2 %lu, got %lu\n", // l, info->op1.i, val1, info->op2.i, val2); // fprintf(stderr, "cond: %s\n", get_cmd(op1, op2, info->op >> 8).to_string().c_str()); @@ -1410,6 +1760,13 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // Cache the expression AFTER updating value_cache to maintain consistency // if an exception is thrown above cache_expr(l, get_cmd(op1, op2, info->op >> 8)); + if (op1_is_int || op2_is_int) { + // After sort coercion, comparison is pure Int - no BV variables. + // SEQ_ONLY prevents spurious linking of string bytes. + TRACK_LABEL_SEQ_ONLY(); + } else { + TRACK_LABEL_PROPAGATE_BOTH(); + } break; } // concat @@ -1421,11 +1778,13 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // just use the String. The constant bytes don't contribute to constraints. if (!op1.is_bv() && info->l2 == 0) { cache_expr(l, op1); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(val1); break; } if (!op2.is_bv() && info->l1 == 0) { cache_expr(l, op2); + TRACK_LABEL_BV_ONLY(); RECORD_VALUE(val2); break; } @@ -1435,6 +1794,7 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { throw z3::exception("concat with non-bitvector operand (string op involved)"); } cache_expr(l, z3::concat(op2, op1)); // little endian + TRACK_LABEL_BV_ONLY(); RECORD_VALUE((val2 << op1.get_sort().bv_size()) | (val1)); break; } @@ -1464,7 +1824,19 @@ int Z3AstParser::parse_cond(dfsan_label label, bool result, bool add_nested, std // parse last branch cond input_dep_set_t inputs; - z3::expr cond = serialize(label, inputs); + z3::expr cond = serialize(label, inputs).simplify(); + + // fix cond if it's bv1 + if (cond.is_bv() && cond.get_sort().bv_size() == 1) { + cond = (cond != context_.bv_val(0, 1)).simplify(); + } + + if (Z3_get_bool_value(context_, cond) != Z3_L_UNDEF) { + // constant condition, no need to add constraint + // fprintf(stderr, "DEBUG parse_cond: label %u = %s is constant %d,\n", + // label, cond.to_string().c_str(), result); + return 0; + } // add negated last branch condition z3::expr r = context_.bool_val(result); @@ -1485,6 +1857,9 @@ int Z3AstParser::parse_cond(dfsan_label label, bool result, bool add_nested, std task->push_back((cond != r)); + // mark expression type for linking detection + mark_expr_type(label, inputs); + // collect additional input deps collect_more_deps(inputs); @@ -1501,7 +1876,12 @@ int Z3AstParser::parse_cond(dfsan_label label, bool result, bool add_nested, std return 0; // success } catch (z3::exception e) { - fprintf(stderr, "WARNING: parsing error: %s\n", e.msg()); + fprintf(stderr, "WARNING: parsing error: %s (label=%u)\n", e.msg(), label); + try { dump_value_cache(label); } catch (...) {} + } catch (std::exception& e) { + fprintf(stderr, "WARNING: std::exception in parse_cond: %s\n", e.what()); + } catch (...) { + fprintf(stderr, "WARNING: unknown exception in parse_cond\n"); } // exception happened, nothing added @@ -1566,13 +1946,36 @@ int Z3AstParser::parse_gep(dfsan_label ptr_label, uptr ptr, dfsan_label index_la } #endif + // mark expression type for linking detection + mark_expr_type(index_label, inputs); + // collect nested constraints collect_more_deps(inputs); + z3_task_t nested_tasks; add_nested_constraints(inputs, &nested_tasks); + // Normalize index expr to 64-bit bitvector. + // String-derived constraints may produce Int-sort indices. + z3::expr idx = i; + if (idx.is_bool()) { + // Defensive normalization: bool -> 1-bit BV. + idx = z3::ite(idx, context_.bv_val(1, 1), context_.bv_val(0, 1)); + } + if (idx.is_int()) { + idx = z3::int2bv(64, idx); + } else if (idx.is_bv()) { + unsigned idx_bits = idx.get_sort().bv_size(); + if (idx_bits < 64) { + idx = z3::zext(idx, 64 - idx_bits); + } else if (idx_bits > 64) { + idx = idx.extract(63, 0); + } + } else { + throw z3::exception("GEP index has unsupported sort"); + } + // first, check against fixed array bounds if available - z3::expr idx = z3::zext(i, 64 - size); if (num_elems > 0) { construct_index_tasks(idx, index, 0, num_elems, 1, nested_tasks, tasks); } else { @@ -1602,6 +2005,10 @@ int Z3AstParser::parse_gep(dfsan_label ptr_label, uptr ptr, dfsan_label index_la return 0; // success } catch (z3::exception e) { // logf("WARNING: solving error: %s\n", e.msg()); + } catch (std::exception& e) { + fprintf(stderr, "WARNING: std::exception in parse_gep: %s\n", e.what()); + } catch (...) { + fprintf(stderr, "WARNING: unknown exception in parse_gep\n"); } // exception happened, nothing added @@ -1617,16 +2024,31 @@ int Z3AstParser::add_constraints(dfsan_label label, uint64_t result) { try { input_dep_set_t inputs; z3::expr expr = serialize(label, inputs); + + // fprintf(stderr, "DEBUG add_constraints for label %u: expr=(%s == %lu)\n", + // label, expr.to_string().c_str(), result); + // for (auto off : inputs) { + // fprintf(stderr, "DEBUG: input dep: (%u, %u)\n", off.first, off.second); + // } + + // mark expression type for linking detection + mark_expr_type(label, inputs); + collect_more_deps(inputs); + // prepare result uint16_t size = get_label_info(label)->size; z3::expr r = context_.bv_val(result, size); // add constraint if (expr.is_bool()) r = context_.bool_val(result); + else if (expr.is_int()) r = context_.int_val(result); #if FILTER_WRONG_AST // double check if label is valid - if (value_cache_[label] != result) { + // Skip validation for indexOf operations (op1 repurposed for haystack pointer, + // RECORD_VALUE uses placeholder 0 which propagates wrong concrete values to + // downstream ICmp labels, causing spurious mismatches) + if (!label_contains_indexof(label) && value_cache_[label] != result) { // recalculated value must match the recorded value fprintf(stderr, "WARNING: value mismatch for label %u: expected %ld, got %ld\n", label, value_cache_[label], result); @@ -1635,27 +2057,82 @@ int Z3AstParser::add_constraints(dfsan_label label, uint64_t result) { #endif save_constraint(expr == r, inputs); + + // Track ICmp comparisons for Int mirroring of BV nested constraints. + // When both operands later get int-* variables (from sort coercion or fsubstr), + // add_nested_constraints will add the equivalent Int comparison to prevent + // the optimizer from minimizing int-* variables in ways that violate BV ordering. + dfsan_label_info *cmp_info = get_label_info(label); + if ((cmp_info->op & 0xff) == __dfsan::ICmp) { + cmp_info_t cmp{cmp_info->l1, cmp_info->l2, + (uint16_t)(cmp_info->op >> 8), (bool)result}; + for (auto &off : inputs) { + auto c = get_branch_dep(off); + if (c != nullptr) { + c->cmp_deps.push_back(cmp); + } + } + } } catch (z3::exception e) { return -1; + } catch (std::exception& e) { + fprintf(stderr, "WARNING: std::exception in add_constraints: %s\n", e.what()); + return -1; + } catch (...) { + fprintf(stderr, "WARNING: unknown exception in add_constraints\n"); + return -1; } return 0; } -void Z3AstParser::save_constraint(z3::expr expr, input_dep_set_t &inputs) { +int Z3AstParser::record_minimize(dfsan_label label, bool allow_zero) { + if (label < CONST_OFFSET || label == __dfsan::kInitializingLabel || label >= size_) { + return -1; + } + + try { + input_dep_set_t inputs; + z3::expr expr = serialize(label, inputs); + if (expr.is_bv() && !inputs.empty()) { + minimize_hints_.push_back({expr, allow_zero, inputs}); + } + } catch (z3::exception e) { + fprintf(stderr, "WARNING: z3 exception in record_minimize: %s\n", e.msg()); + return -1; + } catch (...) { + return -1; + } + + return 0; +} + +void Z3AstParser::mark_expr_type(dfsan_label label, input_dep_set_t &inputs) { + bool is_bv = is_label_bv_.at(label); + bool is_seq = is_label_seq_.at(label); + // fprintf(stderr, "DEBUG mark_expr_type: label %u is_bv=%d is_seq=%d\n", label, is_bv, is_seq); + for (auto off : inputs) { auto c = get_branch_dep(off); if (c == nullptr) { - auto nc = std::make_unique(); - c = nc.get(); - set_branch_dep(off, std::move(nc)); + throw z3::exception("branch_dep not found for input byte"); } + // Update type flags (accumulate, don't overwrite) + c->used_in_bv |= is_bv; + c->used_in_seq |= is_seq; + // fprintf(stderr, "DEBUG mark_expr_type: offset (%u, %u), used_in_bv=%d, used_in_seq=%d\n", + // off.first, off.second, c->used_in_bv, c->used_in_seq); + } +} + +void Z3AstParser::save_constraint(z3::expr expr, input_dep_set_t &inputs) { + for (auto off : inputs) { + auto c = get_branch_dep(off); if (c == nullptr) { - throw z3::exception("out of memory"); - } else { - c->input_deps.insert(inputs.begin(), inputs.end()); - c->expr_deps.insert(expr); + throw z3::exception("branch_dep not found for input byte"); } + c->input_deps.insert(inputs.begin(), inputs.end()); + c->expr_deps.insert(expr); } } @@ -1679,10 +2156,18 @@ void Z3AstParser::collect_more_deps(input_dep_set_t &inputs) { size_t Z3AstParser::add_nested_constraints(input_dep_set_t &inputs, z3_task_t *task) { expr_set_t added; + std::vector need_linking; + for (auto &off : inputs) { // fprintf(stderr, "adding offset %d\n", off.second); auto deps = get_branch_dep(off); if (deps != nullptr) { + // Check if this offset is used in both constraint types + if (deps->used_in_bv && deps->used_in_seq) { + // fprintf(stderr, "DEBUG: need linking for offset (%u, %u)\n", off.first, off.second); + need_linking.push_back(off); + } + for (auto &expr : deps->expr_deps) { if (added.insert(expr).second) { // fprintf(stderr, "adding expr: %s\n", expr.to_string().c_str()); @@ -1691,9 +2176,132 @@ size_t Z3AstParser::add_nested_constraints(input_dep_set_t &inputs, z3_task_t *t } } } + + // Add linking constraints for overlapping offsets + for (auto &off : need_linking) { + add_string_bitvec_link(off, task); + } + + // Add Int mirrors of BV comparisons when both operands have int-* variables. + // This prevents the optimizer from minimizing int-* variables in ways that + // violate BV ordering constraints (e.g., bvsle alloc n → int_alloc <= int_n). + if (!int_var_cache_.empty()) { + std::set> processed_cmps; + for (auto &off : inputs) { + auto deps = get_branch_dep(off); + if (deps == nullptr) continue; + for (auto &cmp : deps->cmp_deps) { + auto key = std::make_pair(cmp.l1, cmp.l2); + if (!processed_cmps.insert(key).second) continue; + auto it1 = int_var_cache_.find(cmp.l1); + auto it2 = int_var_cache_.find(cmp.l2); + if (it1 != int_var_cache_.end() && it2 != int_var_cache_.end()) { + // Both operands have int-* variables — add Int comparison + z3::expr int_cmp = get_cmd(it1->second, it2->second, cmp.predicate); + z3::expr int_constraint = cmp.result ? int_cmp : !int_cmp; + task->push_back(int_constraint); + // fprintf(stderr, "DEBUG: adding Int mirror: %s\n", + // int_constraint.to_string().c_str()); + } + } + } + } + return added.size(); } +void Z3AstParser::add_string_bitvec_link(offset_t off, z3_task_t *task) { + // Check if input_id is valid + if (off.first >= string_ranges_.size()) return; + + auto &ranges = string_ranges_[off.first]; + + // Use upper_bound with transparent comparator - search with just uint32_t + // Find first range where start > off.second, then go back one + auto it = ranges.upper_bound(off.second); + + if (it != ranges.begin()) { + --it; + if (off.second >= it->start && off.second < it->end) { + // Found covering range, use cached exprs directly + uint32_t pos_in_string = off.second - it->start; + + z3::expr str_var = it->str_expr; // cached str- expr + + // Get cached input- expr from branch_dependency + auto branch_dep = get_branch_dep(off); + if (branch_dep == nullptr) { + throw z3::exception("branch_dep not found for linking"); + } + z3::expr input_var = branch_dep->input_expr; + + // str_var[pos] as integer == input_var as integer + // Extract single-char substring, then convert to code point + z3::expr pos_expr = context_.int_val(pos_in_string); + z3::expr one = context_.int_val(1); + z3::expr single_char_str(context_, Z3_mk_seq_extract(context_, str_var, pos_expr, one)); + z3::expr char_code(context_, Z3_mk_string_to_code(context_, single_char_str)); + z3::expr byte_val = z3::bv2int(input_var, false); + + // Add the linking constraint + task->push_back(char_code == byte_val); + + // fprintf(stderr, "DEBUG: adding link constraint: %s\n", + // (char_code == byte_val).to_string().c_str()); + } + } +} + +void Z3AstParser::add_haystack_ptr_nonnull(dfsan_label ptr_label, + input_dep_set_t &deps) { + if (ptr_label < CONST_OFFSET || ptr_label >= size_) return; + uint16_t op = get_label_info(ptr_label)->op; + // Alloca/Free labels track concrete stack/heap/global bounds, not a symbolic + // pointer value — skip them (this also keeps file-mode pointers untouched). + if (op == __dfsan::Alloca || op == __dfsan::Free) return; + try { + z3::expr cptr = serialize(ptr_label, deps); + if (cptr.is_bv()) { + aux_constraints_.push_back( + cptr != context_.bv_val(0, cptr.get_sort().bv_size())); + } + } catch (z3::exception &) { + // best-effort: if the pointer can't be serialized, skip the constraint + } +} + +void Z3AstParser::register_string_range(uint32_t input, uint32_t start, + uint32_t end, z3::expr str_var) { + if (input >= string_ranges_.size()) return; + auto &ranges = string_ranges_[input]; + + auto result = ranges.emplace(start, end, str_var); + if (!result.second) return; // already exists (same start and end) + + // Check all existing ranges for containment relationships and add + // linking constraints so that overlapping string variables stay consistent. + for (auto &existing : ranges) { + if (existing.start == start && existing.end == end) continue; // skip self + + // New range is a subset of existing range + if (start >= existing.start && end <= existing.end) { + uint32_t offset = start - existing.start; + uint32_t len = end - start; + aux_constraints_.push_back(str_var == z3::expr(context_, + Z3_mk_seq_extract(context_, existing.str_expr, + context_.int_val(offset), context_.int_val(len)))); + } + // Existing range is a subset of new range + else if (existing.start >= start && existing.end <= end) { + uint32_t offset = existing.start - start; + uint32_t len = existing.end - existing.start; + aux_constraints_.push_back(existing.str_expr == z3::expr(context_, + Z3_mk_seq_extract(context_, str_var, + context_.int_val(offset), context_.int_val(len)))); + } + } +} + Z3ParserSolver::solving_status Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solutions) { solving_status ret = unknown_error; @@ -1707,6 +2315,11 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut // Use default solver to auto-detect theory (needed for string constraints) z3::solver solver(context_); solver.set("timeout", timeout); + // add auxiliary constraints (e.g., Int variable bounds from sort coercion) + for (const auto &ac : aux_constraints_) { + // fprintf(stderr, "DEBUG solve_task[%lu]: adding aux constraint: %s\n", task_id, ac.to_string().c_str()); + solver.add(ac); + } // solve the first constraint (optimistic) z3::expr e = task->at(0); solver.add(e); @@ -1715,6 +2328,7 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut // fprintf(stderr, "DEBUG solve_task[%lu]: result = %d (sat=1, unsat=0, unknown=2)\n", task_id, (int)res); if (res == z3::sat) { ret = opt_sat; + bool str_abstract = false; // true if model from string_solver=none // optimistic sat, save a model z3::model m = solver.get_model(); // fprintf(stderr, "DEBUG solve_task[%lu]: optimistic SAT model:\n%s\n", task_id, m.to_string().c_str()); @@ -1738,51 +2352,121 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut task_id, solver.to_smt2().c_str()); ret = opt_sat_nested_unsat; } else { - ret = opt_sat_nested_timeout; + // Nested check timed out with seq solver. Retry with + // string_solver=none which treats string ops (e.g., last_indexof) + // abstractly, avoiding expensive seq axiom instantiation while + // still solving BV/Int arithmetic correctly. + z3::set_param("smt.string_solver", "none"); + z3::solver fallback(context_); + fallback.set("timeout", timeout); + for (const auto &ac : aux_constraints_) { + fallback.add(ac); + } + for (const auto &expr : *task) { + fallback.add(expr); + } + z3::check_result fb_res = fallback.check(); + z3::set_param("smt.string_solver", "seq"); + // fprintf(stderr, "DEBUG solve_task[%lu]: string_solver=none fallback result = %d\n", task_id, (int)fb_res); + if (fb_res == z3::sat) { + ret = nested_sat; + m = fallback.get_model(); + // fprintf(stderr, "DEBUG solve_task[%lu]: fallback model:\n%s\n", task_id, m.to_string().c_str()); + str_abstract = true; + } else { + ret = opt_sat_nested_timeout; + } } + // pop nested constraints so solver is clean for optimization below + solver.pop(); } else { ret = nested_sat; // XXX: upgrade to nested_sat? } - // Check if model contains strlen symbols and optimize if needed + // Check if model contains strlen/str symbols and optimize if needed + // Skip string optimization when model came from string_solver=none + // (string variables are abstract and can't be meaningfully minimized) std::vector> strlen_vars; // (var, max_len) - const uint64_t MAX_STRLEN_EXTEND = 4096; // Reasonable max extension + std::vector str_len_minimize; // str.len(str_var) to minimize + // MAX_STRLEN_EXTEND is defined at file scope + + // Collect input offsets from the model for minimize hint matching + std::unordered_set model_inputs; for (unsigned i = 0; i < m.num_consts(); ++i) { z3::func_decl decl = m.get_const_decl(i); - if (decl.name().kind() == Z3_STRING_SYMBOL && - decl.name().str().find("strlen") == 0) { - uint32_t input, offset, null_from_input; - uint64_t orig_len; - if (sscanf(decl.name().str().c_str(), strlen_name_format, - &input, &offset, &orig_len, &null_from_input) == 4) { - z3::expr strlen_var = context_.constant(decl.name(), decl.range()); - uint64_t max_len = orig_len + MAX_STRLEN_EXTEND; - strlen_vars.emplace_back(strlen_var, max_len); + if (decl.name().kind() == Z3_STRING_SYMBOL) { + if (decl.name().str().find("strlen") == 0) { + uint32_t input, offset, null_from_input; + uint64_t orig_len; + if (sscanf(decl.name().str().c_str(), strlen_name_format, + &input, &offset, &orig_len, &null_from_input) == 4) { + z3::expr strlen_var = context_.constant(decl.name(), decl.range()); + uint64_t max_len = orig_len + MAX_STRLEN_EXTEND; + strlen_vars.emplace_back(strlen_var, max_len); + } + } else if (!str_abstract && decl.name().str().find("str-") == 0) { + // Minimize string variable lengths to avoid unnecessarily large strings + z3::expr str_var = context_.constant(decl.name(), decl.range()); + z3::expr len_expr(context_, Z3_mk_seq_length(context_, str_var)); + str_len_minimize.push_back(len_expr); + } else if (decl.name().str().find("int-") == 0) { + // Int variables from sort coercion (e.g., int-0-0-64) — minimize + // to keep close to original value and avoid changing allocation sizes + z3::expr int_var = context_.constant(decl.name(), decl.range()); + str_len_minimize.push_back(int_var); + } else if (decl.name().str().find("input") == 0) { + uint32_t input, offset; + if (sscanf(decl.name().str().c_str(), input_name_format, &input, &offset) == 2) { + model_inputs.emplace(input, offset); + } } } } - if (!strlen_vars.empty()) { - // fprintf(stderr, "DEBUG solve_task[%lu]: found %zu strlen variables, optimizing...\n", task_id, strlen_vars.size()); - // Step 1: Try optimizer to minimize strlen values (no hard bounds) + // Find matching minimize hints based on input dep overlap + std::vector> alloc_minimize; + for (const auto &hint : minimize_hints_) { + for (const auto &dep : hint.deps) { + if (model_inputs.count(dep)) { + alloc_minimize.push_back({hint.expr, hint.allow_zero}); + break; + } + } + } + + if (!strlen_vars.empty() || !str_len_minimize.empty() || !alloc_minimize.empty()) { + // Try optimizer to minimize values (no hard bounds) z3::optimize opt(context_); z3::params p(context_); p.set("timeout", timeout); opt.set(p); + for (const auto &ac : aux_constraints_) { + opt.add(ac); + } for (const auto &expr : *task) { opt.add(expr); } for (const auto &sv : strlen_vars) { - // fprintf(stderr, "DEBUG solve_task[%lu]: minimizing %s (max=%lu)\n", task_id, sv.first.to_string().c_str(), sv.second); opt.minimize(sv.first); } + for (const auto &sl : str_len_minimize) { + opt.minimize(sl); + } + for (const auto &am : alloc_minimize) { + opt.minimize(am.first); + if (!am.second) { + opt.add(am.first != 0); + } + } bool use_optimized = false; - if (opt.check() == z3::sat) { + auto opt_result = opt.check(); + // fprintf(stderr, "DEBUG solve_task[%lu]: optimizer result = %d\n", task_id, (int)opt_result); + if (opt_result == z3::sat) { z3::model opt_model = opt.get_model(); - // fprintf(stderr, "DEBUG solve_task[%lu]: optimized SAT model:\n%s\n", task_id, opt_model.to_string().c_str()); + // fprintf(stderr, "DEBUG solve_task[%lu]: optimized model:\n%s\n", task_id, opt_model.to_string().c_str()); // Check if all strlen values are within bounds bool all_within_bounds = true; for (const auto &sv : strlen_vars) { @@ -1796,26 +2480,21 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut if (all_within_bounds) { m = opt_model; use_optimized = true; - // fprintf(stderr, "DEBUG solve_task[%lu]: using optimized model (all within bounds)\n", task_id); } // else: optimized model exceeds bounds, fall back to bounded solver } // Step 2: If optimization failed or exceeded bounds, try solver with bound constraints - if (!use_optimized) { - // fprintf(stderr, "DEBUG solve_task[%lu]: adding bound constraints and re-checking\n", task_id); + if (!use_optimized && !strlen_vars.empty()) { + // Only try bounded solver if there are strlen vars to bound solver.push(); for (const auto &sv : strlen_vars) { solver.add(z3::ule(sv.first, context_.bv_val(sv.second, sv.first.get_sort().bv_size()))); } if (solver.check() == z3::sat) { m = solver.get_model(); - // fprintf(stderr, "DEBUG solve_task[%lu]: bounded SAT model:\n%s\n", task_id, m.to_string().c_str()); - } else { - // Step 3: Unsolvable within bounds, skip - solver.pop(); - return ret; } + // else: bounded check failed, fall through with original model m solver.pop(); } } @@ -1830,7 +2509,13 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut ret = opt_timeout; } } catch (z3::exception ze) { - fprintf(stderr, "WARNING: solve_task[%lu]: EXCEPTION: %s\n", task_id, ze.msg()); + fprintf(stderr, "WARNING: solve_task[%lu]: z3 exception: %s\n", task_id, ze.msg()); + ret = unknown_error; + } catch (std::bad_alloc &) { + fprintf(stderr, "WARNING: solve_task[%lu]: out of memory\n", task_id); + ret = unknown_error; + } catch (std::exception &e) { + fprintf(stderr, "WARNING: solve_task[%lu]: exception: %s\n", task_id, e.what()); ret = unknown_error; } @@ -1842,6 +2527,36 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { // from qsym unsigned num_constants = m.num_consts(); // fprintf(stderr, "DEBUG generate_solution: model has %u constants\n", num_constants); + + // Pre-scan str- variables: collect per-input info to detect sibling + // variables (adjacent/overlapping ranges from loop iterations). + // When a sibling has content (non-empty), empty ones must not be deleted + // as that would shift offsets and corrupt the buffer layout. + struct str_info { uint32_t offset; uint32_t orig_len; uint32_t new_len; }; + std::map> str_vars_by_input; + for (unsigned i = 0; i < num_constants; i++) { + z3::func_decl decl = m.get_const_decl(i); + z3::symbol name = decl.name(); + if (name.kind() == Z3_STRING_SYMBOL && name.str().find("str-") == 0) { + uint32_t input, offset, orig_len; + if (sscanf(name.str().c_str(), str_name_format, &input, &offset, &orig_len) == 3) { + z3::expr e = m.get_const_interp(decl); + uint32_t new_len = 0; + if (e.is_string_value()) { + new_len = decode_z3_string(e.get_string()).size(); + } + str_vars_by_input[input].push_back({offset, orig_len, new_len}); + } + } + } + // Sort each input's str- variables by offset so adjacency is easy to check + for (auto &entry : str_vars_by_input) { + std::sort(entry.second.begin(), entry.second.end(), + [](const str_info &a, const str_info &b) { + return a.offset < b.offset; + }); + } + for (unsigned i = 0; i < num_constants; i++) { z3::func_decl decl = m.get_const_decl(i); z3::expr e = m.get_const_interp(decl); @@ -1855,9 +2570,9 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { uint32_t offset; sscanf(name.str().c_str(), input_name_format, &input, &offset); uint8_t value = (uint8_t)e.get_numeral_int(); - // fprintf(stderr, "DEBUG input-%u-%u: SET offset %u = 0x%02x (individual byte)\n", - // input, offset, offset, value); - solutions.emplace_back(input, offset, value); + // fprintf(stderr, "DEBUG input-%u-%u: SET offset %d = 0x%02x (individual byte)\n", + // input, offset, (int32_t)offset, value); + solutions.emplace_back(input, (int32_t)offset, value); } else if (!name.str().compare("fsize")) { // FIXME: // off_t size = (off_t)e.get_numeral_int64(); @@ -1933,6 +2648,10 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { if (target_len > orig_len) { // Extending: insert bytes to make the string longer uint64_t extend_by = target_len - orig_len; + if (extend_by > MAX_STRLEN_EXTEND) { + extend_by = MAX_STRLEN_EXTEND; + target_len = orig_len + extend_by; + } std::vector fill_bytes(extend_by, 'A'); solutions.emplace_back(input, offset + (uint32_t)orig_len, std::move(fill_bytes)); // For plain strings (null_from_input=1), add null terminator at new end @@ -1955,7 +2674,7 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { uint32_t input; uint32_t offset; uint32_t orig_len; - if (sscanf(name.str().c_str(), "str-%u-%u-%u", &input, &offset, &orig_len) != 3) { + if (sscanf(name.str().c_str(), str_name_format, &input, &offset, &orig_len) != 3) { continue; // Skip malformed string variable } @@ -1967,6 +2686,60 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { fprintf(stderr, "DEBUG generate_solution: str-%u-%u-%u: orig=%u, new=%u, raw='%s'\n", input, offset, orig_len, orig_len, new_len, raw_str.c_str()); + // Skip empty str- variables that are connected (via adjacent or + // overlapping ranges) to a non-empty sibling in the same input. + // The sorted vector lets us walk outward from our position to find + // the contiguous group and check if any member has content. + // Deleting would shift offsets and corrupt siblings' content. + if (new_len == 0 && orig_len > 0) { + auto it = str_vars_by_input.find(input); + if (it != str_vars_by_input.end()) { + const auto &siblings = it->second; + // Find our position in the sorted vector + size_t self_idx = 0; + for (size_t si = 0; si < siblings.size(); si++) { + if (siblings[si].offset == offset && + siblings[si].orig_len == orig_len) { + self_idx = si; + break; + } + } + // Walk left: check adjacent/overlapping ranges + bool has_nonempty_sibling = false; + uint32_t group_start = offset; + for (size_t si = self_idx; si > 0; si--) { + const auto &prev = siblings[si - 1]; + uint32_t prev_end = prev.offset + prev.orig_len; + if (prev_end >= group_start) { // adjacent or overlapping + group_start = prev.offset; + if (prev.new_len > 0) { has_nonempty_sibling = true; break; } + } else { + break; + } + } + // Walk right: check adjacent/overlapping ranges + if (!has_nonempty_sibling) { + uint32_t group_end = offset + orig_len; + for (size_t si = self_idx + 1; si < siblings.size(); si++) { + const auto &next = siblings[si]; + if (next.offset <= group_end) { // adjacent or overlapping + uint32_t next_end = next.offset + next.orig_len; + if (next_end > group_end) group_end = next_end; + if (next.new_len > 0) { has_nonempty_sibling = true; break; } + } else { + break; + } + } + } + if (has_nonempty_sibling) { + fprintf(stderr, "DEBUG generate_solution: str-%u-%u-%u: " + "skipping empty (has non-empty adjacent sibling)\n", + input, offset, orig_len); + continue; + } + } + } + if (new_len > orig_len) { // Extending: set common prefix, then insert extra bytes for (uint32_t j = 0; j < orig_len; j++) { @@ -1991,6 +2764,22 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { } } } + } else if (name.str().find("int-") == 0) { + // Int variable from BV-to-Int sort coercion (int-input-offset-bits) + // Maps back to input byte(s) + uint32_t input, offset, bits; + if (sscanf(name.str().c_str(), int_name_format, + &input, &offset, &bits) != 3) { + continue; + } + uint64_t value = e.get_numeral_uint64(); + uint32_t bytes = bits / 8; + if (bytes == 0) bytes = 1; + // Emit byte-level solutions (little-endian) + for (uint32_t i = 0; i < bytes; i++) { + solutions.emplace_back(input, offset + i, (uint8_t)(value & 0xff)); + value >>= 8; + } } else if (name.str().find("strrchr_idx_") == 0 || name.str().find("strchr_idx_") == 0) { // Index variables from strchr/strrchr - skip, they're intermediate @@ -2018,13 +2807,13 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { // Replace null bytes within string ranges (tracked in string_ranges_) for (auto &sol : solutions) { if (sol.op == solution_op_t::SET && sol.val == 0x00) { - auto it = string_ranges_.find(sol.id); - if (it != string_ranges_.end()) { - for (const auto &range : it->second) { + if (sol.id < string_ranges_.size()) { + auto &ranges = string_ranges_[sol.id]; + for (const auto &range : ranges) { // If this offset is within a string range (but not at the end), replace null - if (sol.offset >= range.first && sol.offset < range.second) { + if (sol.offset >= range.start && sol.offset < range.end) { // fprintf(stderr, "DEBUG generate_solution: replacing null at offset %u (in range [%u,%u))\n", - // sol.offset, range.first, range.second); + // sol.offset, range.start, range.end); sol.val = 'A'; // Replace null with 'A' break; } @@ -2036,6 +2825,43 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { // fprintf(stderr, "DEBUG generate_solution: finished with %zu solutions\n", solutions.size()); } +int Z3ParserSolver::export_task_smt2(uint64_t task_id, int fd) { + // Use tasks_.find() to peek without removing + auto it = tasks_.find(task_id); + if (it == tasks_.end()) { + return -1; + } + auto task = it->second; + + try { + // Create solver and add all constraints + z3::solver solver(context_); + for (const auto &ac : aux_constraints_) { + solver.add(ac); + } + for (const auto &expr : *task) { + solver.add(expr); + } + + // Export as SMT2 + std::string smt2 = solver.to_smt2(); + ssize_t written = write(fd, smt2.c_str(), smt2.size()); + if (written < 0 || (size_t)written != smt2.size()) { + return -1; + } + return 0; + } catch (z3::exception &e) { + fprintf(stderr, "WARNING: export_task_smt2[%lu]: %s\n", task_id, e.msg()); + return -1; + } catch (std::bad_alloc &) { + fprintf(stderr, "WARNING: export_task_smt2[%lu]: out of memory\n", task_id); + return -1; + } catch (std::exception &e) { + fprintf(stderr, "WARNING: export_task_smt2[%lu]: %s\n", task_id, e.what()); + return -1; + } +} + // Build Z3 string from a content label (Load or Concat of bytes) // Creates a symbolic string variable with naming convention: str-input-offset-len z3::expr Z3AstParser::build_string_from_label(dfsan_label label, input_dep_set_t &deps) { @@ -2051,19 +2877,31 @@ z3::expr Z3AstParser::build_string_from_label(dfsan_label label, input_dep_set_t uint32_t input = get_label_info(info->l1)->op2.i; uint32_t len = info->l2; // number of bytes loaded - // Track string range for null-byte post-processing - string_ranges_[input].emplace_back(offset, offset + len); + // Create a single symbolic string variable: str-input-offset-len + char name[256]; + snprintf(name, sizeof(name), str_name_format, input, offset, len); + z3::symbol symbol = context_.str_symbol(name); + z3::expr str_var = context_.constant(symbol, context_.string_sort()); + + { // handle ucsan's lazy input allocation + if (input >= branch_deps_.size()) + branch_deps_.resize(input + 1); + if (is_negative_offset(offset) && input >= neg_branch_deps_.size()) { + neg_branch_deps_.resize(input + 1); + } + }; // Add dependencies for all bytes in the range for (uint32_t i = 0; i < len; i++) { + if (get_branch_dep({input, offset + i}) == nullptr) { + set_branch_dep({input, offset + i}, + std::make_unique(str_var)); + } deps.insert(std::make_pair(input, offset + i)); } - // Create a single symbolic string variable: str-input-offset-len - char name[256]; - snprintf(name, sizeof(name), "str-%u-%u-%u", input, offset, len); - z3::symbol symbol = context_.str_symbol(name); - z3::expr str_var = context_.constant(symbol, context_.string_sort()); + // Track string range and add linking constraints for overlapping ranges + register_string_range(input, offset, offset + len, str_var); // Cache string info for this label string_info_cache_[label] = {input, offset, len}; @@ -2221,23 +3059,36 @@ z3::expr Z3AstParser::build_string_from_label(dfsan_label label, input_dep_set_t uint32_t len = offsets.size(); uint32_t start_offset = offsets[0]; - // Track string range for null-byte post-processing - string_ranges_[input_id].emplace_back(start_offset, start_offset + len); + // Create single symbolic string variable + char name[256]; + snprintf(name, sizeof(name), str_name_format, input_id, start_offset, len); + z3::symbol symbol = context_.str_symbol(name); + z3::expr str_var = context_.constant(symbol, context_.string_sort()); + + { // handle ucsan's lazy input allocation + if (input_id >= branch_deps_.size()) + branch_deps_.resize(input_id + 1); + if (input_id >= neg_branch_deps_.size()) { + neg_branch_deps_.resize(input_id + 1); + } + }; // Add dependencies for all bytes for (uint32_t off : offsets) { + if (get_branch_dep({input_id, off}) == nullptr) { + set_branch_dep({input_id, off}, + std::make_unique(str_var)); + } deps.insert(std::make_pair(input_id, off)); } - // Create single symbolic string variable - char name[256]; - snprintf(name, sizeof(name), "str-%u-%u-%u", input_id, start_offset, len); - z3::symbol symbol = context_.str_symbol(name); + // Track string range and add linking constraints for overlapping ranges + register_string_range(input_id, start_offset, start_offset + len, str_var); // Cache string info for this label string_info_cache_[label] = {input_id, start_offset, len}; - return context_.constant(symbol, context_.string_sort()); + return str_var; } // Fall back to recursive concatenation if not consecutive @@ -2285,19 +3136,36 @@ z3::expr Z3AstParser::build_string_from_label(dfsan_label label, input_dep_set_t uint32_t offset = info->op1.i; uint32_t input = info->op2.i; - // Track string range for null-byte post-processing (single byte) - string_ranges_[input].emplace_back(offset, offset + 1); deps.insert(std::make_pair(input, offset)); // Create a single-char symbolic string char name[256]; - snprintf(name, sizeof(name), "str-%u-%u-%u", input, offset, 1); + snprintf(name, sizeof(name), str_name_format, input, offset, 1); z3::symbol symbol = context_.str_symbol(name); + z3::expr str_var = context_.constant(symbol, context_.string_sort()); + + { // handle ucsan's lazy input allocation + if (input >= branch_deps_.size()) + branch_deps_.resize(input + 1); + if (is_negative_offset(offset) && input >= neg_branch_deps_.size()) { + neg_branch_deps_.resize(input + 1); + } + }; + + // Add dependency + if (get_branch_dep({input, offset}) == nullptr) { + set_branch_dep({input, offset}, + std::make_unique(str_var)); + } + deps.insert(std::make_pair(input, offset)); + + // Track string range and add linking constraints for overlapping ranges + register_string_range(input, offset, offset + 1, str_var); // Cache string info for this label string_info_cache_[label] = {input, offset, 1}; - return context_.constant(symbol, context_.string_sort()); + return str_var; } // Last resort: empty string diff --git a/solvers/z3.cpp b/solvers/z3.cpp index 3c354433..e86ef6fa 100644 --- a/solvers/z3.cpp +++ b/solvers/z3.cpp @@ -26,7 +26,7 @@ static z3::solver __z3_solver(__z3_context, "QF_BV"); static symsan::Z3ParserSolver *__z3_parser = nullptr; // filter? -SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint32_t __taint_trace_callstack; +extern SANITIZER_INTERFACE_ATTRIBUTE THREADLOCAL uint32_t __taint_trace_callstack; static std::unordered_set __solved_labels; typedef std::pair trace_context; @@ -407,7 +407,7 @@ __taint_trace_memcmp(dfsan_label label) { __z3_parser->record_memcmp(label, content_ptr, info->size); } -extern "C" void InitializeSolver() { +extern "C" void InitializeSymSanSolver() { __output_dir = flags().output_dir; __instance_id = flags().instance_id; __session_id = flags().session_id; diff --git a/tests/cpp_eh.cpp b/tests/cpp_eh.cpp new file mode 100644 index 00000000..86df5ab2 --- /dev/null +++ b/tests/cpp_eh.cpp @@ -0,0 +1,69 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("AABB"*10)' > %t.bin +// RUN: clang++ -o %t.uninstrumented %s +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang++ -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s +// NOTE: fastgen only (like the other cpp_* tests). In-process Z3 is not used +// for C++ because the Z3 C++ API pulls in the instrumented libc++ and can loop. + +// Symbolic propagation across C++ exception handling. +// +// The symbolic input value is thrown and re-acquired in a catch clause, so the +// symbolic expression must survive the whole EH path +// (__cxa_allocate_exception -> __cxa_throw -> unwind -> __cxa_begin_catch) and +// land in the catch parameter. The branch below is only solvable if `caught` +// still carries the symbolic expression of the input; if EH drops the label the +// solver cannot flip the branch and the generated input stays "Bad". +// +// The exception runtime (__cxa_* / personality / unwinder) must run concretely: +// EH is a coupled subsystem and instrumenting any of it breaks the unwind, so a +// throw never reaches its catch. This is handled by the build, not the test: +// the EH entry points are marked uninstrumented in done_abilist.txt, and +// ko_clang links the plain libc++abi-native/libunwind-native (see +// rebuild_native.sh). The symbolic value still propagates through the concrete +// EH runtime via address-keyed shadow memory, so the branch is solvable. + +#include +#include +#include +#include +#include "lib.h" + +int __attribute__((noinline)) thrower(int32_t y) { + throw y; + return 0; +} + +int main(int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[20]; + FILE* fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + int32_t x = 0; + memcpy(&x, buf + 1, 4); // x is bytes 1..4 of the input + + int32_t caught = -1; + try { + thrower(x); + } catch (int e) { + caught = e; // must carry x's symbolic expression + } + + // caught == x, and caught*caught - 6*caught == -8 has integer roots 2 and 4. + if (caught * caught - 6 * caught == -8) { + // CHECK-GEN: Good + printf("Good\n"); + } else { + // CHECK-ORIG: Bad + printf("Bad\n"); + } +} diff --git a/tests/cpp_string.cpp b/tests/cpp_string.cpp index a078125b..4d860ddb 100644 --- a/tests/cpp_string.cpp +++ b/tests/cpp_string.cpp @@ -5,7 +5,7 @@ // RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s // RUN: env KO_USE_FASTGEN=1 %ko-clang++ -o %t.fg %s // RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin -// RUN: %t.uninstrumented %t.out/id-0-0-2 | FileCheck --check-prefix=CHECK-GEN %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN %s // doesn't work with in-process z3 solver diff --git a/tests/hoist_bounds.c b/tests/hoist_bounds.c new file mode 100644 index 00000000..a401f8fb --- /dev/null +++ b/tests/hoist_bounds.c @@ -0,0 +1,48 @@ +// Test that bounds-check hoisting out of loops still detects OOB. +// A fixed-count loop (trip count known to SCEV) writes symbolic data +// into a heap buffer. The solver should find an allocation size that +// makes the loop go out of bounds. The hoisted summary check in the +// preheader must catch this. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c"import sys; sys.stdout.buffer.write(b'\x1a\x00\x00\x00')" > %t.bin +// RUN: env KO_USE_FASTGEN=1 KO_SOLVE_UB=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out solve_ub=1" %fgtest %t.fg %t.bin +// RUN: not env TAINT_OPTIONS="debug=1 trace_bounds=1 exit_on_memerror=1" %t.fg %t.out/id-0-0-0 2>&1 | FileCheck %s +// CHECK: ERROR: OOB overflow + +#include +#include +#include +#include +#include "lib.h" + +int main (int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return 0; + } + + int size = 0; + + FILE* fp = chk_fopen(argv[1], "rb"); + chk_fread(&size, sizeof(size), 1, fp); + fclose(fp); + + // Allocate with symbolic size + char *buf = malloc(size); + if (!buf) return 0; + + // Fixed-count loop — SCEV can compute trip count = 26 + // When size < 26, the hoisted bounds check should detect OOB +#pragma clang loop vectorize(disable) unroll(disable) + for (int i = 0; i < 26; i++) { + buf[i] = 'A' + i; + } + + // Use last element so the loop isn't dead-code-eliminated + printf("%c\n", buf[25]); + free(buf); + return 0; +} diff --git a/tests/hoist_bounds2.c b/tests/hoist_bounds2.c new file mode 100644 index 00000000..5dd1f31d --- /dev/null +++ b/tests/hoist_bounds2.c @@ -0,0 +1,47 @@ +// Test bounds checking with fixed-size buffer and symbolic loop count. +// The loop trip count is symbolic, so SCEV cannot hoist the bounds +// check — per-iteration checks must still detect the OOB. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c"import sys; sys.stdout.buffer.write(b'\x01\x00\x00\x00')" > %t.bin +// RUN: env KO_USE_FASTGEN=1 KO_SOLVE_UB=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out solve_ub=1" %fgtest %t.fg %t.bin +// RUN: not env TAINT_OPTIONS="debug=1 trace_bounds=1 exit_on_memerror=1" %t.fg %t.out/id-0-0-1 2>&1 | FileCheck %s +// CHECK: ERROR: OOB overflow + +#include +#include +#include +#include +#include "lib.h" + +int main (int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return 0; + } + + int count = 0; + + FILE* fp = chk_fopen(argv[1], "rb"); + chk_fread(&count, sizeof(count), 1, fp); + fclose(fp); + + // Fixed-size heap buffer, small enough that solver-generated + // count=2 triggers OOB + char *buf = malloc(1); + if (!buf) return 0; + + // Variable loop count — SCEV computes BTC as a runtime expression, + // so the hoisted summary check covers [buf, buf + count). + // When count > 1, the loop goes OOB. +#pragma clang loop vectorize(disable) unroll(disable) + for (int i = 0; i < count; i++) { + buf[i] = 'A' + (i % 26); + } + + printf("%c\n", buf[0]); + free(buf); + return 0; +} diff --git a/tests/hoist_bounds3.c b/tests/hoist_bounds3.c new file mode 100644 index 00000000..68840162 --- /dev/null +++ b/tests/hoist_bounds3.c @@ -0,0 +1,47 @@ +// Test bounds checking when both allocation size and loop count are symbolic. +// The solver should find a (size, count) pair where count > size, triggering OOB. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c"import sys; sys.stdout.buffer.write(b'\x1a\x00\x01\x00')" > %t.bin +// RUN: env KO_USE_FASTGEN=1 KO_SOLVE_UB=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out solve_ub=1" %fgtest %t.fg %t.bin +// RUN: not env TAINT_OPTIONS="debug=1 trace_bounds=1 exit_on_memerror=1" %t.fg %t.out/id-0-0-1 2>&1 | FileCheck %s +// CHECK: ERROR: OOB overflow + +#include +#include +#include +#include +#include "lib.h" + +int main (int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return 0; + } + + uint16_t size = 0; + uint16_t count = 0; + + FILE* fp = chk_fopen(argv[1], "rb"); + chk_fread(&size, sizeof(size), 1, fp); + chk_fread(&count, sizeof(count), 1, fp); + fclose(fp); + + // Both size and count are symbolic; +1 so malloc(0) can't happen + char *buf = malloc(size + 1); + if (!buf) return 0; + + // Variable loop count over variable-size buffer. + // Initial input: size=26, count=1 (safe). + // Solver should find size/count pair where count > size+1 → OOB. +#pragma clang loop vectorize(disable) unroll(disable) + for (int i = 0; i < count; i++) { + buf[i] = 'A' + (i % 26); + } + + printf("%c\n", buf[0]); + free(buf); + return 0; +} diff --git a/tests/hoist_strlen_bounds.c b/tests/hoist_strlen_bounds.c new file mode 100644 index 00000000..d3b82387 --- /dev/null +++ b/tests/hoist_strlen_bounds.c @@ -0,0 +1,54 @@ +// Test strlen-bounded loop hoisting: when SCEV can't compute the trip +// count because the loop exit depends on a null terminator in memory, +// the pass detects the while(ptr[i]) pattern and emits a hoisted +// __taint_solve_str_bounds check. The solver should find a string length +// that overflows the destination buffer. +// +// This mimics the shellescape pattern: a while(from[i]) loop copies +// characters from a source string into a fixed-size destination buffer. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c"import sys; sys.stdout.buffer.write(b'AB\x00' + b'\x00' * 5)" > %t.bin +// RUN: env KO_USE_FASTGEN=1 KO_SOLVE_UB=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out solve_ub=1" %fgtest %t.fg %t.bin +// RUN: not env TAINT_OPTIONS="debug=1 trace_bounds=1 exit_on_memerror=1" %t.fg %t.out/id-0-0-1 2>&1 | FileCheck %s +// CHECK: ERROR: OOB overflow + +#include +#include +#include +#include +#include "lib.h" + +// Prevent inlining so the loop structure is preserved +__attribute__((noinline)) +void copy_str(const char *src, char *dst) { + int i = 0; +#pragma clang loop vectorize(disable) unroll(disable) + while (src[i] != '\0') { + dst[i] = src[i]; + i++; + } + dst[i] = '\0'; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return 0; + } + + char input[8]; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(input, 1, sizeof(input), fp); + fclose(fp); + input[7] = '\0'; + + // Small destination buffer — if strlen(input) > 3, OOB + char dst[4]; + copy_str(input, dst); + + printf("%s\n", dst); + return 0; +} diff --git a/tests/memchr_plant_offset.c b/tests/memchr_plant_offset.c new file mode 100644 index 00000000..618d3998 --- /dev/null +++ b/tests/memchr_plant_offset.c @@ -0,0 +1,49 @@ +// String-theory DIAGNOSTIC: plant a byte at a pinned offset in an all-zero +// buffer using memchr (length-bounded, NOT strlen-bounded). +// +// memchr(buf, c, n) searches a fixed n bytes regardless of NULs, so the +// symbolic haystack covers the full range even on a zero buffer. This test +// should PASS where strchr_plant_gap.c (same zero buffer) fails, pinpointing +// the strchr failure to strlen-truncation of the haystack rather than to the +// solver's string theory in general. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python3 -c "import sys; sys.stdout.buffer.write(b'\x00'*16)" > %t.bin +// RUN: clang -o %t.uninstrumented %s +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[64] = {0}; + FILE *fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t n = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + + // Solver must plant '@' at exactly offset 5 within the first 16 bytes. + char *p = (char *)memchr(buf, '@', 16); + if (p && (p - buf) == 5) { + // CHECK-GEN: PLANTED at 5 + printf("PLANTED at 5\n"); + } else { + // CHECK-ORIG: no byte at 5 + printf("no byte at 5\n"); + } + return 0; +} diff --git a/tests/strchr_plant_filled.c b/tests/strchr_plant_filled.c new file mode 100644 index 00000000..7e147e2b --- /dev/null +++ b/tests/strchr_plant_filled.c @@ -0,0 +1,48 @@ +// String-theory: plant a delimiter at a PINNED offset in a fully-filled buffer. +// +// Baseline for strchr_plant_gap.c. Here the input fills the buffer with +// non-zero bytes, so strlen() sees the whole object and the solver can place +// '"' at offset 4 with an in-place SET. This case already works; it exists so +// the gap test isolates the strlen-truncation as the single difference. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python3 -c "import sys; sys.stdout.buffer.write(b'A'*16)" > %t.bin +// RUN: clang -o %t.uninstrumented %s +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[64] = {0}; + FILE *fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t n = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + buf[n] = '\0'; + + // Solver must plant '"' at exactly offset 4 (first occurrence). + char *p = strchr(buf, '"'); + if (p && (p - buf) == 4) { + // CHECK-GEN: PLANTED at 4 + printf("PLANTED at 4\n"); + } else { + // CHECK-ORIG: no delimiter at 4 + printf("no delimiter at 4\n"); + } + return 0; +} diff --git a/tests/strchr_plant_gap.c b/tests/strchr_plant_gap.c new file mode 100644 index 00000000..00b8dfc1 --- /dev/null +++ b/tests/strchr_plant_gap.c @@ -0,0 +1,54 @@ +// String-theory: plant a delimiter at a pinned offset when the buffer starts +// with an early NUL (extension required). +// +// Identical to strchr_plant_filled.c EXCEPT the input is all-zero, so the +// concrete strlen() is 0. __dfsw_strchr bounds the symbolic haystack by strlen +// (dfsan_custom.cpp get_str_label), yet the solver still plants '"' at offset 5 +// here -- it exposes enough of the object extent (Alloca bounds from the stack +// buffer) and extends the string. This locks in that capability: a regression +// would show up as the gen input failing to reach offset 5. +// +// Contrast with memchr_plant_offset.c: memchr is length-bounded (not +// strlen-bounded) and also succeeds on the same zero buffer. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python3 -c "import sys; sys.stdout.buffer.write(b'\x00'*16)" > %t.bin +// RUN: clang -o %t.uninstrumented %s +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[64] = {0}; + FILE *fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t n = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + buf[n] = '\0'; + + // Solver must plant '"' at exactly offset 5, extending past the early NUL. + char *p = strchr(buf, '"'); + if (p && (p - buf) == 5) { + // CHECK-GEN: PLANTED at 5 + printf("PLANTED at 5\n"); + } else { + // CHECK-ORIG: no delimiter at 5 + printf("no delimiter at 5\n"); + } + return 0; +} diff --git a/tests/strchr_sibling_byte.c b/tests/strchr_sibling_byte.c new file mode 100644 index 00000000..a33566a5 --- /dev/null +++ b/tests/strchr_sibling_byte.c @@ -0,0 +1,63 @@ +// String-theory: a strchr index constraint conjoined with a direct byte +// constraint on the SAME buffer must be solved consistently. +// +// We require BOTH: +// strchr(buf,'@') - buf == 3 (i.e. buf[3]=='@', buf[0..2] != '@') +// buf[2] == 'Z' +// A trivial solution exists: "AAZ@" (or "B0Z@", etc). +// +// The haystack string variable (input offsets 0..3) and the per-byte scalar +// variable (offset 2) OVERLAP; the solver links them (string<->bitvec linking +// constraints) so the indexof model and the scalar byte model agree, instead of +// returning a model where they disagree. +// +// The seed plants '@' off-target (at offset 2) so that strchr finds it and the +// conjunction branch (p && (p-buf)==3 && buf[2]=='Z') is actually reached. With +// a seed lacking '@' that branch is never executed (strchr short-circuits). +// fgtest flips the leading `p != NULL` null-check first (id-0-0-0, the +// not-found counterfactual -> "miss"); the conjunction flip is id-0-0-1, which +// must satisfy both constraints and print HIT. +// +// Direct strchr planting alone works (see strchr_plant_filled/gap.c); this test +// covers the conjunction with an overlapping scalar byte constraint. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python3 -c "import sys; sys.stdout.buffer.write(b'AA@A')" > %t.bin +// RUN: clang -o %t.uninstrumented %s +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[64] = {0}; + FILE *fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t n = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + buf[n] = '\0'; + + char *p = strchr(buf, '@'); + if (p && (p - buf) == 3 && buf[2] == 'Z') { + // CHECK-GEN: HIT + printf("HIT\n"); + } else { + // CHECK-ORIG: miss + printf("miss\n"); + } + return 0; +}