diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fba8df37..fb90068c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,27 @@ jobs: - uses: actions/checkout@v4 - name: install dependencies - run: sudo apt-get update && sudo apt-get install -y llvm-14 clang-14 libc++-14-dev libc++abi-14-dev python3-minimal libz3-dev libgoogle-perftools-dev libboost-container-dev python3-dev + run: sudo apt-get update && sudo apt-get install -y llvm-14 clang-14 libc++-14-dev libc++abi-14-dev python3-minimal libgoogle-perftools-dev libboost-container-dev python3-dev libbsd-dev + + - name: Cache Z3 + id: cache-z3 + uses: actions/cache@v4 + with: + path: ~/z3 + key: z3-4.15.4-x64-glibc-2.39 + + - name: Install Z3 + run: | + if [ ! -d ~/z3 ]; then + wget https://github.com/Z3Prover/z3/releases/download/z3-4.15.4/z3-4.15.4-x64-glibc-2.39.zip + unzip z3-4.15.4-x64-glibc-2.39.zip + mv z3-4.15.4-x64-glibc-2.39 ~/z3 + fi + sudo cp ~/z3/bin/z3 /usr/local/bin/ + sudo cp ~/z3/bin/libz3.so /usr/local/lib/ + sudo cp ~/z3/bin/libz3.a /usr/local/lib/ + sudo cp -r ~/z3/include/* /usr/local/include/ + sudo ldconfig # run: | # wget https://apt.llvm.org/llvm.sh # chmod +x llvm.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 52cf8dff..718d83c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,42 @@ project(symsan VERSION 1.2.2 LANGUAGES C CXX ASM) find_package(LLVM 14 REQUIRED CONFIG) +# Find Z3 (minimum version 4.8.15 required for string theory APIs) +# Prefer /usr/local over system +find_library(Z3_LIBRARY NAMES z3 PATHS /usr/local/lib NO_DEFAULT_PATH) +if (NOT Z3_LIBRARY) + find_library(Z3_LIBRARY NAMES z3) +endif() +find_path(Z3_INCLUDE_DIR NAMES z3.h PATHS /usr/local/include NO_DEFAULT_PATH) +if (NOT Z3_INCLUDE_DIR) + find_path(Z3_INCLUDE_DIR NAMES z3.h) +endif() + +# Check Z3 version +if (Z3_INCLUDE_DIR) + file(READ "${Z3_INCLUDE_DIR}/z3_version.h" Z3_VERSION_CONTENT) + string(REGEX MATCH "#define Z3_MAJOR_VERSION[ \t]+([0-9]+)" _ "${Z3_VERSION_CONTENT}") + set(Z3_VERSION_MAJOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define Z3_MINOR_VERSION[ \t]+([0-9]+)" _ "${Z3_VERSION_CONTENT}") + set(Z3_VERSION_MINOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define Z3_BUILD_NUMBER[ \t]+([0-9]+)" _ "${Z3_VERSION_CONTENT}") + set(Z3_VERSION_PATCH ${CMAKE_MATCH_1}) + set(Z3_VERSION "${Z3_VERSION_MAJOR}.${Z3_VERSION_MINOR}.${Z3_VERSION_PATCH}") + + message(STATUS "Found Z3 version: ${Z3_VERSION}") + + # Require at least version 4.8.15 + if (Z3_VERSION_MAJOR LESS 4 OR + (Z3_VERSION_MAJOR EQUAL 4 AND Z3_VERSION_MINOR LESS 8) OR + (Z3_VERSION_MAJOR EQUAL 4 AND Z3_VERSION_MINOR EQUAL 8 AND Z3_VERSION_PATCH LESS 15)) + message(FATAL_ERROR "Z3 version ${Z3_VERSION} found, but version 4.8.15 or later is required (for string theory APIs)") + endif() +endif() + +message(STATUS "Z3_LIBRARY: ${Z3_LIBRARY}") +message(STATUS "Z3_INCLUDE_DIR: ${Z3_INCLUDE_DIR}") +message(STATUS "Z3_VERSION: ${Z3_VERSION}") + if (LLVM_FOUND) message(STATUS "LLVM_VERSION_MAJOR: ${LLVM_VERSION_MAJOR}") message(STATUS "LLVM_VERSION_MINOR: ${LLVM_VERSION_MINOR}") diff --git a/backend/fastgen.cpp b/backend/fastgen.cpp index f952c186..abe3be7c 100644 --- a/backend/fastgen.cpp +++ b/backend/fastgen.cpp @@ -334,7 +334,7 @@ __taint_trace_memcmp(dfsan_label label) { uint16_t has_content = 1; // if both operands are symbolic, skip sending the content - if (info->l1 != CONST_LABEL && info->l2 != CONST_LABEL) + if ((info->l1 != CONST_LABEL && info->l2 != CONST_LABEL) || info->size == 0) has_content = 0; pipe_msg msg = { @@ -357,7 +357,10 @@ __taint_trace_memcmp(dfsan_label label) { size_t msg_size = sizeof(memcmp_msg) + info->size; memcmp_msg *mmsg = (memcmp_msg*)__builtin_alloca(msg_size); mmsg->label = label; - internal_memcpy(mmsg->content, (void*)info->op1.i, info->size); // concrete oprand is always in op1 + // Copy concrete content: use op1 if l1 is concrete, else op2 + void *concrete_ptr = (info->l1 == CONST_LABEL) ? (void*)info->op1.i : (void*)info->op2.i; + internal_memcpy(mmsg->content, concrete_ptr, info->size); + AOUT("sending memcmp content for label %d, size %u, msg_size=%lu\n", label, info->size, msg_size); // FIXME: assuming single writer so msg will arrive in the same order if (internal_write(__pipe_fd, mmsg, msg_size) < 0) { diff --git a/compiler/ko_clang.c b/compiler/ko_clang.c index 03cd225d..428223a3 100644 --- a/compiler/ko_clang.c +++ b/compiler/ko_clang.c @@ -179,7 +179,9 @@ static void add_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++] = "-lz3"; + cc_params[cc_par_cnt++] = "-Wl,-rpath,/usr/local/lib"; } if (getenv("KO_USE_FASTGEN")) { diff --git a/driver/CMakeLists.txt b/driver/CMakeLists.txt index ba3d604e..a3b22674 100644 --- a/driver/CMakeLists.txt +++ b/driver/CMakeLists.txt @@ -13,7 +13,7 @@ target_include_directories(FGTest PUBLIC target_link_libraries(FGTest PRIVATE launcher z3parser - z3 + ${Z3_LIBRARY} rt ) install (TARGETS FGTest DESTINATION ${SYMSAN_BIN_DIR}) diff --git a/driver/fgtest.cpp b/driver/fgtest.cpp index 01a8c316..9f31a056 100644 --- a/driver/fgtest.cpp +++ b/driver/fgtest.cpp @@ -10,6 +10,7 @@ extern "C" { #include "parse-z3.h" +#include #include #include #include @@ -45,33 +46,71 @@ static const char* __output_dir = "."; static uint32_t __instance_id = 0; static uint32_t __session_id = 0; static uint32_t __current_index = 0; +static int __enum_gep = 0; // GEP enumeration enabled by default static z3::context __z3_context; // z3parser symsan::Z3ParserSolver *__z3_parser = nullptr; static void generate_input(symsan::Z3ParserSolver::solution_t &solutions) { + using op_t = symsan::Z3ParserSolver::solution_op_t; + + // Build the new input in memory to handle INSERT/DELETE properly + std::vector new_input(input_buf, input_buf + input_size); + + // Sort solutions by offset in descending order so INSERT/DELETE don't + // invalidate subsequent offsets + std::vector order(solutions.size()); + for (size_t i = 0; i < order.size(); ++i) order[i] = i; + std::sort(order.begin(), order.end(), [&solutions](size_t a, size_t b) { + return solutions[a].offset > solutions[b].offset; + }); + + for (size_t idx : order) { + const auto& sol = solutions[idx]; + switch (sol.op) { + case op_t::SET: + if (sol.offset < new_input.size()) { + AOUT("SET offset %d = %x\n", sol.offset, sol.val); + new_input[sol.offset] = sol.val; + } + break; + + case op_t::INSERT: + if (sol.offset <= new_input.size()) { + AOUT("INSERT %zu bytes at offset %d\n", sol.data.size(), sol.offset); + new_input.insert(new_input.begin() + sol.offset, + sol.data.begin(), sol.data.end()); + } + break; + + case op_t::DELETE: + if (sol.offset < new_input.size()) { + size_t del_len = std::min((size_t)sol.len, + new_input.size() - sol.offset); + AOUT("DELETE %zu bytes at offset %d\n", del_len, sol.offset); + new_input.erase(new_input.begin() + sol.offset, + new_input.begin() + sol.offset + del_len); + } + break; + } + } + + // Write the new input to file char path[PATH_MAX]; snprintf(path, PATH_MAX, "%s/id-%d-%d-%d", __output_dir, __instance_id, __session_id, __current_index++); - int fd = open(path, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); + int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR); if (fd == -1) { AOUT("failed to open new input file for write"); return; } - if (write(fd, input_buf, input_size) == -1) { - AOUT("failed to copy original input\n"); - close(fd); - return; - } - AOUT("generate #%d output\n", __current_index - 1); + AOUT("generate #%d output (size: %zu -> %zu)\n", + __current_index - 1, input_size, new_input.size()); - for (auto const& sol : solutions) { - uint8_t value = sol.val; - AOUT("offset %d = %x\n", sol.offset, value); - lseek(fd, sol.offset, SEEK_SET); - write(fd, &value, sizeof(value)); + if (write(fd, new_input.data(), new_input.size()) == -1) { + AOUT("failed to write new input\n"); } close(fd); @@ -89,7 +128,7 @@ static void __solve_cond(dfsan_label label, uint8_t r, bool add_nested, void *ad for (auto id : tasks) { // solve symsan::Z3ParserSolver::solution_t solutions; - auto status = __z3_parser->solve_task(id, 5000U, solutions); + auto status = __z3_parser->solve_task(id, 30000U, solutions); // 30 seconds if (solutions.size() != 0) { AOUT("branch solved\n"); generate_input(solutions); @@ -111,14 +150,14 @@ static void __handle_gep(dfsan_label ptr_label, uptr ptr, std::vector tasks; if (__z3_parser->parse_gep(ptr_label, ptr, index_label, index, num_elems, - elem_size, current_offset, true, tasks)) { + elem_size, current_offset, __enum_gep, tasks)) { AOUT("WARNING: failed to parse gep %d @%p\n", index_label, addr); return; } for (auto id : tasks) { symsan::Z3ParserSolver::solution_t solutions; - auto status = __z3_parser->solve_task(id, 5000U, solutions); + auto status = __z3_parser->solve_task(id, 30000U, solutions); // 30 seconds if (solutions.size() != 0) { AOUT("gep solved\n"); generate_input(solutions); @@ -173,6 +212,13 @@ int main(int argc, char* const argv[]) { debug = 1; } + // check for session_id + char *session_opt = strstr(options, "session_id="); + if (session_opt) { + session_opt += strlen("session_id="); + __session_id = atoi(session_opt); + } + // check if solve_ub is enabled char *solve_ub_opt = strstr(options, "solve_ub="); if (solve_ub_opt) { @@ -180,6 +226,14 @@ int main(int argc, char* const argv[]) { if (strcmp(solve_ub_opt, "1") == 0 || strcmp(solve_ub_opt, "true") == 0) solve_ub = 1; } + + // check if GEP enumeration is disabled + char *enum_gep_opt = strstr(options, "enum_gep="); + if (enum_gep_opt) { + enum_gep_opt += strlen("enum_gep="); // skip "enum_gep=" + if (strncmp(enum_gep_opt, "0", 1) == 0 || strncmp(enum_gep_opt, "false", 5) == 0) + __enum_gep = 0; + } } // load input file diff --git a/include/parse-z3.h b/include/parse-z3.h index 692da3cd..46c3e118 100644 --- a/include/parse-z3.h +++ b/include/parse-z3.h @@ -34,8 +34,23 @@ class Z3AstParser : public ASTParser { z3::context &context_; const char* input_name_format; const char* atoi_name_format; + const char* strlen_name_format; + + // String ranges for null-byte post-processing (input_id -> list of (start, end)) + std::unordered_map>> string_ranges_; + + // String info cache: label -> (input_id, offset, length) + struct string_info_t { + uint32_t input_id; + uint32_t offset; + uint32_t length; + }; + std::unordered_map string_info_cache_; private: + // Original input cache + std::vector inputs_cache_; + // fsize flag bool has_fsize; @@ -43,7 +58,6 @@ class Z3AstParser : public ASTParser { using input_dep_set_t = std::unordered_set; // caches - std::vector inputs_cache_; std::vector tsize_cache_; std::vector deps_cache_; std::vector expr_cache_; @@ -116,6 +130,11 @@ class Z3AstParser : public ASTParser { void construct_index_tasks(z3::expr &index, uint64_t curr, uint64_t lb, uint64_t ub, uint64_t step, z3_task_t &nested, std::vector &tasks); + + // String theory helpers for strchr/strstr + 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); }; class Z3ParserSolver : public Z3AstParser { @@ -125,10 +144,35 @@ class Z3ParserSolver : public Z3AstParser { : Z3AstParser(base, size, context) {} ~Z3ParserSolver() {} + // Solution operation types + enum class solution_op_t : uint8_t { + SET, // Set byte at offset to val + INSERT, // Insert bytes at offset (shifts following bytes right) + DELETE // Delete len bytes starting at offset (shifts following bytes left) + }; + struct solution_val { - uint32_t id; - uint32_t offset; - uint8_t val; + solution_op_t op; + uint32_t id; // input id + uint32_t offset; // position in file + union { + uint8_t val; // for SET: the byte value + uint32_t len; // for DELETE: number of bytes to delete + }; + std::vector data; // for INSERT: bytes to insert + + // Constructors for convenience + // SET: set single byte at offset + solution_val(uint32_t id, uint32_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) + : 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) + : op(op), id(id), offset(offset), len(len) {} }; enum solving_status { diff --git a/instrumentation/TaintPass.cpp b/instrumentation/TaintPass.cpp index 3b0306a1..12a2929f 100644 --- a/instrumentation/TaintPass.cpp +++ b/instrumentation/TaintPass.cpp @@ -356,6 +356,13 @@ class Taint { WK_Memcmp, WK_Strcmp, WK_Strncmp, + WK_Strchr, // strchr/memchr - find first char occurrence + WK_Strrchr, // strrchr/memrchr - find last char occurrence + WK_Strstr, // strstr/memmem - find substring + WK_Prefixof, // prefix check (e.g., g_str_has_prefix) + WK_Suffixof, // suffix check (e.g., g_str_has_suffix) + WK_Strcat, // strcat/strncat - string concatenation + WK_Strsub, // substr(s, start, len) - substring from start with len }; Module *Mod; @@ -368,6 +375,7 @@ class Taint { IntegerType *PrimitiveShadowTy; PointerType *PrimitiveShadowPtrTy; IntegerType *IntptrTy; + PointerType *VoidPtrTy; ConstantInt *ZeroPrimitiveShadow; ConstantInt *UninitializedPrimitiveShadow; ConstantInt *ShadowPtrAndMask; @@ -390,15 +398,14 @@ class Taint { FunctionType *TaintTraceSelectFnTy; FunctionType *TaintTraceIndirectCallFnTy; FunctionType *TaintTraceGEPFnTy; + FunctionType *TaintTraceGEPPtrFnTy; FunctionType *TaintPushStackFrameFnTy; FunctionType *TaintPopStackFrameFnTy; FunctionType *TaintTraceAllocaFnTy; FunctionType *TaintCheckBoundsFnTy; FunctionType *TaintSolveBoundsFnTy; + FunctionType *TaintSolveSizeFnTy; FunctionType *TaintTraceGlobalFnTy; - FunctionType *TaintMemcmpFnTy; - FunctionType *TaintStrcmpFnTy; - FunctionType *TaintStrncmpFnTy; FunctionType *TaintDebugFnTy; FunctionCallee TaintUnionFn; FunctionCallee TaintCheckedUnionFn; @@ -415,15 +422,14 @@ class Taint { FunctionCallee TaintTraceSelectFn; FunctionCallee TaintTraceIndirectCallFn; FunctionCallee TaintTraceGEPFn; + FunctionCallee TaintTraceGEPPtrFn; FunctionCallee TaintPushStackFrameFn; FunctionCallee TaintPopStackFrameFn; FunctionCallee TaintTraceAllocaFn; FunctionCallee TaintCheckBoundsFn; FunctionCallee TaintSolveBoundsFn; + FunctionCallee TaintSolveSizeFn; FunctionCallee TaintTraceGlobalFn; - FunctionCallee TaintMemcmpFn; - FunctionCallee TaintStrcmpFn; - FunctionCallee TaintStrncmpFn; FunctionCallee TaintDebugFn; SmallPtrSet TaintRuntimeFunctions; Constant *CallStack; @@ -928,6 +934,7 @@ bool Taint::initializeModule(Module &M) { PrimitiveShadowTy = IntegerType::get(*Ctx, ShadowWidthBits); PrimitiveShadowPtrTy = PointerType::getUnqual(PrimitiveShadowTy); IntptrTy = DL.getIntPtrType(*Ctx); + VoidPtrTy = PointerType::getUnqual(Int8Ty); ZeroPrimitiveShadow = ConstantInt::getSigned(PrimitiveShadowTy, 0); UninitializedPrimitiveShadow = ConstantInt::getSigned(PrimitiveShadowTy, -1); ShadowPtrMul = ConstantInt::get(IntptrTy, ShadowWidthBytes); @@ -982,6 +989,9 @@ 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( @@ -995,19 +1005,12 @@ bool Taint::initializeModule(Module &M) { { PrimitiveShadowTy, Int64Ty, PrimitiveShadowTy, Int64Ty }, false); TaintSolveBoundsFnTy = FunctionType::get( Type::getVoidTy(*Ctx), TaintTraceGEPArgs, false); // use the same args as GEP + TaintSolveSizeFnTy = FunctionType::get( + Type::getVoidTy(*Ctx), + { PrimitiveShadowTy, Int64Ty, PrimitiveShadowTy, Int64Ty, Int32Ty }, false); TaintTraceGlobalFnTy = FunctionType::get( PrimitiveShadowTy, { Int64Ty, Int64Ty }, false); - TaintMemcmpFnTy = FunctionType::get( - PrimitiveShadowTy, - { Type::getInt8PtrTy(*Ctx), Type::getInt8PtrTy(*Ctx), Int64Ty }, false); - TaintStrcmpFnTy = FunctionType::get( - PrimitiveShadowTy, - { Type::getInt8PtrTy(*Ctx), Type::getInt8PtrTy(*Ctx) }, false); - TaintStrncmpFnTy = FunctionType::get( - PrimitiveShadowTy, - { Type::getInt8PtrTy(*Ctx), Type::getInt8PtrTy(*Ctx), Int64Ty }, false); - TaintDebugFnTy = FunctionType::get(Type::getVoidTy(*Ctx), {PrimitiveShadowTy, PrimitiveShadowTy, PrimitiveShadowTy, PrimitiveShadowTy, PrimitiveShadowTy}, false); @@ -1038,6 +1041,20 @@ Taint::WrapperKind Taint::getWrapperKind(Function *F) { return WK_Strcmp; if (ABIList.isIn(*F, "strncmp")) return WK_Strncmp; + if (ABIList.isIn(*F, "strchr")) + return WK_Strchr; + if (ABIList.isIn(*F, "strrchr")) + return WK_Strrchr; + if (ABIList.isIn(*F, "strstr")) + return WK_Strstr; + if (ABIList.isIn(*F, "prefixof")) + return WK_Prefixof; + if (ABIList.isIn(*F, "suffixof")) + return WK_Suffixof; + if (ABIList.isIn(*F, "strcat")) + return WK_Strcat; + if (ABIList.isIn(*F, "strsub")) + return WK_Strsub; if (ABIList.isIn(*F, "functional")) return WK_Functional; if (ABIList.isIn(*F, "discard")) @@ -1280,6 +1297,13 @@ 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); @@ -1329,24 +1353,10 @@ void Taint::initializeCallbackFunctions(Module &M) { AttributeList AL; AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); AL = AL.addFnAttribute(M.getContext(), Attribute::NoMerge); + AL = AL.addParamAttribute(M.getContext(), 0, Attribute::ZExt); AL = AL.addParamAttribute(M.getContext(), 2, Attribute::ZExt); - TaintMemcmpFn = - Mod->getOrInsertFunction("__taint_memcmp", TaintMemcmpFnTy, AL); - } - { - AttributeList AL; - AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); - AL = AL.addFnAttribute(M.getContext(), Attribute::NoMerge); - TaintStrcmpFn = - Mod->getOrInsertFunction("__taint_strcmp", TaintStrcmpFnTy, AL); - } - { - AttributeList AL; - AL = AL.addFnAttribute(M.getContext(), Attribute::NoUnwind); - AL = AL.addFnAttribute(M.getContext(), Attribute::NoMerge); - AL = AL.addParamAttribute(M.getContext(), 2, Attribute::ZExt); - TaintStrncmpFn = - Mod->getOrInsertFunction("__taint_strncmp", TaintStrncmpFnTy, AL); + TaintSolveSizeFn = + Mod->getOrInsertFunction("__taint_solve_size", TaintSolveSizeFnTy, AL); } TaintRuntimeFunctions.insert( @@ -1363,6 +1373,8 @@ 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( @@ -1376,11 +1388,7 @@ void Taint::initializeCallbackFunctions(Module &M) { TaintRuntimeFunctions.insert( TaintSolveBoundsFn.getCallee()->stripPointerCasts()); TaintRuntimeFunctions.insert( - TaintMemcmpFn.getCallee()->stripPointerCasts()); - TaintRuntimeFunctions.insert( - TaintStrcmpFn.getCallee()->stripPointerCasts()); - TaintRuntimeFunctions.insert( - TaintStrncmpFn.getCallee()->stripPointerCasts()); + TaintSolveSizeFn.getCallee()->stripPointerCasts()); } bool Taint::runImpl(Module &M) { @@ -1894,13 +1902,10 @@ void TaintFunction::solveBounds(Value *Ptr, Value* Size, Instruction *Pos) { PtrShadow = getShadow(Ptr); } Value *Addr = IRB.CreatePtrToInt(Ptr, TT.Int64Ty); - Value *Index = IRB.CreateZExtOrTrunc(Size, TT.Int64Ty); - ConstantInt *NumEl = ConstantInt::get(TT.Int64Ty, 0); // no allocation size - ConstantInt *ElSize = ConstantInt::get(TT.Int64Ty, 1); // bytes array - ConstantInt *Offset = ConstantInt::get(TT.Int64Ty, 0); // no offset + Value *Size64 = IRB.CreateZExtOrTrunc(Size, TT.Int64Ty); ConstantInt *CID = ConstantInt::get(TT.Int32Ty, TT.getInstructionId(Pos)); - IRB.CreateCall(TT.TaintSolveBoundsFn, - {PtrShadow, Addr, SizeShadow, Index, NumEl, ElSize, Offset, CID}); + IRB.CreateCall(TT.TaintSolveSizeFn, + {PtrShadow, Addr, SizeShadow, Size64, CID}); } // Generates IR to load shadow corresponding to bytes [Addr, Addr+Size), where @@ -2492,6 +2497,13 @@ void TaintFunction::visitGEPInst(GetElementPtrInst *I) { // propagate bounds info setShadow(I, Bounds); } + + // 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)) { + IRBuilder<> IRB(I->getNextNode()); + Bounds = IRB.CreateCall(TT.TaintTraceGEPPtrFn, {Bounds, I, Base}); + } } void TaintVisitor::visitGetElementPtrInst(GetElementPtrInst &GEPI) { @@ -2784,6 +2796,7 @@ void TaintVisitor::addShadowArguments(Function *F, CallBase &CB, bool TaintVisitor::visitWrappedCallBase(Function *F, CallBase &CB) { IRBuilder<> IRB(&CB); Value *Shadow = nullptr; + FunctionType *FT = F->getFunctionType(); switch (TF.TT.getWrapperKind(F)) { case Taint::WK_Warning: CB.setCalledFunction(F); @@ -2800,32 +2813,209 @@ bool TaintVisitor::visitWrappedCallBase(Function *F, CallBase &CB) { //FIXME: // visitOperandShadowInst(CS); return true; - case Taint::WK_Memcmp: - CB.setCalledFunction(F); - assert(CB.arg_size() == 3); - Shadow = IRB.CreateCall(TF.TT.TaintMemcmpFn, - {CB.getArgOperand(0), - CB.getArgOperand(1), - CB.getArgOperand(2)}); - TF.setShadow(&CB, Shadow); + case Taint::WK_Memcmp: { + // int memcmp(const void *s1, const void *s2, size_t n) + assert(CB.arg_size() == 3 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_memcmp", CustomFn.TransformedType); + + std::vector Args; + // Add original arguments + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + // Add shadow arguments (including return label pointer) + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + // Load return shadow + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); return true; - case Taint::WK_Strcmp: - CB.setCalledFunction(F); - assert(CB.arg_size() == 2); - Shadow = IRB.CreateCall(TF.TT.TaintStrcmpFn, - {CB.getArgOperand(0), - CB.getArgOperand(1)}); - TF.setShadow(&CB, Shadow); + } + case Taint::WK_Strcmp: { + // int strcmp(const char *s1, const char *s2) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strcmp", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); return true; - case Taint::WK_Strncmp: - CB.setCalledFunction(F); - assert(CB.arg_size() == 3); - Shadow = IRB.CreateCall(TF.TT.TaintStrncmpFn, - {CB.getArgOperand(0), - CB.getArgOperand(1), - CB.getArgOperand(2)}); - TF.setShadow(&CB, Shadow); + } + case Taint::WK_Strncmp: { + // int strncmp(const char *s1, const char *s2, size_t n) + assert(CB.arg_size() == 3 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strncmp", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } + case Taint::WK_Strchr: { + // char *strchr(char *s, int c) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strchr", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } + case Taint::WK_Strrchr: { + // char *strrchr(char *s, int c) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strrchr", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } + case Taint::WK_Strstr: { + // char *strstr(char *haystack, char *needle) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strstr", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } + case Taint::WK_Prefixof: { + // int prefixof(const char *str, const char *prefix) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_prefixof", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); return true; + } + case Taint::WK_Suffixof: { + // int suffixof(const char *str, const char *suffix) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_suffixof", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } + case Taint::WK_Strcat: { + // char *strcat(char *dest, const char *src) + assert(CB.arg_size() == 2 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strcat", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } + case Taint::WK_Strsub: { + // char *strsub(char *s, size_t len) + assert(CB.arg_size() == 3 && !FT->getReturnType()->isVoidTy()); + TransformedFunction CustomFn = TF.TT.getCustomFunctionType(FT); + FunctionCallee DfswFn = TF.TT.Mod->getOrInsertFunction("__dfsw_strsub", CustomFn.TransformedType); + + std::vector Args; + for (unsigned i = 0; i < FT->getNumParams(); i++) + Args.push_back(CB.getArgOperand(i)); + addShadowArguments(F, CB, Args, IRB); + + CallInst *CustomCI = IRB.CreateCall(DfswFn, Args); + + LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(FT->getReturnType()), TF.LabelReturnAlloca); + TF.setShadow(CustomCI, LabelLoad); + + CB.replaceAllUsesWith(CustomCI); + CB.eraseFromParent(); + return true; + } case Taint::WK_Custom: // Don't try to handle invokes of custom functions, it's too complicated. // Instead, invoke the dfsw$ wrapper, which will in turn call the __dfsw_ @@ -2902,6 +3092,7 @@ bool TaintVisitor::visitWrappedCallBase(Function *F, CallBase &CB) { LoadInst *LabelLoad = IRB.CreateLoad(TF.TT.getShadowTy(RetTy), TF.LabelReturnAlloca); TF.setShadow(CustomCI, LabelLoad); + } CI->replaceAllUsesWith(CustomCI); diff --git a/runtime/dfsan/dfsan.cpp b/runtime/dfsan/dfsan.cpp index 2f41b2f6..ddd3d31f 100644 --- a/runtime/dfsan/dfsan.cpp +++ b/runtime/dfsan/dfsan.cpp @@ -19,6 +19,7 @@ //===----------------------------------------------------------------------===// #include "sanitizer_common/sanitizer_atomic.h" +#include "sanitizer_common/sanitizer_allocator_internal.h" #include "sanitizer_common/sanitizer_common.h" #include "sanitizer_common/sanitizer_file.h" #include "sanitizer_common/sanitizer_flags.h" @@ -124,8 +125,15 @@ static void dfsan_check_label(dfsan_label label) { if (label == kInitializingLabel) { Report("FATAL: Taint: out of labels\n"); Die(); - } else if (label >= __alloca_stack_top) { - Report("FATAL: Exhausted labels\n"); + } + // Alloca labels are in range [__alloca_stack_top, __alloca_stack_bottom] + if (label >= __alloca_stack_top && label <= __alloca_stack_bottom) { + return; // Valid Alloca label + } + // For regular labels, check against __dfsan_last_label + dfsan_label last = atomic_load(&__dfsan_last_label, memory_order_relaxed); + if (label > last) { + Report("FATAL: Invalid label %u > last %u\n", label, last); Die(); } } @@ -214,18 +222,21 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, // backup old op-values uint64_t orig_op1 = op1, orig_op2 = op2; - // special handling for bounds, which may use all four fields - // fatoi also uses both concrete operand fields - // record icmp and fmemcmp operands as well + // Preserve op1/op2 for certain operations: + // - Alloca: uses op1/op2 for bounds tracking + // - ICmp: records both operands for comparison + // - Higher-order ops (>= fmemcmp): use op1/op2 for various purposes if (op == __dfsan::fmemcmp) { - // XXX: hacky, but maybe good enough for i2s inference - // for symbolic operand, record a piece (up to 8 bytes) of the data + // fmemcmp special: copy up to 8 bytes of the data for i2s inference uint16_t len = size > 8 ? 8 : size; // for fmemcmp, size is in bytes, not bits if (l1 >= CONST_OFFSET) internal_memcpy(&op1, (void*)op1, len); if (l2 >= CONST_OFFSET) internal_memcpy(&op2, (void*)op2, len); - } else if (op != __dfsan::Alloca && - (op & 0xff) != __dfsan::ICmp && - op != __dfsan::fatoi) { + } else if (op < __dfsan::fmemcmp && + op != __dfsan::Alloca && + op != __dfsan::PtrToInt && + (op & 0xff) != __dfsan::ICmp) { + // Not a higher-order op and not Alloca/ICmp/PtrToInt - zero out for symbolic operands + // PtrToInt needs op1 preserved to compute base pointer for string ops if (l1 >= CONST_OFFSET) op1 = 0; if (l2 >= CONST_OFFSET) op2 = 0; } @@ -854,11 +865,11 @@ void __taint_solve_bounds(dfsan_label ptr_label, uint64_t ptr, 64, index, lower_bound); __taint_trace_cond(lb, 0, UndefinedCheck, ub_index_underflow); - // check overflow, index * elem_size + current_offset + ptr >= upper_bound - // => index >= (upper_bound - current_offset - ptr) / elem_size + // 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; - dfsan_label ub = __taint_union(index_label, 0, (bvuge << 8) | ICmp, + (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); __taint_trace_cond(ub, 0, UndefinedCheck, ub_index_overflow); } else { @@ -887,6 +898,84 @@ void __taint_solve_bounds(dfsan_label ptr_label, uint64_t ptr, } } +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) + return; + + void *addr = __builtin_return_address(0); + + if (size_label == kInitializingLabel) { + // uninitialized label + AOUT("WARNING: uninitialized size label %u @%p\n", size_label, addr); + __taint_trace_memerr(ptr_label, ptr, size_label, size, F_MEMERR_UBI, addr); + if (flags().exit_on_memerror) Die(); + else return; + } + if (ptr_label == kInitializingLabel) { + // uninitialized label + AOUT("WARNING: uninitialized pointer label %u @%p\n", ptr_label, addr); + __taint_trace_memerr(ptr_label, ptr, size_label, size, F_MEMERR_UBI, addr); + if (flags().exit_on_memerror) Die(); + else return; + } + + AOUT("solve size: %lu = %d, ptr: %p = %d\n", + size, size_label, (void*)ptr, ptr_label); + + // construct size solving tasks here + uint16_t size_bits = get_label_info(size_label)->size; + + // check overflow with buffer bounds if ptr has bounds info + if (ptr_label != 0) { + dfsan_label_info *bounds_info = get_label_info(ptr_label); + 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); + + if (bounds_info->l2 == 0) { + // concrete allocation size + // 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); + __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); + __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); + uint64_t actual_size = size + offset; + dfsan_label overflow = __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); + } + } +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void dfsan_store_label(dfsan_label l, void *addr, uptr size) { if (l == 0) return; @@ -1292,6 +1381,188 @@ static void InitializeTaintSocket() { } } +// Hash tables for string label tracking +static uptr content_map_capacity = 0; +static struct { + uptr addr; + dfsan_label label; +} *__taint_content_map = nullptr; +static uptr content_map_count = 0; + +static uptr indexof_map_capacity = 0; +static struct { + uptr addr; + dfsan_label label; +} *__taint_indexof_map = nullptr; +static uptr indexof_map_count = 0; + +// Hash function optimized for shadow memory addresses (0x700000040000 ~ 0x800000000000) +// Focus on middle bits where entropy is highest +static inline uptr hash_addr(uptr addr, uptr capacity) { + addr >>= 3; // Remove low 3 bits (8-byte alignment) + addr *= 2654435769UL; // Multiplicative hash + return addr & (capacity - 1); // Fast modulo for power-of-2 +} + +// 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)); + + // Rehash existing entries + for (uptr i = 0; i < content_map_capacity; i++) { + if (__taint_content_map[i].addr != 0) { + uptr hash = hash_addr(__taint_content_map[i].addr, new_capacity); + while (new_map[hash].addr != 0) { + hash = (hash + 1) & (new_capacity - 1); + } + new_map[hash] = __taint_content_map[i]; + } + } + + InternalFree(__taint_content_map); + __taint_content_map = new_map; + content_map_capacity = new_capacity; +} + +// 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)); + + for (uptr i = 0; i < indexof_map_capacity; i++) { + if (__taint_indexof_map[i].addr != 0) { + uptr hash = hash_addr(__taint_indexof_map[i].addr, new_capacity); + while (new_map[hash].addr != 0) { + hash = (hash + 1) & (new_capacity - 1); + } + new_map[hash] = __taint_indexof_map[i]; + } + } + + InternalFree(__taint_indexof_map); + __taint_indexof_map = new_map; + indexof_map_capacity = new_capacity; +} + +static void InitializeStringMaps() { + // Round up to nearest power of 2 for efficient hashing + uptr capacity = flags().string_map_capacity; + if (capacity < 16) capacity = 16; // Minimum size + // Round up to power of 2 + capacity--; + capacity |= capacity >> 1; + capacity |= capacity >> 2; + capacity |= capacity >> 4; + capacity |= capacity >> 8; + capacity |= capacity >> 16; + capacity |= capacity >> 32; + capacity++; + + // 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)); + 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)); + indexof_map_count = 0; +} + +extern "C" void taint_set_str_content_label(void *addr, dfsan_label label) { + AOUT("taint_set_str_content_label: addr=%p, label=%u\n", addr, label); + + // Grow if needed + if (content_map_count > (content_map_capacity * 7 / 10)) { + grow_content_map(); + } + + uptr hash = hash_addr((uptr)addr, content_map_capacity); + + // Linear probing + while (__taint_content_map[hash].addr != 0 && + __taint_content_map[hash].addr != (uptr)addr) { + hash = (hash + 1) & (content_map_capacity - 1); + } + + if (__taint_content_map[hash].addr == 0) { + content_map_count++; + } else { + AOUT("update content label: old = %u\n", __taint_content_map[hash].label); + } + + __taint_content_map[hash].addr = (uptr)addr; + __taint_content_map[hash].label = label; +} + +extern "C" dfsan_label taint_get_str_content_label(const void *addr) { + uptr hash = hash_addr((uptr)addr, content_map_capacity); + uptr start = hash; + + while (__taint_content_map[hash].addr != 0) { + if (__taint_content_map[hash].addr == (uptr)addr) { + AOUT("taint_get_str_content_label: addr=%p, found label=%u\n", + addr, __taint_content_map[hash].label); + return __taint_content_map[hash].label; + } + hash = (hash + 1) & (content_map_capacity - 1); + if (hash == start) break; + } + AOUT("addr=%p, not found\n", addr); + return 0; +} + +extern "C" void taint_set_str_indexof_label(void *addr, dfsan_label label) { + AOUT("taint_set_str_indexof_label: addr=%p, label=%u\n", addr, label); + + if (indexof_map_count > (indexof_map_capacity * 7 / 10)) { + grow_indexof_map(); + } + + uptr hash = hash_addr((uptr)addr, indexof_map_capacity); + + while (__taint_indexof_map[hash].addr != 0 && + __taint_indexof_map[hash].addr != (uptr)addr) { + hash = (hash + 1) & (indexof_map_capacity - 1); + } + + if (__taint_indexof_map[hash].addr == 0) { + indexof_map_count++; + } else { + AOUT("update indexof label: old = %u\n", __taint_indexof_map[hash].label); + } + + __taint_indexof_map[hash].addr = (uptr)addr; + __taint_indexof_map[hash].label = label; +} + +extern "C" dfsan_label taint_get_str_indexof_label(const void *addr) { + uptr hash = hash_addr((uptr)addr, indexof_map_capacity); + uptr start = hash; + + while (__taint_indexof_map[hash].addr != 0) { + if (__taint_indexof_map[hash].addr == (uptr)addr) { + AOUT("addr=%p, found label=%u\n", addr, __taint_indexof_map[hash].label); + return __taint_indexof_map[hash].label; + } + hash = (hash + 1) & (indexof_map_capacity - 1); + if (hash == start) break; + } + AOUT("addr=%p, not found\n", addr); + return 0; +} + // information is passed implicitly through flags() extern "C" void InitializeSolver(); @@ -1399,6 +1670,8 @@ if (flags().shm_fd != -1) { InitializeTaintSocket(); + InitializeStringMaps(); + InitializeSolver(); // Register the fini callback to run when the program terminates successfully diff --git a/runtime/dfsan/dfsan.h b/runtime/dfsan/dfsan.h index 5847f43d..348211be 100644 --- a/runtime/dfsan/dfsan.h +++ b/runtime/dfsan/dfsan.h @@ -100,6 +100,12 @@ int is_stdin_taint(void); void taint_set_offset_label(dfsan_label label); dfsan_label taint_get_offset_label(); +// taint tracking for string operations +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); + // taint source utmp off_t get_utmp_offset(void); void set_utmp_offset(off_t offset); @@ -167,17 +173,32 @@ enum operators { #undef HANDLE_MEMORY_INST #undef HANDLE_CAST_INST #undef HANDLE_OTHER_INST -#undef LAST_OTHER_INST +#undef LAST_OTHER_INST // last_llvm_op = 67 for llvm14 // self-defined - Free = last_llvm_op + 3, - Extract = last_llvm_op + 4, - Concat = last_llvm_op + 5, - Arg = last_llvm_op + 6, + Free = last_llvm_op + 3, // 70 + Extract = last_llvm_op + 4, // 71 + Concat = last_llvm_op + 5, // 72 + Arg = last_llvm_op + 6, // 73 // higher-order - fmemcmp = last_llvm_op + 7, - fsize = last_llvm_op + 8, - fatoi = last_llvm_op + 9, - LastOp = last_llvm_op + 10, + fmemcmp = last_llvm_op + 7, // 74 + fsize = last_llvm_op + 8, // 75 + fatoi = last_llvm_op + 9, // 76 + fstrlen = last_llvm_op + 10, // 77 + // string search ops that return positions (for chaining detection) + fstr_op_start = last_llvm_op + 11, // 78 + fstrchr = last_llvm_op + 11, // 78 strchr/memchr + fstrrchr = last_llvm_op + 12, // 79 strrchr/memrchr + fstrstr = last_llvm_op + 13, // 80 strstr/memmem + fstrpbrk = last_llvm_op + 14, // 81 strpbrk - find first char from set + fstr_off = last_llvm_op + 15, // 82 string op + constant offset (for ptr arithmetic) + fsubstr = last_llvm_op + 16, // 83 substr(s, 0, len) - for bounded search + fstrcat = last_llvm_op + 17, // 84 strcat/strncat - string concatenation + fstr_op_end = last_llvm_op + 18, // 85 + // string comparison (returns 0/1, NOT a position - must be outside fstr_op range) + 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 }; enum predicate { @@ -219,6 +240,7 @@ static inline bool is_commutative(unsigned char op) { case Add: case Mul: case fmemcmp: + case fstrcmp: return true; default: return false; @@ -250,6 +272,9 @@ enum undefined_check_ids { ub_shift_base, ub_index_underflow, ub_index_overflow, + ub_size_underflow, + ub_size_overflow, + ub_size_to_buffer_overflow, ub_integer_to_buffer_overflow, ub_null_pointer, ub_unsigned_integer_truncation, diff --git a/runtime/dfsan/dfsan_custom.cpp b/runtime/dfsan/dfsan_custom.cpp index 11cd91ec..a03c98a6 100644 --- a/runtime/dfsan/dfsan_custom.cpp +++ b/runtime/dfsan/dfsan_custom.cpp @@ -63,6 +63,196 @@ 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) { + 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 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 0; +} + +// Unified method to get string label with explicit length +// Checks (in order): +// 1. Runtime content map (for strncpy/strcat destinations) +// 2. Pointer label itself being a content-type string op (for chaining) +// 3. indexOf map at address s for suffix case (strcpy from pos+1) +// 4. If n_label derives from a string op, create fsubstr to preserve constraint +// 5. Buffer content labels via dfsan_read_label +static inline dfsan_label get_str_label_n(const void *s, dfsan_label s_label, + size_t n, dfsan_label n_label) { + AOUT("get_str_label_n: s=%p, s_label=%u, n=%zu, n_label=%u\n", s, s_label, n, n_label); + + // 1. Check content map for fsubstr/fstrcat labels (from strncpy/strcat destinations) + dfsan_label content = taint_get_str_content_label(s); + if (content != 0) { + AOUT("get_str_label_n: step 1 returns content=%u\n", content); + return content; + } + + // 2. Check if pointer label itself is a content-type string op (for chaining) + // Only chain on fsubstr/fstrcat, NOT indexOf ops (fstrchr, fstrrchr, etc.) + if (s_label >= CONST_OFFSET) { + dfsan_label_info *info = dfsan_get_label_info(s_label); + AOUT("get_str_label_n: step 2 s_label op=%u, is_content=%d\n", + info ? info->op : 0, info ? is_content_string_op(info->op) : 0); + if (info && is_content_string_op(info->op)) { + AOUT("get_str_label_n: step 2 returns s_label=%u\n", s_label); + return s_label; + } + } + + // 3. Check for suffix case: searching from a previous indexOf result position + // Creates fsubstr(content, start_pos, remaining) for: + // a) strcpy(suffix, pos + 1) where gep_ptr stored fstr_off at pos+1 + // b) memchr(t1, c, len) where t1 was returned by previous indexOf + dfsan_label start_label = taint_get_str_indexof_label(s); + if (start_label != 0) { + dfsan_label_info *start_info = dfsan_get_label_info(start_label); + if (start_info) { + dfsan_label indexOf_label = 0; + + if (start_info->op == __dfsan::fstr_off && start_info->l1 >= CONST_OFFSET) { + // Case 3a: fstr_off points to indexOf op + indexOf_label = start_info->l1; + } else if (is_indexof_op(start_info->op)) { + // Case 3b: Direct indexOf - only if s_label confirms indexOf origin + // This distinguishes memchr(t1,...) from memchr(buf,...) when t1==buf + dfsan_label_info *s_info = (s_label >= CONST_OFFSET) ? + dfsan_get_label_info(s_label) : nullptr; + if (s_info && is_indexof_op(s_info->op)) { + indexOf_label = start_label; + } + } + + if (indexOf_label != 0) { + dfsan_label_info *idx_info = dfsan_get_label_info(indexOf_label); + if (idx_info && idx_info->l1 >= CONST_OFFSET) { + // Create suffix fsubstr: substr(content, start_pos, remaining) + // l1=content, l2=position label, op1=concrete len, op2=1 (suffix mode) + return dfsan_union(idx_info->l1, start_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)n, 1); + } + } + } + } + + // 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); + 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; + + if (str_op_content >= CONST_OFFSET) { + // Get content label from buffer if available for same-buffer verification + dfsan_label content_label = (n > 0) ? dfsan_read_label(s, n) : 0; + + // 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); + same_buffer = (src_base != 0 && src_base == str_op_base); + } + // When n=0, trust that n_label derives from same buffer + // (the alternative is losing the constraint entirely) + + if (same_buffer) { + // Create fsubstr: substr(str_op_content, 0, str_op_label) + // l1 = original content, l2 = string op label (index), op1 = concrete n, op2 = 0 + return dfsan_union(str_op_content, str_op_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)n, 0); + } + } + } + + // 5. Fall back to reading buffer content labels + return dfsan_read_label(s, n); +} + +// Unified method to get string label for null-terminated strings +// Uses strlen to determine length +// Also checks if null terminator was placed at a strchr/strstr result position +static inline dfsan_label get_str_label(const char *s, dfsan_label s_label) { + size_t len = strlen(s); + + // Check if null terminator was placed at a position found by strchr/strstr/etc. + // This allows us to recover symbolic length when code does: + // pos = strchr(buf, '_'); *pos = '\0'; strcpy(dest, buf); + dfsan_label term_label = taint_get_str_indexof_label(s + len); + + return get_str_label_n(s, s_label, len + 1, term_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)) @@ -71,6 +261,21 @@ static inline dfsan_label get_label_for(int fd, off_t offset) { else return (offset + CONST_OFFSET); } +static void *dfsan_memcpy(void *dest, const void *src, size_t n) { + if (n == 0) return dest; + dfsan_label *sdest = shadow_for(dest); + const dfsan_label *ssrc = shadow_for(src); + // FIXME: check and avoid copying labels? + internal_memcpy((void *)sdest, (const void *)ssrc, n * sizeof(dfsan_label)); + return internal_memcpy(dest, src, n); +} + +static void dfsan_memset(void *s, int c, dfsan_label c_label, size_t n) { + if (n == 0) return; + internal_memset(s, c, n); + dfsan_set_label(c_label, s, n); +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_offset(dfsan_label offset_label, int64_t offset, unsigned size); @@ -90,6 +295,11 @@ void __taint_solve_bounds(dfsan_label ptr_label, uint64_t ptr, uint64_t num_elems, uint64_t elem_size, int64_t current_offset, uint32_t cid); +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); + extern "C" SANITIZER_INTERFACE_ATTRIBUTE void __taint_trace_memerr(dfsan_label ptr_label, uptr ptr, dfsan_label size_label, uint64_t size, @@ -192,25 +402,71 @@ __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, dfsan_label *ret_label) { - *ret_label = 0; - return strchr(s, c); - /* FIXME - for (size_t i = 0;; ++i) { - if (s[i] == c || s[i] == 0) { - if (flags().strict_data_dependencies) { - *ret_label = s_label; - } else { - *ret_label = taint_union(taint_read_label(s, i + 1), - taint_union(s_label, c_label)); - } - return s[i] == 0 ? nullptr : const_cast(s+i); + char *ret = strchr(s, c); + + // Use unified get_str_label to get source label + // Handles str_map, pointer label fsubstr, and buffer content + dfsan_label src_label = get_str_label(s, s_label); + + // Create label if source or char is tainted + if (src_label != 0 || c_label != 0) { + // Determine which operand is concrete and set size accordingly + size_t haystack_len = strlen(s); + uint16_t content_len = (src_label == 0) ? (uint16_t)haystack_len : 0; + + // l1 = src_label (source - for chaining or content dependencies) + // l2 = c_label (target char - may be symbolic!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = char value + // size = haystack length if concrete, else 0 + *ret_label = dfsan_union(src_label, c_label, __dfsan::fstrchr, + content_len, + (uint64_t)s, + (uint64_t)(uint8_t)c); + + // Send concrete haystack content if haystack is concrete + if (content_len > 0 && *ret_label) { + __taint_trace_memcmp(*ret_label); + } + + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); } + } else { + *ret_label = 0; } - */ + return ret; } SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strpbrk(const char *s, @@ -218,31 +474,51 @@ SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strpbrk(const char *s, dfsan_label s_label, dfsan_label accept_label, dfsan_label *ret_label) { - *ret_label = 0; const char *ret = strpbrk(s, accept); - /* FIXME - if (flags().strict_data_dependencies) { - *ret_label = ret ? s_label : 0; + size_t accept_len = strlen(accept); + + // Use unified get_str_label for source string + dfsan_label src_label = get_str_label(s, s_label); + + // Use unified get_str_label for accept string + dfsan_label real_accept_label = get_str_label(accept, accept_label); + + if (src_label != 0 || real_accept_label != 0) { + // Determine which operand is concrete and set size accordingly + size_t haystack_len = strlen(s); + uint16_t content_len = 0; + if (src_label == 0) { + content_len = (uint16_t)haystack_len; + } else if (real_accept_label == 0) { + content_len = (uint16_t)accept_len; + } + + // l1 = src_label (source content) + // l2 = accept_label (character set - may be symbolic) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = accept pointer (for concrete content retrieval) + // size = haystack length if haystack concrete, else accept length if accept concrete, else 0 + dfsan_label label = dfsan_union(src_label, real_accept_label, __dfsan::fstrpbrk, + content_len, + (uint64_t)s, + (uint64_t)accept); + + // Send concrete content (haystack or accept) + if (content_len > 0 && label) { + __taint_trace_memcmp(label); + } + + *ret_label = label; + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(const_cast(ret), *ret_label); + } } else { - size_t s_bytes_read = (ret ? ret - s : strlen(s)) + 1; - *ret_label = - dfsan_union(dfsan_read_label(s, s_bytes_read), - dfsan_union(dfsan_read_label(accept, strlen(accept) + 1), - dfsan_union(s_label, accept_label))); + *ret_label = 0; } - */ return const_cast(ret); } -extern "C" SANITIZER_INTERFACE_ATTRIBUTE -dfsan_label __taint_memcmp(const void *s1, const void *s2, size_t n) { - dfsan_label l1 = dfsan_read_label(s1, n); - dfsan_label l2 = dfsan_read_label(s2, n); - dfsan_label ret = dfsan_union(l1, l2, fmemcmp, n, (uint64_t)s1, (uint64_t)s2); - if (ret) __taint_trace_memcmp(ret); - return ret; -} - DECLARE_WEAK_INTERCEPTOR_HOOK(dfsan_weak_hook_memcmp, uptr caller_pc, const void *s1, const void *s2, size_t n, dfsan_label s1_label, dfsan_label s2_label, @@ -258,8 +534,24 @@ SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_memcmp(const void *s1, const void *s2, __taint_check_bounds(s1_label, (uptr)s1, n_label, n); __taint_check_bounds(s2_label, (uptr)s2, n_label, n); int ret = memcmp(s1, s2, n); - //AOUT("memcmp: n = %lu\n", n); - *ret_label = __taint_memcmp(s1, s2, n); + + // Check for fsubstr labels + dfsan_label l1 = get_str_label_n(s1, s1_label, n, n_label); + dfsan_label l2 = get_str_label_n(s2, s2_label, n, n_label); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + return ret; + } + + // Check if either side is a string op - use string theory comparison + bool l1_is_string_op = (l1 >= CONST_OFFSET && is_string_op(dfsan_get_label_info(l1)->op)); + bool l2_is_string_op = (l2 >= CONST_OFFSET && is_string_op(dfsan_get_label_info(l2)->op)); + + uint16_t op = (l1_is_string_op || l2_is_string_op) ? __dfsan::fstrcmp : __dfsan::fmemcmp; + dfsan_label cmp = dfsan_union(l1, l2, op, n, (uint64_t)s1, (uint64_t)s2); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; return ret; } @@ -271,21 +563,24 @@ SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_bcmp(const void *s1, const void *s2, __taint_check_bounds(s1_label, (uptr)s1, n_label, n); __taint_check_bounds(s2_label, (uptr)s2, n_label, n); int ret = bcmp(s1, s2, n); - //AOUT("bcmp: n = %lu\n", n); - *ret_label = __taint_memcmp(s1, s2, n); - return ret; -} -extern "C" SANITIZER_INTERFACE_ATTRIBUTE -dfsan_label __taint_strcmp(const char *s1, const char *s2) { - size_t n = strlen(s1) + 1; // including tailing '\0' - if (dfsan_get_label(s1) != 0) - n = strlen(s2) + 1; // including tailing '\0' - dfsan_label l1 = dfsan_read_label(s1, n); - dfsan_label l2 = dfsan_read_label(s2, n); - // ugly hack ... - dfsan_label ret = dfsan_union(l1, l2, fmemcmp, n, (uint64_t)s1, (uint64_t)s2); - if (ret) __taint_trace_memcmp(ret); + // Check for fsubstr labels (from strncpy with symbolic length) + dfsan_label l1 = get_str_label_n(s1, s1_label, n, n_label); + dfsan_label l2 = get_str_label_n(s2, s2_label, n, n_label); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + return ret; + } + + // Check if either side is a string op - use string theory comparison + bool l1_is_string_op = (l1 >= CONST_OFFSET && is_string_op(dfsan_get_label_info(l1)->op)); + bool l2_is_string_op = (l2 >= CONST_OFFSET && is_string_op(dfsan_get_label_info(l2)->op)); + + uint16_t op = (l1_is_string_op || l2_is_string_op) ? __dfsan::fstrcmp : __dfsan::fmemcmp; + dfsan_label cmp = dfsan_union(l1, l2, op, n, (uint64_t)s1, (uint64_t)s2); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; return ret; } @@ -300,35 +595,192 @@ SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_strcmp(const char *s1, const char *s2, CALL_WEAK_INTERCEPTOR_HOOK(dfsan_weak_hook_strcmp, GET_CALLER_PC(), s1, s2, s1_label, s2_label); int ret = strcmp(s1, s2); - // check which one is tainted - //AOUT("strcmp: %s <=> %s\n", s1, s2); - *ret_label = __taint_strcmp(s1, s2); + + AOUT("strcmp: s1=%p s2=%p s1_label=%u s2_label=%u\n", s1, s2, s1_label, s2_label); + + // Use unified get_str_label to get labels for both strings + // Handles str_map, pointer label fsubstr, and buffer content + dfsan_label l1 = get_str_label(s1, s1_label); + dfsan_label l2 = get_str_label(s2, s2_label); + AOUT("strcmp: l1=%u l2=%u\n", l1, l2); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + } else { + // Determine length for comparison (use concrete side if one is fsubstr) + size_t n = strlen(s1) + 1; + dfsan_label s1_fsubstr = taint_get_str_content_label(s1); + if (s1_fsubstr != 0) + n = strlen(s2) + 1; // use concrete side for length + + // fstrcmp is commutative - dfsan_union will swap to put concrete in op1 + dfsan_label cmp = dfsan_union(l1, l2, __dfsan::fstrcmp, n, + (uint64_t)s1, (uint64_t)s2); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; + } + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_prefixof( + const char *str, const char *prefix, + dfsan_label str_label, dfsan_label prefix_label, + dfsan_label *ret_label) { + + // Execute concrete operation (simple check) + int ret = 0; + size_t prefix_len = strlen(prefix); + size_t str_len = strlen(str); + if (str_len >= prefix_len && memcmp(str, prefix, prefix_len) == 0) { + ret = 1; + } + + // Get unified labels (handles fsubstr chaining and content maps) + dfsan_label l1 = get_str_label(str, str_label); + dfsan_label l2 = get_str_label(prefix, prefix_label); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + } else { + // Determine length for memcmp_cache (use concrete side if one is fsubstr) + size_t n = strlen(str) + 1; + dfsan_label str_fsubstr = taint_get_str_content_label(str); + if (str_fsubstr != 0) + n = strlen(prefix) + 1; // use concrete side for length + + // Create label - fprefixof is commutative, dfsan_union will normalize + dfsan_label cmp = dfsan_union(l1, l2, __dfsan::fprefixof, n, + (uint64_t)str, (uint64_t)prefix); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; + } return ret; } +SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_suffixof( + const char *str, const char *suffix, + dfsan_label str_label, dfsan_label suffix_label, + dfsan_label *ret_label) { + + // Execute concrete operation + int ret = 0; + size_t suffix_len = strlen(suffix); + size_t str_len = strlen(str); + if (str_len >= suffix_len && + memcmp(str + (str_len - suffix_len), suffix, suffix_len) == 0) { + ret = 1; + } + + // Get unified labels + dfsan_label l1 = get_str_label(str, str_label); + dfsan_label l2 = get_str_label(suffix, suffix_label); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + } else { + // Determine length for memcmp_cache + size_t n = strlen(str) + 1; + dfsan_label str_fsubstr = taint_get_str_content_label(str); + if (str_fsubstr != 0) + n = strlen(suffix) + 1; + + // Create label + dfsan_label cmp = dfsan_union(l1, l2, __dfsan::fsuffixof, n, + (uint64_t)str, (uint64_t)suffix); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; + } + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strsub( + const char *s, size_t start, size_t len, + dfsan_label s_label, dfsan_label start_label, dfsan_label len_label, + dfsan_label *ret_label) { + + *ret_label = 0; + // Execute concrete operation + // Skip 'start' characters, then duplicate 'len' characters + if (s == NULL || len == 0) { + return NULL; + } + + size_t str_len = strlen(s); + if (start >= str_len) { + return NULL; + } + + // Point to start position + const char *src = s + start; + size_t remaining = str_len - start; + size_t copy_len = (len < remaining) ? len : remaining; + + // Allocate and copy substring (like strndup) + char *p = (char *)malloc(copy_len + 1); + if (p == NULL) { + return NULL; + } + dfsan_memcpy(p, src, copy_len); + p[copy_len] = '\0'; + + // Get unified label for the string + dfsan_label str_label = get_str_label(s, s_label); + + if (str_label == 0 && start_label == 0 && len_label == 0) { + // No taint, nothing to propagate + } else { + // Compose strsub(str, start, len) using two fsubstr operations: + // 1. suffix_from_pos(str, start) = str[start:] using fsubstr with op2=1 (suffix mode) + // 2. prefix(suffix, len) = suffix[0:len] using fsubstr with op2=0 (prefix mode) + + // Step 1: Create suffix label representing str[start:] + // l1 = string content, l2 = start position label + // op1 = concrete remaining length, op2 = 1 (suffix mode) + dfsan_label suffix_label = str_label; + if (start_label != 0 || start > 0) { + suffix_label = dfsan_union(str_label, start_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)remaining, 1); + } + + // Step 2: Take first len chars from suffix: suffix[0:len] + // l1 = suffix label, l2 = len label + // op1 = concrete len, op2 = 0 (prefix mode) + dfsan_label substr_label = dfsan_union(suffix_label, len_label, __dfsan::fsubstr, + sizeof(void*) * 8, (uint64_t)copy_len, 0); + + // Store label in content map so downstream ops can find it + if (substr_label != 0) { + taint_set_str_content_label(p, substr_label); + *ret_label = substr_label; + } + } + + return p; +} + SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_strcasecmp(const char *s1, const char *s2, dfsan_label s1_label, dfsan_label s2_label, dfsan_label *ret_label) { int ret = strcasecmp(s1, s2); // doing an optimistic solving, hoping we can get the same case - // check which one is tainted - //AOUT("strcasecmp: %s <=> %s\n", s1, s2); - *ret_label = __taint_strcmp(s1, s2); - return ret; -} + // Use unified get_str_label for fsubstr support + dfsan_label l1 = get_str_label(s1, s1_label); + dfsan_label l2 = get_str_label(s2, s2_label); -extern "C" SANITIZER_INTERFACE_ATTRIBUTE -dfsan_label __taint_strncmp(const char *s1, const char *s2, size_t n) { - if (n == 0) return 0; - if (dfsan_get_label(s1) == 0 && strlen(s1) < (n - 1)) - n = strlen(s1) + 1; - if (dfsan_get_label(s2) == 0 && strlen(s2) < (n - 1)) - n = strlen(s2) + 1; - dfsan_label l1 = dfsan_read_label(s1, n); - dfsan_label l2 = dfsan_read_label(s2, n); - // ugly hack ... - dfsan_label ret = dfsan_union(l1, l2, fmemcmp, n, (uint64_t)s1, (uint64_t)s2); - if (ret) __taint_trace_memcmp(ret); + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + } else { + size_t n = strlen(s1) + 1; + dfsan_label s1_fsubstr = taint_get_str_content_label(s1); + if (s1_fsubstr != 0) + n = strlen(s2) + 1; + + // fstrcmp is commutative - dfsan_union will swap to put concrete in op1 + dfsan_label cmp = dfsan_union(l1, l2, __dfsan::fstrcmp, n, + (uint64_t)s1, (uint64_t)s2); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; + } return ret; } @@ -351,8 +803,25 @@ SANITIZER_INTERFACE_ATTRIBUTE int __dfsw_strncmp(const char *s1, const char *s2, n, s1_label, s2_label, n_label); int ret = strncmp(s1, s2, n); - //AOUT("strncmp: %s <=> %s\n", s1, s2); - *ret_label = __taint_strncmp(s1, s2, n); + + // Use unified get_str_label for fsubstr support + dfsan_label l1 = get_str_label(s1, s1_label); + dfsan_label l2 = get_str_label(s2, s2_label); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + } else { + // Adjust n for shorter strings when one side is concrete + if (l1 == 0 && strlen(s1) < (n - 1)) + n = strlen(s1) + 1; + if (l2 == 0 && strlen(s2) < (n - 1)) + n = strlen(s2) + 1; + + dfsan_label cmp = dfsan_union(l1, l2, __dfsan::fstrcmp, n, + (uint64_t)s1, (uint64_t)s2); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; + } return ret; } @@ -364,40 +833,54 @@ __dfsw_strncasecmp(const char *s1, const char *s2, size_t n, *ret_label = 0; return 0; } - + int ret = strncasecmp(s1, s2, n); - // doing an optimistic solving here too, hoping the case can be the seame - //AOUT("strncmp: %s <=> %s\n", s1, s2); - *ret_label = __taint_strncmp(s1, s2, n); + // doing an optimistic solving here too, hoping the case can be the same + // Use unified get_str_label for fsubstr support + dfsan_label l1 = get_str_label(s1, s1_label); + dfsan_label l2 = get_str_label(s2, s2_label); + + if (l1 == 0 && l2 == 0) { + *ret_label = 0; + } else { + // Adjust n for shorter strings when one side is concrete + if (l1 == 0 && strlen(s1) < (n - 1)) + n = strlen(s1) + 1; + if (l2 == 0 && strlen(s2) < (n - 1)) + n = strlen(s2) + 1; + + dfsan_label cmp = dfsan_union(l1, l2, __dfsan::fstrcmp, n, + (uint64_t)s1, (uint64_t)s2); + if (cmp) __taint_trace_memcmp(cmp); + *ret_label = cmp; + } return ret; } SANITIZER_INTERFACE_ATTRIBUTE size_t __dfsw_strlen(const char *s, dfsan_label s_label, dfsan_label *ret_label) { size_t ret = strlen(s); - *ret_label = 0; - /* - if (flags().strict_data_dependencies) { + dfsan_label str_label = dfsan_read_label(s, ret + 1); + + if (str_label == 0) { *ret_label = 0; } else { - *ret_label = taint_read_label(s, ret + 1); - }*/ - return ret; -} - -static void *dfsan_memcpy(void *dest, const void *src, size_t n) { - if (n == 0) return dest; - dfsan_label *sdest = shadow_for(dest); - const dfsan_label *ssrc = shadow_for(src); - // FIXME: check and avoid copying labels? - internal_memcpy((void *)sdest, (const void *)ssrc, n * sizeof(dfsan_label)); - return internal_memcpy(dest, src, n); -} + // Check if the null terminator byte is from input (tainted) + // If not, it was added programmatically (e.g., by the program setting '\0') + dfsan_label null_label = dfsan_read_label(s + ret, 1); + bool null_from_input = (null_label != 0); -static void dfsan_memset(void *s, int c, dfsan_label c_label, size_t n) { - if (n == 0) return; - internal_memset(s, c, n); - dfsan_set_label(c_label, s, n); + // Create fstrlen label: + // - l1 = 0 (following fsize/fatoi pattern to avoid Alloca rejection) + // - l2 = str_label (content label for dependencies) + // - op1 = null_from_input flag (1 if null is from input, 0 if programmatic) + // - op2 = actual length (for solution generation) + // Note: str_label contains the offset info via Load labels + *ret_label = dfsan_union(0, str_label, fstrlen, + sizeof(size_t) * 8, + null_from_input ? 1 : 0, ret); + } + return ret; } SANITIZER_INTERFACE_ATTRIBUTE @@ -466,7 +949,99 @@ char *__dfsw_strcat(char *dest, const char *src, dfsan_label d_label, size_t dest_len = strlen(dest); size_t copy_len = strlen(src) + 1; // including tailing '\0' __taint_check_bounds(d_label, (uptr)dest, 0, dest_len + copy_len); + + // Get labels for both strings using unified label retrieval + dfsan_label dest_str_label = get_str_label(dest, d_label); + dfsan_label src_str_label = get_str_label(src, s_label); + + AOUT("strcat: dest=%p, src=%p, dest_label=%u, src_label=%u, " + "dest_str_label=%u, src_str_label=%u\n", + dest, src, d_label, s_label, dest_str_label, src_str_label); + + // Perform the actual strcat (copy src to dest + dest_len) dfsan_memcpy(dest + dest_len, src, copy_len); + + // If either string is tainted, create fstrcat label + if (dest_str_label != 0 || src_str_label != 0) { + // Create fstrcat: l1=dest, l2=src + // op1=dest pointer, op2=src pointer (for concrete content access) + // size = length of concrete operand (for memcmp_cache), 0 if both symbolic + size_t src_len = copy_len - 1; // excluding null + uint16_t concrete_len = 0; + if (dest_str_label == 0) { + concrete_len = (uint16_t)dest_len; + } else if (src_str_label == 0) { + concrete_len = (uint16_t)src_len; + } + dfsan_label concat_label = dfsan_union(dest_str_label, src_str_label, + __dfsan::fstrcat, + concrete_len, + (uint64_t)dest, + (uint64_t)src); + AOUT("strcat: created fstrcat label=%u\n", concat_label); + // Send concrete content through pipe if one side is concrete + if (concrete_len > 0) { + __taint_trace_memcmp(concat_label); + } + // Store in str_map so downstream ops can find it + taint_set_str_content_label(dest, concat_label); + } + + *ret_label = d_label; + return dest; +} + +SANITIZER_INTERFACE_ATTRIBUTE char * +__dfsw_strncat(char *dest, const char *src, size_t n, + dfsan_label d_label, dfsan_label s_label, dfsan_label n_label, + dfsan_label *ret_label) { + size_t dest_len = strlen(dest); + size_t src_len = strlen(src); + size_t copy_len = (n < src_len) ? n : src_len; // min(n, strlen(src)) + __taint_check_bounds(d_label, (uptr)dest, 0, dest_len + copy_len + 1); + + AOUT("strncat: dest=%p, src=%p, n=%zu, d_label=%u, s_label=%u, n_label=%u\n", + dest, src, n, d_label, s_label, n_label); + + // Get dest label using unified label retrieval + dfsan_label dest_str_label = get_str_label(dest, d_label); + + // Get src label - use get_str_label_n to handle symbolic n + // This will create fsubstr if n_label derives from a string op (e.g., strchr) + dfsan_label src_str_label = get_str_label_n(src, s_label, copy_len, n_label); + + AOUT("strncat: dest_str_label=%u, src_str_label=%u, copy_len=%zu\n", + dest_str_label, src_str_label, copy_len); + + // Perform the actual strncat + dfsan_memcpy(dest + dest_len, src, copy_len); + dest[dest_len + copy_len] = '\0'; + + // If either string is tainted, create fstrcat label + if (dest_str_label != 0 || src_str_label != 0) { + // Create fstrcat: l1=dest, l2=src + // op1=dest pointer, op2=src pointer (for concrete content access) + // size = length of concrete operand (for memcmp_cache), 0 if both symbolic + uint16_t concrete_len = 0; + if (dest_str_label == 0) { + concrete_len = (uint16_t)dest_len; + } else if (src_str_label == 0) { + concrete_len = (uint16_t)copy_len; + } + dfsan_label concat_label = dfsan_union(dest_str_label, src_str_label, + __dfsan::fstrcat, + concrete_len, + (uint64_t)dest, + (uint64_t)src); + AOUT("strncat: created fstrcat label=%u\n", concat_label); + // Send concrete content through pipe if one side is concrete + if (concrete_len > 0) { + __taint_trace_memcmp(concat_label); + } + // Store in content map so downstream ops can find it + taint_set_str_content_label(dest, concat_label); + } + *ret_label = d_label; return dest; } @@ -475,7 +1050,40 @@ SANITIZER_INTERFACE_ATTRIBUTE char * __dfsw_strdup(const char *s, dfsan_label s_label, dfsan_label *ret_label) { size_t len = strlen(s); void *p = malloc(len+1); + if (p == nullptr) { + *ret_label = 0; + return nullptr; + } dfsan_memcpy(p, s, len+1); + + // Propagate string label to duplicated string + dfsan_label str_label = get_str_label(s, s_label); + if (str_label != 0) { + taint_set_str_content_label(static_cast(p), str_label); + } + + *ret_label = 0; + return static_cast(p); +} + +SANITIZER_INTERFACE_ATTRIBUTE char * +__dfsw_strndup(const char *s, size_t n, dfsan_label s_label, + dfsan_label n_label, dfsan_label *ret_label) { + size_t len = strnlen(s, n); + void *p = malloc(len + 1); + if (p == nullptr) { + *ret_label = 0; + return nullptr; + } + dfsan_memcpy(p, s, len); + ((char *)p)[len] = '\0'; + + // Propagate string label to duplicated string + dfsan_label str_label = get_str_label_n(s, s_label, len, n_label); + if (str_label != 0) { + taint_set_str_content_label(static_cast(p), str_label); + } + *ret_label = 0; return static_cast(p); } @@ -484,7 +1092,18 @@ SANITIZER_INTERFACE_ATTRIBUTE char * __dfsw___strdup(const char *s, dfsan_label s_label, dfsan_label *ret_label) { size_t len = strlen(s); void *p = malloc(len+1); + if (p == nullptr) { + *ret_label = 0; + return nullptr; + } dfsan_memcpy(p, s, len+1); + + // Propagate string label to duplicated string + dfsan_label str_label = get_str_label(s, s_label); + if (str_label != 0) { + taint_set_str_content_label(static_cast(p), str_label); + } + *ret_label = 0; return static_cast(p); } @@ -492,8 +1111,7 @@ __dfsw___strdup(const char *s, dfsan_label s_label, dfsan_label *ret_label) { SANITIZER_INTERFACE_ATTRIBUTE char * __dfsw___strndup(const char *s, size_t n, dfsan_label s_label, dfsan_label n_label, dfsan_label *ret_label) { - size_t len = strlen(s); - len = len > n ? n : len; + size_t len = strnlen(s, n); char *p = static_cast(malloc(len+1)); if (p == nullptr) { *ret_label = 0; @@ -501,7 +1119,13 @@ __dfsw___strndup(const char *s, size_t n, dfsan_label s_label, } dfsan_memcpy(p, s, len); // copy at most n bytes p[len] = '\0'; - dfsan_set_label(0, p + len, 1); + + // Propagate string label to duplicated string + dfsan_label str_label = get_str_label_n(s, s_label, len, n_label); + if (str_label != 0) { + taint_set_str_content_label(static_cast(p), str_label); + } + *ret_label = 0; return p; } @@ -511,15 +1135,65 @@ __dfsw_strncpy(char *s1, const char *s2, size_t n, dfsan_label s1_label, dfsan_label s2_label, dfsan_label n_label, dfsan_label *ret_label) { size_t len = strlen(s2); + size_t copy_len = len < n ? len : n; + if (n_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; + bool created_fsubstr = false; + + if (str_op_label != 0) { + // Get the content label from the string op + dfsan_label_info *str_op_info = dfsan_get_label_info(str_op_label); + dfsan_label str_op_content = str_op_info->l1; // content from strchr + + // Verify buffers match: str_op searched the same buffer we're copying + // When copy_len = 0, we can't read from s2, so trust str_op_content + bool buffers_match = false; + if (str_op_content >= CONST_OFFSET) { + 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); + buffers_match = (src_base != 0 && src_base == str_op_base); + } + } else { + // copy_len = 0: trust the str_op_content (empty substring case) + buffers_match = true; + } + } + + if (buffers_match) { + // Create fsubstr: represents substr(src, 0, len) where len is symbolic + // Use str_op_content (full haystack) for proper string theory solving + dfsan_label substr_label = dfsan_union(str_op_content, str_op_label, + __dfsan::fsubstr, + sizeof(void*) * 8, + (uint64_t)n, 0); + + // Store fsubstr label in runtime map keyed by destination address + // This survives buffer content being overwritten (e.g., key[len] = '\0') + taint_set_str_content_label(s1, substr_label); + + *ret_label = s1_label; + } + } + + // Normal case: copy byte-by-byte labels if (len < n) { - dfsan_memcpy(s1, s2, len+1); - dfsan_memset(s1+len+1, 0, 0, n-len-1); + dfsan_memcpy(s1, s2, len + 1); } else { dfsan_memcpy(s1, s2, n); } + // Handle padding (strncpy pads with zeros if len < n) + if (len < n) { + dfsan_memset(s1 + len + 1, 0, 0, n - len - 1); + } + *ret_label = s1_label; return s1; } @@ -888,11 +1562,23 @@ char *__dfsw_strcpy(char *dest, const char *src, dfsan_label dst_label, size_t len = strlen(src) + 1; __taint_check_bounds(dst_label, (uptr)dest, 0, len); char *ret = strcpy(dest, src); + *ret_label = dst_label; + + // Use get_str_label to properly get the source label + // This handles str_map, pointer label string ops, and buffer content + dfsan_label real_src_label = get_str_label(src, src_label); + AOUT("strcpy: src='%p', src_label=%d, real_src_label=%d\n", src, src_label, real_src_label); + + if (real_src_label != 0) { + // Store the label in runtime map keyed by destination address + taint_set_str_content_label(dest, real_src_label); + *ret_label = real_src_label; + } + if (ret) { internal_memcpy(shadow_for(dest), shadow_for(src), sizeof(dfsan_label) * len); } - *ret_label = dst_label; return ret; } @@ -1182,7 +1868,36 @@ SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memchr(void *s, int c, size_t n, dfsan_label n_label, dfsan_label *ret_label) { void *ret = memchr(s, c, n); - *ret_label = ret ? s_label : 0; + + // Use unified get_str_label_n for source label + // Pass n_label to handle fsubstr creation when n derives from a string op + dfsan_label src_label = get_str_label_n(s, s_label, n, n_label); + + if (src_label != 0 || c_label != 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) + // 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, + content_len, + (uint64_t)s, (uint64_t)(uint8_t)c); + + // Send concrete content if haystack is concrete + if (content_len > 0 && *ret_label) { + __taint_trace_memcmp(*ret_label); + } + + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); + } + } else { + *ret_label = 0; + } return ret; } @@ -1191,7 +1906,76 @@ SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strrchr(char *s, int c, dfsan_label c_label, dfsan_label *ret_label) { char *ret = strrchr(s, c); - *ret_label = ret ? s_label : 0; + + // Use unified get_str_label for source label + dfsan_label src_label = get_str_label(s, s_label); + + if (src_label != 0 || c_label != 0) { + // Determine which operand is concrete and set size accordingly + size_t haystack_len = strlen(s); + uint16_t content_len = (src_label == 0) ? (uint16_t)haystack_len : 0; + + // l1 = src_label (source - for chaining or content dependencies) + // l2 = c_label (target char - may be symbolic!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = char value + // size = haystack length if concrete, else 0 + *ret_label = dfsan_union(src_label, c_label, __dfsan::fstrrchr, + content_len, + (uint64_t)s, + (uint64_t)(uint8_t)c); + + // Send concrete haystack content if haystack is concrete + if (content_len > 0 && *ret_label) { + __taint_trace_memcmp(*ret_label); + } + + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); + } + } else { + *ret_label = 0; + } + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memrchr(const void *s, int c, size_t n, + dfsan_label s_label, + dfsan_label c_label, + dfsan_label n_label, + dfsan_label *ret_label) { + void *ret = const_cast(memrchr(s, c, n)); + + // Use unified get_str_label_n for source label + // Pass n_label to handle fsubstr creation when n derives from a string op + dfsan_label src_label = get_str_label_n(s, s_label, n, n_label); + + if (src_label != 0 || c_label != 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) + // 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, + content_len, + (uint64_t)s, (uint64_t)(uint8_t)c); + + // Send concrete content if haystack is concrete + if (content_len > 0 && *ret_label) { + __taint_trace_memcmp(*ret_label); + } + + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); + } + } else { + *ret_label = 0; + } return ret; } @@ -1200,7 +1984,165 @@ SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strstr(char *haystack, char *needle, dfsan_label needle_label, dfsan_label *ret_label) { char *ret = strstr(haystack, needle); - *ret_label = ret ? haystack_label : 0; + + // Use unified get_str_label for haystack and needle + dfsan_label src_label = get_str_label(haystack, haystack_label); + dfsan_label real_needle_label = get_str_label(needle, needle_label); + + if (src_label != 0 || real_needle_label != 0) { + // Determine which operand is concrete and set size accordingly + size_t haystack_len = strlen(haystack); + size_t needle_len = strlen(needle); + uint16_t content_len = 0; + if (src_label == 0) { + content_len = (uint16_t)haystack_len; + } else if (real_needle_label == 0) { + content_len = (uint16_t)needle_len; + } + + // l1 = src_label (source - for chaining or content dependencies) + // l2 = real_needle_label (may be symbolic string!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = needle pointer (for concrete content retrieval) + // size = haystack length if haystack concrete, else needle length if needle concrete, else 0 + dfsan_label label = dfsan_union(src_label, real_needle_label, __dfsan::fstrstr, + content_len, + (uint64_t)haystack, + (uint64_t)needle); + + // Send concrete content (haystack or needle) + if (content_len > 0 && label) { + __taint_trace_memcmp(label); + } + + *ret_label = label; + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); + } + } else { + *ret_label = 0; + } + return ret; +} + +// strnstr implementation (BSD function not available on Linux) +static char *strnstr_impl(const char *haystack, const char *needle, size_t len) { + size_t needle_len = strlen(needle); + if (needle_len == 0) + return (char *)haystack; + + if (len == 0) + return NULL; + + for (size_t i = 0; i < len && haystack[i]; i++) { + if (i + needle_len > len) + break; + if (strncmp(haystack + i, needle, needle_len) == 0) + return (char *)(haystack + i); + } + return NULL; +} + +SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strnstr(char *haystack, char *needle, + size_t len, + dfsan_label haystack_label, + dfsan_label needle_label, + dfsan_label len_label, + dfsan_label *ret_label) { + char *ret = strnstr_impl(haystack, needle, len); + + // Use unified get_str_label_n for haystack (respects length parameter) + dfsan_label src_label = get_str_label_n(haystack, haystack_label, + strnlen(haystack, len), len_label); + // Use unified get_str_label for needle + dfsan_label real_needle_label = get_str_label(needle, needle_label); + + if (src_label != 0 || real_needle_label != 0) { + // Determine which operand is concrete and set size accordingly + size_t haystack_len = strnlen(haystack, len); + size_t needle_len = strlen(needle); + uint16_t content_len = 0; + if (src_label == 0) { + content_len = (uint16_t)haystack_len; + } else if (real_needle_label == 0) { + content_len = (uint16_t)needle_len; + } + + // l1 = src_label (source - for chaining or content dependencies) + // l2 = real_needle_label (may be symbolic string!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = needle pointer (for concrete content retrieval) + // size = haystack length if haystack concrete, else needle length if needle concrete, else 0 + dfsan_label label = dfsan_union(src_label, real_needle_label, __dfsan::fstrstr, + content_len, + (uint64_t)haystack, + (uint64_t)needle); + + // Send concrete content (haystack or needle) + if (content_len > 0 && label) { + __taint_trace_memcmp(label); + } + + *ret_label = label; + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); + } + } else { + *ret_label = 0; + } + return ret; +} + +SANITIZER_INTERFACE_ATTRIBUTE void *__dfsw_memmem(const void *haystack, size_t haystacklen, + const void *needle, size_t needlelen, + dfsan_label haystack_label, + dfsan_label haystacklen_label, + dfsan_label needle_label, + dfsan_label needlelen_label, + dfsan_label *ret_label) { + void *ret = memmem(haystack, haystacklen, needle, needlelen); + + // Use unified get_str_label_n for haystack and needle + // Pass haystacklen_label to handle fsubstr creation when haystacklen derives from a string op + dfsan_label src_label = + get_str_label_n(haystack, haystack_label, haystacklen, haystacklen_label); + + // Use unified get_str_label_n for needle + dfsan_label real_needle_label = + get_str_label_n(needle, needle_label, needlelen, needlelen_label); + + if (src_label != 0 || real_needle_label != 0) { + // Determine which operand is concrete and set size accordingly + uint16_t content_len = 0; + if (src_label == 0) { + content_len = (uint16_t)haystacklen; + } else if (real_needle_label == 0) { + content_len = (uint16_t)needlelen; + } + + // l1 = src_label (haystack content) + // l2 = real_needle_label (needle content - may be symbolic!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = needle pointer (for concrete content retrieval) + // size = haystack length if haystack concrete, else needle length if needle concrete, else 0 + dfsan_label label = dfsan_union(src_label, real_needle_label, __dfsan::fstrstr, + content_len, + (uint64_t)haystack, (uint64_t)needle); + + // Send concrete content (haystack or needle) + if (content_len > 0 && label) { + __taint_trace_memcmp(label); + } + *ret_label = label; + // Store the result pointer to recover symbolic length + if (ret) { + taint_set_str_indexof_label(ret, *ret_label); + } + } else { + *ret_label = 0; + } return ret; } diff --git a/runtime/dfsan/dfsan_flags.inc b/runtime/dfsan/dfsan_flags.inc index 794c9df2..b39c47fa 100644 --- a/runtime/dfsan/dfsan_flags.inc +++ b/runtime/dfsan/dfsan_flags.inc @@ -45,3 +45,5 @@ DFSAN_FLAG(const char *, output_dir, ".", "The path for output file.") DFSAN_FLAG(int, instance_id, 0, "instance id for multi-instance fuzzing.") DFSAN_FLAG(int, session_id, 0, "session/round id.") DFSAN_FLAG(bool, force_stdin, false, "force tainting stdin.") +DFSAN_FLAG(bool, enum_gep, false, "enable GEP index enumeration.") +DFSAN_FLAG(int, string_map_capacity, 256, "initial capacity for string label maps.") diff --git a/runtime/dfsan/done_abilist.txt b/runtime/dfsan/done_abilist.txt index a2538bff..c454b3b3 100644 --- a/runtime/dfsan/done_abilist.txt +++ b/runtime/dfsan/done_abilist.txt @@ -284,7 +284,11 @@ fun:stpcpy=custom fun:strcat=custom fun:strcpy=custom fun:strdup=custom +fun:strncat=custom fun:strncpy=custom +fun:strndup=custom + +# transformation (fatoi) fun:strtod=custom fun:strtol=custom fun:strtoll=custom @@ -301,6 +305,7 @@ fun:toupper=custom fun:bcmp=custom fun:memchr=custom fun:memcmp=custom +fun:memrchr=custom fun:strcasecmp=custom fun:strchr=custom fun:strcmp=custom @@ -310,6 +315,10 @@ fun:strncmp=custom fun:strpbrk=custom fun:strrchr=custom fun:strstr=custom +fun:strnstr=custom +# not standard Linux +fun:strnstr=uninstrumented +fun:memmem=custom ## from afl++ # memcmp-like @@ -318,7 +327,7 @@ fun:OPENSSL_memcmp=memcmp fun:memcmp_const_time=memcmp fun:memcmpct=memcmp -# strcmp-like +# strcmp-like (fstrcmp) fun:xmlStrcmp=strcmp fun:xmlStrEqual=strcmp fun:g_strcmp0=strcmp @@ -333,15 +342,8 @@ fun:g_ascii_strcasecmp=strcmp fun:Curl_strcasecompare=strcmp fun:Curl_safe_strcasecompare=strcmp fun:cmsstrcasecmp=strcmp -# FIXME: strstr?? -fun:g_strstr_len=strcmp -fun:ap_strcasestr=strcmp -fun:xmlStrstr=strcmp -fun:xmlStrcasestr=strcmp -fun:g_str_has_prefix=strcmp -fun:g_str_has_suffix=strcmp - -# strncmp-like + +# strncmp-like (fstrcmp) fun:xmlStrncmp=strncmp fun:curl_strnequal=strncmp fun:strnicmp=strncmp @@ -352,6 +354,26 @@ fun:g_ascii_strncasecmp=strcmp fun:Curl_strncasecompare=strncmp fun:g_strncasecmp=strncmp +# fstrchr +fun:xmlStrchr=strchr + +# fstrrchr + +# strstr (fstrstr) +fun:g_strstr_len=strstr +fun:ap_strcasestr=strstr +fun:xmlStrstr=strstr +fun:xmlStrcasestr=strstr + +# fprefixof +fun:g_str_has_prefix=prefixof + +# fsuffixof +fun:g_str_has_suffix=suffixof + +# fsubstr +fun:xmlStrsub=substr + # Functions which take action based on global state, such as running a callback # set by a separate function. fun:write=custom diff --git a/solvers/CMakeLists.txt b/solvers/CMakeLists.txt index 4c4409c4..0134e1ae 100644 --- a/solvers/CMakeLists.txt +++ b/solvers/CMakeLists.txt @@ -44,7 +44,7 @@ target_include_directories(rgd-solver PRIVATE target_link_libraries(rgd-solver PRIVATE tcmalloc - z3 + ${Z3_LIBRARY} jigsaw profiler ) diff --git a/solvers/jigsaw/CMakeLists.txt b/solvers/jigsaw/CMakeLists.txt index 52951c10..8b49a7ff 100644 --- a/solvers/jigsaw/CMakeLists.txt +++ b/solvers/jigsaw/CMakeLists.txt @@ -1,5 +1,3 @@ -cmake_minimum_required(VERSION 3.5.1) - project(jigsaw CXX) set(CMAKE_CXX_STANDARD 17) diff --git a/solvers/z3-ts.cpp b/solvers/z3-ts.cpp index 59f84ef0..2b9f9e15 100644 --- a/solvers/z3-ts.cpp +++ b/solvers/z3-ts.cpp @@ -2,6 +2,7 @@ #include "parse-z3.h" +#include #include #include #include @@ -43,6 +44,16 @@ static const std::unordered_map OP_MAP { {RELATIONAL_ICMP(__dfsan::bvslt), "Slt"}, {RELATIONAL_ICMP(__dfsan::bvsle), "Sle"}, #undef RELATIONAL_ICMP + // higher-order string ops + {__dfsan::fstrchr, "strchr"}, + {__dfsan::fstrrchr, "strrchr"}, + {__dfsan::fstrstr, "strstr"}, + {__dfsan::fstrpbrk, "strpbrk"}, + {__dfsan::fstr_off, "stroff"}, + {__dfsan::fsubstr, "substr"}, + {__dfsan::fstrcat, "strcat"}, + {__dfsan::fprefixof, "prefixof"}, + {__dfsan::fsuffixof, "suffixof"}, }; static std::string get_op_name(uint32_t op) { @@ -53,6 +64,61 @@ static std::string get_op_name(uint32_t op) { return std::to_string(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; +} + +// Helper function to check if label tree contains indexOf operations +// (used to skip validation since op1 is repurposed for haystack pointer) +bool Z3AstParser::label_contains_indexof(dfsan_label label) { + if (label < CONST_OFFSET) return false; + + dfsan_label_info *info = get_label_info(label); + if (is_indexof_op(info->op)) return true; + + // Recursively check dependencies + if (info->l1 >= CONST_OFFSET && label_contains_indexof(info->l1)) return true; + if (info->l2 >= CONST_OFFSET && label_contains_indexof(info->l2)) return true; + + return false; +} + +// Decode Z3's escaped string format (e.g., "\u{1}\u{2}" -> bytes 0x01, 0x02) +static std::vector decode_z3_string(const std::string &str) { + std::vector result; + size_t i = 0; + while (i < str.size()) { + if (i + 3 < str.size() && str[i] == '\\' && str[i+1] == 'u' && str[i+2] == '{') { + // Parse \u{XXXX} escape sequence + size_t end = str.find('}', i + 3); + if (end != std::string::npos) { + std::string hex_str = str.substr(i + 3, end - (i + 3)); + uint32_t code_point = std::stoul(hex_str, nullptr, 16); + // For simplicity, assume code points fit in a byte (for ASCII/Latin-1) + result.push_back((uint8_t)(code_point & 0xFF)); + i = end + 1; + continue; + } + } + // Regular character + result.push_back((uint8_t)str[i]); + i++; + } + return result; +} + void Z3AstParser::dump_value_cache(dfsan_label label) { if (label >= value_cache_.size()) { throw z3::exception("invalid label for value cache"); @@ -72,13 +138,15 @@ void Z3AstParser::dump_value_cache(dfsan_label label) { Z3AstParser::Z3AstParser(void *base, size_t size, z3::context &context) : ASTParser(base, size), context_(context) { input_name_format = "input-%u-%u"; - atoi_name_format = "atoi-%u-%u-%d"; + 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 } int Z3AstParser::restart(std::vector &inputs) { // reset caches memcmp_cache_.clear(); + string_ranges_.clear(); tsize_cache_.clear(); tsize_cache_.resize(1); // reserve for CONST_OFFSET for (Z3_ast ast : expr_cache_) { @@ -94,6 +162,7 @@ int Z3AstParser::restart(std::vector &inputs) { value_cache_.clear(); value_cache_.resize(1); // reserve for CONST_OFFSET #endif + string_info_cache_.clear(); branch_deps_.clear(); branch_deps_.resize(inputs.size()); @@ -294,7 +363,29 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { cache_expr(l, e); RECORD_VALUE(value_cache_[info->l1]); continue; - } //FIXME: other casting ops (PtrToInt, BitCast)? + } else if (info->op == __dfsan::PtrToInt) { + // PtrToInt converts a pointer to integer + // If the source is a string op result, convert the index to bitvector + 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 + z3::expr idx = get_cached_expr(info->l1, input_deps); + z3::expr bv_idx = z3::int2bv(info->size, idx); + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, bv_idx); + RECORD_VALUE(value_cache_[info->l1]); + continue; + } + } + // For other PtrToInt cases, pass through (shouldn't normally reach here) + z3::expr e = get_cached_expr(info->l1, input_deps); + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, e); + RECORD_VALUE(value_cache_[info->l1]); + continue; + } //FIXME: other casting ops (BitCast)? // symsan-defined else if (info->op == __dfsan::Extract) { z3::expr base = get_cached_expr(info->l1, input_deps); @@ -365,15 +456,681 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { uint32_t offset = get_label_info(src->l1)->op1.i; // legacy: offset in op1 uint32_t input = get_label_info(src->l1)->op2.i; int base = info->op1.i; + uint64_t orig_len = info->op2.i; // FIXME: dependencies? tsize_cache_.emplace_back(1); // XXX: hacky, avoid string theory - snprintf(name, sizeof(name), atoi_name_format, input, offset, base); + snprintf(name, sizeof(name), atoi_name_format, input, offset, base, orig_len); z3::symbol symbol = context_.str_symbol(name); z3::sort sort = context_.bv_sort(info->size); cache_expr(l, context_.constant(symbol, sort)); RECORD_VALUE(0); // FIXME: map to atoi result? continue; + } else if (info->op == __dfsan::fstrlen) { + // Symbolic string length + // - l1 = 0 (following fsize/fatoi pattern) + // - l2 = content label (for input dependencies) + // - op1 = null_from_input flag (1 if null terminator is from input, 0 if programmatic) + // - op2 = actual length + + // Extract offset and input_id from content label (l2) + uint32_t offset = 0; + uint32_t input_id = 0; + uint32_t null_from_input = info->op1.i; + + if (info->l2 >= CONST_OFFSET) { + // Walk the content label to find base input offset + dfsan_label_info *str_info = get_label_info(info->l2); + + // Handle Concat chain (common for multi-byte strings) + while (str_info->op == __dfsan::Concat && str_info->l1 >= CONST_OFFSET) { + str_info = get_label_info(str_info->l1); + } + + // Base input labels have op=0, offset in op1 + // (created by dfsan_create_label, not dfsan_union) + if (str_info->op == 0) { + // Direct input byte - offset stored in op1 + offset = str_info->op1.i; + input_id = 0; // default input + } else if (str_info->op == __dfsan::Load) { + // Load from memory - get offset from pointer label + dfsan_label_info *ptr_info = get_label_info(str_info->l1); + offset = ptr_info->op1.i; + input_id = ptr_info->op2.i; + } + } + + tsize_cache_.emplace_back(1); + // Create symbolic variable: strlen-input-offset-origlen-null_from_input + snprintf(name, sizeof(name), strlen_name_format, input_id, offset, + info->op2.i, null_from_input); + z3::symbol symbol = context_.str_symbol(name); + z3::sort sort = context_.bv_sort(info->size); + cache_expr(l, context_.constant(symbol, sort)); + RECORD_VALUE(info->op2.i); // actual length for value cache + 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) + // l2 = c_label (target character - may be symbolic!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = char value + // size = haystack length if haystack concrete, else 0 + + // Build source string from l1 (content label) + z3::expr haystack_str = context_.string_val(""); + z3::expr start_offset = context_.int_val(0); + + dfsan_label haystack_label = info->l1; + dfsan_label concrete_label = l; // Track which label sent the concrete content + if (haystack_label >= CONST_OFFSET) { + // Symbolic haystack + dfsan_label_info *src_info = get_label_info(haystack_label); + + if (is_content_string_op(src_info->op)) { + // l1 is a fsubstr/strcat - use the cached substr expression directly + haystack_str = get_cached_expr(haystack_label, input_deps); + } else if (is_indexof_op(src_info->op)) { + if (src_info->op == __dfsan::fstr_off) { + // Chained call via pointer arithmetic: strchr(t1 + N, c) + // Use build_string_from_label which handles fstr_off specially + // (creates insertion point if beyond end, or suffix if within bounds) + haystack_label = src_info->l1; + // start_offset stays 0 since we're searching from the start of the suffix/insertion point + } else { + // Chained call: search starts after previous match + z3::expr prev_idx = get_cached_expr(info->l1, input_deps); + start_offset = prev_idx + 1; + // Walk back to find original haystack content + haystack_label = info->l1; + dfsan_label_info *chain_info = src_info; + while (is_indexof_op(chain_info->op)) { + concrete_label = haystack_label; // Save before updating + haystack_label = chain_info->l1; + if (haystack_label < CONST_OFFSET) break; + chain_info = get_label_info(haystack_label); + } + } + // Build string from original haystack label + if (haystack_label >= CONST_OFFSET) { + haystack_str = build_string_from_label(haystack_label, input_deps); + } + } else { + // Build string from byte content (Load, Concat, or single byte) + haystack_str = build_string_from_label(haystack_label, input_deps); + } + } + + if (haystack_label < CONST_OFFSET) { + // Concrete haystack - retrieve from memcmp_cache using concrete_label + auto it = memcmp_cache_.find(concrete_label); + if (it != memcmp_cache_.end()) { + dfsan_label_info *concrete_info = get_label_info(concrete_label); + std::string haystack(reinterpret_cast(it->second.get()), concrete_info->size); + haystack_str = context_.string_val(haystack); + } else { + throw z3::exception("cannot find haystack content for strchr"); + } + } + + // Get target character (concrete or symbolic) + // Use z3::unit to create single-char string from integer code point + z3::expr code(context_); + if (info->l2 == 0) { + // Concrete character + uint8_t c = (uint8_t)info->op2.i; + code = context_.int_val(c); + } else { + // Symbolic character - convert bitvector to int + z3::expr c_expr = get_cached_expr(info->l2, input_deps); + if (c_expr.get_sort().bv_size() != 8) { + c_expr = c_expr.extract(7, 0); + } + code = z3::bv2int(c_expr, false); + } + // 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); + + tsize_cache_.emplace_back(1); + cache_expr(l, idx); // cache the index expression (Int sort) + RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops + continue; + } else if (info->op == __dfsan::fstrrchr) { + // strrchr/memrchr: find LAST occurrence of character + // l1 = source pointer label (content bytes or fsubstr) + // l2 = c_label (target character - may be symbolic!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = char value + // size = haystack length if haystack concrete, else 0 + + // Build source string from l1 (content label or fsubstr) + z3::expr haystack_str = context_.string_val(""); + if (info->l1 >= CONST_OFFSET) { + // Symbolic haystack + dfsan_label_info *src_info = get_label_info(info->l1); + if (is_content_string_op(src_info->op)) { + // l1 is a fsubstr/strcat - use the cached substr expression directly + haystack_str = get_cached_expr(info->l1, input_deps); + } else { + haystack_str = build_string_from_label(info->l1, input_deps); + } + } else { + // Concrete haystack - retrieve from memcmp_cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + // Use info->size for haystack length (set in runtime) + std::string haystack(reinterpret_cast(it->second.get()), info->size); + haystack_str = context_.string_val(haystack); + } else { + throw z3::exception("cannot find haystack content for strrchr"); + } + } + + // Get target character (concrete or symbolic) + // Use z3::unit to create single-char string from integer code point + z3::expr code(context_); + if (info->l2 == 0) { + // Concrete character + uint8_t c = (uint8_t)info->op2.i; + code = context_.int_val(c); + } else { + // Symbolic character - convert bitvector to int + z3::expr c_expr = get_cached_expr(info->l2, input_deps); + if (c_expr.get_sort().bv_size() != 8) { + c_expr = c_expr.extract(7, 0); + } + code = z3::bv2int(c_expr, false); + } + // 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)); + + // For reverse search, find the last occurrence + z3::expr idx = z3::last_indexof(haystack_str, target_str); + + tsize_cache_.emplace_back(1); + cache_expr(l, idx); + RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops + continue; + } else if (info->op == __dfsan::fstrstr) { + // strstr: find substring + // l1 = haystack content label (for chaining or byte content) + // l2 = needle_label (may be symbolic!) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = needle pointer (for concrete content retrieval) + // size = haystack length if haystack concrete, else needle length if needle concrete, else 0 + + // Build haystack string from l1 + z3::expr haystack_str = context_.string_val(""); + z3::expr start_offset = context_.int_val(0); + + dfsan_label haystack_label = info->l1; + dfsan_label concrete_label = l; // Track which label sent the concrete content + if (haystack_label >= CONST_OFFSET) { + // Symbolic haystack + dfsan_label_info *src_info = get_label_info(haystack_label); + + if (is_content_string_op(src_info->op)) { + // l1 is a fsubstr/strcat - use the cached substr expression directly + haystack_str = get_cached_expr(haystack_label, input_deps); + } else if (is_indexof_op(src_info->op)) { + if (src_info->op == __dfsan::fstr_off) { + // Chained call via pointer arithmetic + haystack_label = src_info->l1; + } else { + // Chained call: search starts after previous match + z3::expr prev_idx = get_cached_expr(info->l1, input_deps); + start_offset = prev_idx + 1; + // Walk back to find original haystack content + haystack_label = info->l1; + dfsan_label_info *chain_info = src_info; + while (is_indexof_op(chain_info->op)) { + concrete_label = haystack_label; // Save before updating + haystack_label = chain_info->l1; + if (haystack_label < CONST_OFFSET) break; + chain_info = get_label_info(haystack_label); + } + } + // Build string from original haystack label + if (haystack_label >= CONST_OFFSET) { + haystack_str = build_string_from_label(haystack_label, input_deps); + } + } else { + // Build string from byte content + haystack_str = build_string_from_label(haystack_label, input_deps); + } + } + + if (haystack_label < CONST_OFFSET) { + // Concrete haystack - retrieve from memcmp_cache using concrete_label + auto it = memcmp_cache_.find(concrete_label); + if (it != memcmp_cache_.end()) { + dfsan_label_info *concrete_info = get_label_info(concrete_label); + std::string haystack(reinterpret_cast(it->second.get()), concrete_info->size); + haystack_str = context_.string_val(haystack); + } else { + throw z3::exception("cannot find haystack content for strstr"); + } + } + + // Get needle (concrete or symbolic) + z3::expr needle_str(context_); + if (info->l2 == 0) { + // Concrete needle - get from cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + // Build string from cached bytes using info->size for length + std::string needle(reinterpret_cast(it->second.get()), info->size); + needle_str = context_.string_val(needle); + } else { + throw z3::exception("cannot find concrete needle content"); + } + } else { + // Symbolic needle - build string from l2 (Load of tainted buffer) + needle_str = build_string_from_label(info->l2, input_deps); + } + + z3::expr idx = z3::indexof(haystack_str, needle_str, start_offset); + + tsize_cache_.emplace_back(1); + cache_expr(l, idx); + RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops + continue; + } else if (info->op == __dfsan::fstrpbrk) { + // strpbrk: find first character from accept set + // l1 = source content label + // l2 = accept_label (may be symbolic) + // op1 = haystack pointer (for concrete content retrieval) + // op2 = accept pointer (for concrete content retrieval) + // size = haystack length if haystack concrete, else accept length if accept concrete, else 0 + + // Build source string from l1 + z3::expr haystack_str = context_.string_val(""); + z3::expr start_offset = context_.int_val(0); + + dfsan_label haystack_label = info->l1; + dfsan_label concrete_label = l; // Track which label sent the concrete content + if (haystack_label >= CONST_OFFSET) { + // Symbolic haystack + dfsan_label_info *src_info = get_label_info(haystack_label); + + if (is_content_string_op(src_info->op)) { + haystack_str = get_cached_expr(haystack_label, input_deps); + } else if (is_indexof_op(src_info->op)) { + if (src_info->op == __dfsan::fstr_off) { + // Chained call via pointer arithmetic + haystack_label = src_info->l1; + } else { + // Chained call + z3::expr prev_idx = get_cached_expr(info->l1, input_deps); + start_offset = prev_idx + 1; + haystack_label = info->l1; + dfsan_label_info *chain_info = src_info; + while (is_indexof_op(chain_info->op)) { + concrete_label = haystack_label; // Save before updating + haystack_label = chain_info->l1; + if (haystack_label < CONST_OFFSET) break; + chain_info = get_label_info(haystack_label); + } + } + // Build string from original haystack label + if (haystack_label >= CONST_OFFSET) { + haystack_str = build_string_from_label(haystack_label, input_deps); + } + } else { + haystack_str = build_string_from_label(haystack_label, input_deps); + } + } + + if (haystack_label < CONST_OFFSET) { + // Concrete haystack - retrieve from memcmp_cache using concrete_label + auto it = memcmp_cache_.find(concrete_label); + if (it != memcmp_cache_.end()) { + dfsan_label_info *concrete_info = get_label_info(concrete_label); + std::string haystack(reinterpret_cast(it->second.get()), concrete_info->size); + haystack_str = context_.string_val(haystack); + } else { + throw z3::exception("cannot find haystack content for strpbrk"); + } + } + + // Get accept character set + z3::expr idx(context_); + if (info->l2 == 0) { + // Concrete accept set - get from cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end() && info->size > 0) { + // Simplified approach: use first character's index as representative + // and add constraint that any character could be found + // This works well for NULL checks (if (strpbrk(s, accept))) + uint8_t first_c = it->second.get()[0]; + z3::expr code = context_.int_val(first_c); + z3::expr char_str(context_, Z3_mk_string_from_code(context_, code)); + idx = z3::indexof(haystack_str, char_str, start_offset); + } else { + throw z3::exception("cannot find concrete accept content"); + } + } else { + // Symbolic accept set - build string from label + z3::expr accept_str = build_string_from_label(info->l2, input_deps); + // Get the length of accept string + z3::expr accept_len(context_, Z3_mk_seq_length(context_, accept_str)); + // strpbrk returns NULL if accept is empty, so: if (len > 0) indexOf else -1 + z3::expr first_char_str(context_, Z3_mk_seq_extract(context_, accept_str, context_.int_val(0), context_.int_val(1))); + z3::expr idx_if_nonempty = z3::indexof(haystack_str, first_char_str, start_offset); + idx = z3::ite(accept_len > 0, idx_if_nonempty, context_.int_val(-1)); + } + + tsize_cache_.emplace_back(1); + cache_expr(l, idx.simplify()); + RECORD_VALUE(0); // Placeholder - validation skipped for indexOf ops + continue; + } else if (info->op == __dfsan::fsubstr) { + // fsubstr: substring with symbolic position/length + // l1 = original content label (full haystack from previous string op) + // l2 = string op label (position or length depending on mode) + // op1 = concrete length n + // op2 = 0 for prefix mode (from 0 to l2), 1 for suffix mode (from l2 to end) + + // Build the full string from l1 (the original content) + z3::expr full_str = context_.string_val(""); + if (info->l1 >= CONST_OFFSET) { + full_str = build_string_from_label(info->l1, input_deps); + } + + z3::expr substr_expr(context_); + bool suffix_mode = (info->op2.i == 1); + + if (suffix_mode) { + // Suffix mode: substr(str, start_pos, remaining_len) + // l2 is fstr_off - need to extract the start position + z3::expr start_pos = context_.int_val(0); + if (info->l2 >= CONST_OFFSET) { + dfsan_label_info *l2_info = get_label_info(info->l2); + if (l2_info->op == __dfsan::fstr_off) { + // fstr_off: l1 = indexOf op, op2 = byte offset + // start_pos = indexOf_result + offset + z3::expr base_idx = get_cached_expr(l2_info->l1, input_deps); + start_pos = base_idx + context_.int_val((int64_t)l2_info->op2.i); + } else { + // Direct indexOf op + start_pos = get_cached_expr(info->l2, input_deps); + } + } + // Use large length to get "rest of string" - Z3 will clamp to actual length + z3::expr full_len(context_, Z3_mk_seq_length(context_, full_str)); + z3::expr len_expr = full_len - start_pos; + substr_expr = z3::expr(context_, Z3_mk_seq_extract(context_, + full_str, + start_pos, + len_expr)); + } else { + // Prefix mode: substr(str, 0, len) + z3::expr len_expr = context_.int_val((int64_t)info->op1.i); + 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); + } + substr_expr = z3::expr(context_, Z3_mk_seq_extract(context_, + full_str, + context_.int_val(0), + len_expr)); + } + + tsize_cache_.emplace_back(1); + cache_expr(l, substr_expr); + // The substr itself doesn't have a numeric value, but downstream ops will use it + RECORD_VALUE(info->op1.i); + continue; + } else if (info->op == __dfsan::fstrcat) { + // strcat: string concatenation + // l1 = dest string label + // l2 = src string label + // op1 = dest pointer (for concrete content access) + // op2 = src pointer (for concrete content access) + // size = length of concrete operand (for memcmp_cache), 0 if both symbolic + + z3::expr dest_str = context_.string_val(""); + z3::expr src_str = context_.string_val(""); + + // Build dest string from l1 + // Only fsubstr and fstrcat cache String expressions; other string ops cache Int (position) + if (info->l1 >= CONST_OFFSET) { + dfsan_label_info *l1_info = get_label_info(info->l1); + if (is_content_string_op(l1_info->op)) { + dest_str = get_cached_expr(info->l1, input_deps); + } else { + dest_str = build_string_from_label(info->l1, input_deps); + } + } else { + // Concrete dest - get from memcmp_cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + dest_str = context_.string_val(s); + } else { + throw z3::exception("cannot find strcat content"); + } + } + + // Build src string from l2 + if (info->l2 >= CONST_OFFSET) { + dfsan_label_info *l2_info = get_label_info(info->l2); + if (is_content_string_op(l2_info->op)) { + src_str = get_cached_expr(info->l2, input_deps); + } else { + src_str = build_string_from_label(info->l2, input_deps); + } + } else { + // Concrete src - get from memcmp_cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + src_str = context_.string_val(s); + } else { + throw z3::exception("cannot find strcat content"); + } + } + + // Create Z3 string concatenation + z3::expr concat_result = z3::concat(dest_str, src_str); + + tsize_cache_.emplace_back(1); + cache_expr(l, concat_result); + RECORD_VALUE(0); + continue; + } else if (info->op == __dfsan::fstrcmp) { + // String comparison using Z3 string theory + // l1 = first string label (may be fsubstr or content) + // l2 = second string label (may be fsubstr or content) + // size = comparison length (in bytes, for memcmp_cache lookup) + // op1 = s1 pointer (for memcmp_cache lookup) + // op2 = s2 pointer (for memcmp_cache lookup) + + z3::expr str1 = context_.string_val(""); + z3::expr str2 = context_.string_val(""); + + // Build first string + if (info->l1 >= CONST_OFFSET) { + dfsan_label_info *l1_info = get_label_info(info->l1); + if (is_content_string_op(l1_info->op)) { + // fsubstr/fstrcat - get the cached String expression + str1 = get_cached_expr(info->l1, input_deps); + } else { + // Regular content - build string from labels + str1 = build_string_from_label(info->l1, input_deps); + } + } else { + // Concrete - get from memcmp_cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + str1 = context_.string_val(s); + } else { + throw z3::exception("cannot find strcmp content"); + } + } + + // Build second string + if (info->l2 >= CONST_OFFSET) { + dfsan_label_info *l2_info = get_label_info(info->l2); + if (is_content_string_op(l2_info->op)) { + // fsubstr/fstrcat - get the cached String expression + str2 = get_cached_expr(info->l2, input_deps); + } else { + // Regular content - build string from labels + str2 = build_string_from_label(info->l2, input_deps); + } + } else { + // Concrete - get from memcmp_cache + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + str2 = context_.string_val(s); + } else { + throw z3::exception("cannot find strcmp content"); + } + } + + // Create equality: strcmp returns 0 when equal, non-zero otherwise + z3::expr eq = z3::ite(str1 == str2, + context_.bv_val(0, 32), + context_.bv_val(1, 32)); + tsize_cache_.emplace_back(1); + cache_expr(l, eq); + RECORD_VALUE(0); + continue; + } else if (info->op == __dfsan::fprefixof) { + // prefixof: check if str starts with prefix + // l1 = string label, l2 = prefix label + // size = comparison length, op1 = str ptr, op2 = prefix ptr + + z3::expr str = context_.string_val(""); + z3::expr prefix = context_.string_val(""); + + // Build first string (str) + if (info->l1 >= CONST_OFFSET) { + dfsan_label_info *l1_info = get_label_info(info->l1); + if (is_content_string_op(l1_info->op)) { + str = get_cached_expr(info->l1, input_deps); + } else { + str = build_string_from_label(info->l1, input_deps); + } + } else { + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + str = context_.string_val(s); + } else { + throw z3::exception("cannot find prefixof str content"); + } + } + + // Build second string (prefix) + if (info->l2 >= CONST_OFFSET) { + dfsan_label_info *l2_info = get_label_info(info->l2); + if (is_content_string_op(l2_info->op)) { + prefix = get_cached_expr(info->l2, input_deps); + } else { + prefix = build_string_from_label(info->l2, input_deps); + } + } else { + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + prefix = context_.string_val(s); + } else { + throw z3::exception("cannot find prefixof prefix content"); + } + } + + // Use Z3's prefixof: returns 1 if str starts with prefix, else 0 + z3::expr result = z3::ite(z3::prefixof(prefix, str), + context_.bv_val(1, 32), + context_.bv_val(0, 32)); + tsize_cache_.emplace_back(1); + cache_expr(l, result); + RECORD_VALUE(0); + continue; + } else if (info->op == __dfsan::fsuffixof) { + // suffixof: check if str ends with suffix + // l1 = string label, l2 = suffix label + // size = comparison length, op1 = str ptr, op2 = suffix ptr + + z3::expr str = context_.string_val(""); + z3::expr suffix = context_.string_val(""); + + // Build first string (str) - same pattern as fprefixof + if (info->l1 >= CONST_OFFSET) { + dfsan_label_info *l1_info = get_label_info(info->l1); + if (is_content_string_op(l1_info->op)) { + str = get_cached_expr(info->l1, input_deps); + } else { + str = build_string_from_label(info->l1, input_deps); + } + } else { + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + str = context_.string_val(s); + } else { + throw z3::exception("cannot find suffixof str content"); + } + } + + // Build second string (suffix) + if (info->l2 >= CONST_OFFSET) { + dfsan_label_info *l2_info = get_label_info(info->l2); + if (is_content_string_op(l2_info->op)) { + suffix = get_cached_expr(info->l2, input_deps); + } else { + suffix = build_string_from_label(info->l2, input_deps); + } + } else { + auto it = memcmp_cache_.find(l); + if (it != memcmp_cache_.end()) { + std::string s(reinterpret_cast(it->second.get()), info->size); + suffix = context_.string_val(s); + } else { + throw z3::exception("cannot find suffixof suffix content"); + } + } + + // Use Z3's suffixof: returns 1 if str ends with suffix, else 0 + z3::expr result = z3::ite(z3::suffixof(suffix, str), + context_.bv_val(1, 32), + context_.bv_val(0, 32)); + tsize_cache_.emplace_back(1); + cache_expr(l, result); + RECORD_VALUE(0); + continue; + } else if (info->op == __dfsan::fstr_off) { + // fstr_off: string op pointer + constant offset (from GEP) + // l1 = string op label (fstrchr result) + // op2 = byte offset (e.g., 1 for sep + 1) + // The result is a pointer with the offset recorded for later substr calculation + + if (info->l1 >= CONST_OFFSET) { + // Get the index expression for the base string op + z3::expr idx_expr = get_cached_expr(info->l1, input_deps); + int64_t gep_offset = (int64_t)info->op2.i; + + // The fstr_off result is idx + offset (still an Int for string indexing) + z3::expr offset_idx = idx_expr + (int)gep_offset; + + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, offset_idx); + // 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)); + RECORD_VALUE(0); + } + continue; } else if (info->op == __dfsan::Alloca || info->op == __dfsan::Free) { // not expression, do nothing tsize_cache_.emplace_back(0); @@ -383,7 +1140,66 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } // common ops - uint8_t size = info->size; + uint16_t size = info->size; // Must be uint16_t to handle sizes > 255 bits + + // Early check for ICmp with string functions - handle before creating BVs + // because string functions use 'size' field for other purposes (e.g., needle length) + if ((info->op & 0xff) == __dfsan::ICmp) { + uint16_t l1_op = info->l1 >= CONST_OFFSET ? get_label_info(info->l1)->op : 0; + 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); + + if (l1_is_strfunc || l2_is_strfunc) { + // String function comparison - convert index to found/not-found + // strchr returns -1 for not found, >= 0 for found + z3::expr cmp_expr(context_); + z3::expr zero = context_.int_val(0); + int64_t found_pos; + bool found; + uint16_t predicate = info->op >> 8; + + if (l1_is_strfunc && info->l2 == 0 && info->op2.i == 0) { + // Comparing string result with NULL (0) + z3::expr idx = get_cached_expr(info->l1, input_deps); + found_pos = (int64_t)value_cache_[info->l1]; + found = found_pos >= 0; + z3::expr found_expr = idx >= zero; + if (predicate == __dfsan::bvneq) { + cmp_expr = found_expr; // != NULL means found + } else if (predicate == __dfsan::bveq) { + cmp_expr = !found_expr; // == NULL means not found + } else { + throw z3::exception("unsupported predicate for string search result"); + } + } else if (l2_is_strfunc && info->l1 == 0 && info->op1.i == 0) { + // NULL compared with string result + z3::expr idx = get_cached_expr(info->l2, input_deps); + found_pos = (int64_t)value_cache_[info->l2]; + found = found_pos >= 0; + z3::expr found_expr = idx >= zero; + if (predicate == __dfsan::bvneq) { + cmp_expr = found_expr; // != NULL means found + } else if (predicate == __dfsan::bveq) { + cmp_expr = !found_expr; // == NULL means not found + } else { + throw z3::exception("unsupported predicate for string search result"); + } + } else { + throw z3::exception("unsupported string comparison"); + } + + tsize_cache_.emplace_back(tsize_cache_[info->l1] + tsize_cache_[info->l2]); + cache_expr(l, cmp_expr); +#if FILTER_WRONG_AST + // For string ops, calculate value based on found/not-found semantics + bool cmp_result = (predicate == __dfsan::bvneq) ? found : !found; + value_cache_.emplace_back(cmp_result ? 1 : 0); +#endif + continue; + } + } + uint64_t valmask = size < 64 ? (1UL << size) - 1 : ~0UL; // size for concat is a bit complicated ... if (info->op == __dfsan::Concat && info->l1 == 0) { @@ -476,6 +1292,24 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { break; } case __dfsan::Sub: { + // Check for pointer arithmetic pattern: (ptr_with_string_op) - base_addr + // When l1 is PtrToInt of a string op and l2 is constant (untainted base), + // the result is just the index (since ptr = base + index, so ptr - base = index) + if (info->l1 >= CONST_OFFSET && info->l2 == 0) { + dfsan_label_info *l1_info = get_label_info(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) { + // This is (PtrToInt(string_op)) - base_addr = index + // The expression is just the index (op1 already contains int2bv(idx)) + cache_expr(l, op1); + // The value is just the index, not idx - base_addr + RECORD_VALUE(val1); + break; + } + } + } cache_expr(l, op1 - op2); RECORD_VALUE(val1 - val2); break; @@ -523,11 +1357,20 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { } // relational case __dfsan::ICmp: { - cache_expr(l, get_cmd(op1, op2, info->op >> 8)); + // Note: string function ICmps are handled early before BV creation + uint16_t l1_op = info->l1 >= CONST_OFFSET ? get_label_info(info->l1)->op : 0; + uint16_t l2_op = info->l2 >= CONST_OFFSET ? get_label_info(info->l2)->op : 0; + + // fprintf(stderr, "DEBUG serialize ICmp label %u: l1=%u (op=%u), l2=%u (op=%u), predicate=%u\n", + // l, info->l1, l1_op, info->l2, l2_op, info->op >> 8); + // fprintf(stderr, "DEBUG serialize ICmp: val1=%lu (cached), val2=%lu (cached), op1.i=%lu (runtime), op2.i=%lu (runtime)\n", + // val1, val2, (uint64_t)info->op1.i, (uint64_t)info->op2.i); + #if FILTER_WRONG_AST // we have both operands recorded for ICmp 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:" // "expected op1 %lu, got %lu, expected op2 %lu, got %lu\n", // l, info->op1.i, val1, info->op2.i, val2); @@ -535,26 +1378,62 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { // dump_value_cache(info->l1); // dump_value_cache(info->l2); - // memcmp is a special case, just fix it for now - bool is_memcmp = false; - if (get_label_info(info->l1)->op == __dfsan::fmemcmp) { + // Special cases where we don't have the actual value cached: + // - memcmp/atoi/strcmp: fix using runtime value from ICmp + // - indexOf operations: op1 repurposed for haystack pointer, skip validation + bool is_special = false; + if (l1_op == __dfsan::fmemcmp || l1_op == __dfsan::fatoi || l1_op == __dfsan::fstrcmp) { + fprintf(stderr, "DEBUG serialize ICmp: fixing up value_cache_[%u] from %lu to %lu (op=%u)\n", + info->l1, value_cache_[info->l1], (uint64_t)info->op1.i, l1_op); value_cache_[info->l1] = val1 = info->op1.i; - is_memcmp = true; + is_special = true; } - if (get_label_info(info->l2)->op == __dfsan::fmemcmp) { + if (l2_op == __dfsan::fmemcmp || l2_op == __dfsan::fatoi || l2_op == __dfsan::fstrcmp) { + fprintf(stderr, "DEBUG serialize ICmp: fixing up value_cache_[%u] from %lu to %lu (op=%u)\n", + info->l2, value_cache_[info->l2], (uint64_t)info->op2.i, l2_op); value_cache_[info->l2] = val2 = info->op2.i; - is_memcmp = true; + is_special = true; + } + // Check if either operand contains indexOf operations + if ((info->l1 >= CONST_OFFSET && label_contains_indexof(info->l1)) || + (info->l2 >= CONST_OFFSET && label_contains_indexof(info->l2))) { + is_special = true; } - if (!is_memcmp) + if (!is_special) { throw z3::exception("value mismatch for ICmp"); + } } - value_cache_.emplace_back( - eval_icmp(info->op >> 8, val1, val2, size) ? 1 : 0); + uint64_t icmp_result = eval_icmp(info->op >> 8, val1, val2, size) ? 1 : 0; + // fprintf(stderr, "DEBUG serialize ICmp: recording value_cache_[%u] = %lu\n", l, icmp_result); + value_cache_.emplace_back(icmp_result); #endif + // 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)); break; } // concat case __dfsan::Concat: { + // Check if either operand is a String (from fsubstr or string ops) + // We can't concat String with bitvector + if (!op1.is_bv() || !op2.is_bv()) { + // If one operand is String and the other is constant 0 (label 0), + // just use the String. The constant bytes don't contribute to constraints. + if (!op1.is_bv() && info->l2 == 0) { + cache_expr(l, op1); + RECORD_VALUE(val1); + break; + } + if (!op2.is_bv() && info->l1 == 0) { + cache_expr(l, op2); + RECORD_VALUE(val2); + break; + } + // fprintf(stderr, "DEBUG Concat %u: l1=%u (sort=%s, is_bv=%d), l2=%u (sort=%s, is_bv=%d)\n", + // l, info->l1, op1.get_sort().to_string().c_str(), op1.is_bv(), + // info->l2, op2.get_sort().to_string().c_str(), op2.is_bv()); + throw z3::exception("concat with non-bitvector operand (string op involved)"); + } cache_expr(l, z3::concat(op2, op1)); // little endian RECORD_VALUE((val2 << op1.get_sort().bv_size()) | (val1)); break; @@ -591,12 +1470,15 @@ int Z3AstParser::parse_cond(dfsan_label label, bool result, bool add_nested, std z3::expr r = context_.bool_val(result); #if FILTER_WRONG_AST - if (value_cache_[label] != result) { + // Skip validation for indexOf operations (op1 repurposed for haystack pointer) + bool contains_indexof = label_contains_indexof(label); + + if (!contains_indexof && value_cache_[label] != result) { // recalcuated value must match the recorded value - // fprintf(stderr, "WARNING: value mismatch for label %u: expected %ld, got %d\n", - // label, value_cache_[label], result); - // fprintf(stderr, "cond: %s\n", cond.to_string().c_str()); - // dump_value_cache(label); + fprintf(stderr, "WARNING: value mismatch for label %u: expected %lu, got %d\n", + label, value_cache_[label], result); + fprintf(stderr, "cond: %s\n", cond.to_string().c_str()); + dump_value_cache(label); return -1; } #endif @@ -619,7 +1501,7 @@ 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\n", e.msg()); } // exception happened, nothing added @@ -669,7 +1551,7 @@ int Z3AstParser::parse_gep(dfsan_label ptr_label, uptr ptr, dfsan_label index_la try { // prepare current index - uint8_t size = get_label_info(index_label)->size; + uint16_t size = get_label_info(index_label)->size; z3::expr r = context_.bv_val(index, size); input_dep_set_t inputs; @@ -737,7 +1619,7 @@ int Z3AstParser::add_constraints(dfsan_label label, uint64_t result) { z3::expr expr = serialize(label, inputs); collect_more_deps(inputs); // prepare result - uint8_t size = get_label_info(label)->size; + 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); @@ -822,30 +1704,38 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut try { // setup global solver - z3::solver solver(context_, "QF_BV"); + // Use default solver to auto-detect theory (needed for string constraints) + z3::solver solver(context_); solver.set("timeout", timeout); // solve the first constraint (optimistic) z3::expr e = task->at(0); solver.add(e); + // fprintf(stderr, "DEBUG solve_task[%lu]: checking first constraint: %s\n", task_id, e.to_string().c_str()); z3::check_result res = solver.check(); + // 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; // 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()); // check nested, if any if (task->size() > 1) { solver.push(); // add nested constraints + // fprintf(stderr, "DEBUG solve_task[%lu]: adding %zu nested constraints\n", task_id, task->size() - 1); for (size_t i = 1; i < task->size(); i++) { + // fprintf(stderr, "DEBUG solve_task[%lu]: nested[%zu]: %s\n", task_id, i, task->at(i).to_string().c_str()); solver.add(task->at(i)); } res = solver.check(); + // fprintf(stderr, "DEBUG solve_task[%lu]: nested result = %d (sat=1, unsat=0, unknown=2)\n", task_id, (int)res); if (res == z3::sat) { ret = nested_sat; m = solver.get_model(); + // fprintf(stderr, "DEBUG solve_task[%lu]: nested SAT model:\n%s\n", task_id, m.to_string().c_str()); } else if (res == z3::unsat) { - // fprintf(stderr, "WARNING: nested unsat for task %lu: %s\n", - // task_id, solver.to_smt2().data()); + fprintf(stderr, "WARNING: nested unsat for task %lu: %s\n", + task_id, solver.to_smt2().c_str()); ret = opt_sat_nested_unsat; } else { ret = opt_sat_nested_timeout; @@ -853,37 +1743,121 @@ Z3ParserSolver::solve_task(uint64_t task_id, unsigned timeout, solution_t &solut } else { ret = nested_sat; // XXX: upgrade to nested_sat? } + + // Check if model contains strlen symbols and optimize if needed + std::vector> strlen_vars; // (var, max_len) + const uint64_t MAX_STRLEN_EXTEND = 4096; // Reasonable max extension + + 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 (!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) + z3::optimize opt(context_); + z3::params p(context_); + p.set("timeout", timeout); + opt.set(p); + + 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); + } + + bool use_optimized = false; + if (opt.check() == 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()); + // Check if all strlen values are within bounds + bool all_within_bounds = true; + for (const auto &sv : strlen_vars) { + z3::expr val = opt_model.eval(sv.first, true); + uint64_t strlen_val = val.get_numeral_uint64(); + if (strlen_val > sv.second) { + all_within_bounds = false; + break; + } + } + 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); + 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; + } + solver.pop(); + } + } + generate_solution(m, solutions); + // fprintf(stderr, "DEBUG solve_task[%lu]: after generate_solution, solutions.size() = %zu\n", task_id, solutions.size()); } else if (res == z3::unsat) { + // fprintf(stderr, "DEBUG solve_task[%lu]: UNSAT\n", task_id); ret = opt_unsat; - //AOUT("\n%s\n", __z3_solver.to_smt2().c_str()); - //AOUT(" tree_size = %d", __dfsan_label_info[label].tree_size); } else { + // fprintf(stderr, "DEBUG solve_task[%lu]: TIMEOUT\n", task_id); ret = opt_timeout; } } catch (z3::exception ze) { + fprintf(stderr, "WARNING: solve_task[%lu]: EXCEPTION: %s\n", task_id, ze.msg()); ret = unknown_error; } + // fprintf(stderr, "DEBUG solve_task[%lu]: returning with ret=%d, solutions.size() = %zu\n", task_id, ret, solutions.size()); return ret; } 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); 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); z3::symbol name = decl.name(); - // all values should be string symbols if (name.kind() == Z3_STRING_SYMBOL) { + // fprintf(stderr, "DEBUG generate_solution: processing symbol '%s' = %s\n", + // name.str().c_str(), e.to_string().c_str()); if (name.str().find("input") == 0) { uint32_t input; uint32_t offset; sscanf(name.str().c_str(), input_name_format, &input, &offset); uint8_t value = (uint8_t)e.get_numeral_int(); - solutions.push_back({input, offset, value}); + // fprintf(stderr, "DEBUG input-%u-%u: SET offset %u = 0x%02x (individual byte)\n", + // input, offset, offset, value); + solutions.emplace_back(input, offset, value); } else if (!name.str().compare("fsize")) { // FIXME: // off_t size = (off_t)e.get_numeral_int64(); @@ -900,8 +1874,12 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { uint32_t input; uint32_t offset; int base; + uint64_t orig_len; char buf[64]; - sscanf(name.str().c_str(), atoi_name_format, &input, &offset, &base); + int parsed = sscanf(name.str().c_str(), atoi_name_format, &input, &offset, &base, &orig_len); + if (parsed != 4) { + continue; + } const char *format = NULL; switch (base) { case 2: format = "%lb"; break; @@ -911,15 +1889,426 @@ void Z3ParserSolver::generate_solution(z3::model &m, solution_t &solutions) { default: throw z3::exception("unsupported base"); } // XXX: assumed signed - int len = snprintf(buf, 64, format, (int)e.get_numeral_int()); - // len excludes \0 - for (int i = 0; i < len; ++i) { - solutions.push_back({input, offset + i, (uint8_t)buf[i]}); + int new_len = snprintf(buf, 64, format, (int)e.get_numeral_int()); + + if ((uint64_t)new_len > orig_len) { + // Extending: insert extra digits + std::vector insert_bytes(buf + orig_len, buf + new_len); + solutions.emplace_back(input, offset + (uint32_t)orig_len, std::move(insert_bytes)); + // Set the common prefix + for (uint64_t i = 0; i < orig_len; ++i) { + solutions.emplace_back(input, offset + (uint32_t)i, (uint8_t)buf[i]); + } + } else if ((uint64_t)new_len < orig_len) { + // Shrinking: delete extra bytes + solutions.emplace_back(solution_op_t::DELETE, input, + offset + (uint32_t)new_len, + (uint32_t)(orig_len - new_len)); + // Set the new digits + for (int i = 0; i < new_len; ++i) { + solutions.emplace_back(input, offset + i, (uint8_t)buf[i]); + } + } else { + // Same length: just set the digits + for (int i = 0; i < new_len; ++i) { + solutions.emplace_back(input, offset + i, (uint8_t)buf[i]); + } + } + // Set null terminator at the new end + solutions.emplace_back(input, offset + new_len, (uint8_t)0); + } else if (name.str().find("strlen") == 0) { + uint32_t input; + uint32_t offset; + uint64_t orig_len; + uint32_t null_from_input; + if (sscanf(name.str().c_str(), strlen_name_format, + &input, &offset, &orig_len, &null_from_input) != 4) { + throw z3::exception("malformed strlen symbol name"); } - solutions.push_back({input, offset + len, 0}); + + uint64_t target_len = e.get_numeral_uint64(); + // fprintf(stderr, "DEBUG generate_solution: strlen-%u-%u: orig=%lu, target=%lu, null_from_input=%u\n", + // input, offset, orig_len, target_len, null_from_input); + + if (target_len > orig_len) { + // Extending: insert bytes to make the string longer + uint64_t extend_by = target_len - orig_len; + 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 + // For structured formats (null_from_input=0), delimiter handles termination + if (null_from_input) { + solutions.emplace_back(input, offset + (uint32_t)target_len, (uint8_t)0); + } + } else if (target_len < orig_len) { + // Shrinking: delete bytes to make the string shorter + uint64_t shrink_by = orig_len - target_len; + solutions.emplace_back(solution_op_t::DELETE, input, + offset + (uint32_t)target_len, + (uint32_t)shrink_by); + } + // target_len == orig_len: no change needed + } else if (name.str().find("str-") == 0) { + // String variable from strchr/strstr: str-input-offset-len + // Extract byte values from the string and generate solutions + // Handle length changes with INSERT/DELETE like strlen does + 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) { + continue; // Skip malformed string variable + } + + // Get the string value from Z3 and decode escape sequences + if (e.is_string_value()) { + std::string raw_str = e.get_string(); + std::vector bytes = decode_z3_string(raw_str); + uint32_t new_len = bytes.size(); + 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()); + + if (new_len > orig_len) { + // Extending: set common prefix, then insert extra bytes + for (uint32_t j = 0; j < orig_len; j++) { + solutions.emplace_back(input, offset + j, bytes[j]); + } + // Insert the extra bytes after the original range + std::vector insert_bytes(bytes.begin() + orig_len, bytes.end()); + solutions.emplace_back(input, offset + orig_len, std::move(insert_bytes)); + } else if (new_len < orig_len) { + // Shrinking: set new content, then delete extra bytes + for (uint32_t j = 0; j < new_len; j++) { + solutions.emplace_back(input, offset + j, bytes[j]); + } + // Delete the bytes we no longer need + solutions.emplace_back(solution_op_t::DELETE, input, + offset + new_len, + orig_len - new_len); + } else { + // Same length: just set all bytes + for (uint32_t j = 0; j < new_len; j++) { + solutions.emplace_back(input, offset + j, bytes[j]); + } + } + } + } else if (name.str().find("strrchr_idx_") == 0 || + name.str().find("strchr_idx_") == 0) { + // Index variables from strchr/strrchr - skip, they're intermediate + continue; } else { - throw z3::exception("unknown symbol"); + // Skip unknown symbols - Z3 string theory creates internal variables + continue; + } + } + } + + // Post-process solutions: replace null bytes (0x00) with non-null placeholder ('A') + // for bytes within string ranges. Z3 doesn't model C null-termination so may put + // nulls before the target character position. + + // // Debug: print string ranges + // fprintf(stderr, "DEBUG generate_solution: string_ranges_ has %zu entries\n", string_ranges_.size()); + // for (const auto &entry : string_ranges_) { + // fprintf(stderr, "DEBUG generate_solution: input %u has %zu ranges\n", entry.first, entry.second.size()); + // for (const auto &range : entry.second) { + // fprintf(stderr, "DEBUG generate_solution: range [%u, %u)\n", range.first, range.second); + // } + // } + + // 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 this offset is within a string range (but not at the end), replace null + if (sol.offset >= range.first && sol.offset < range.second) { + // fprintf(stderr, "DEBUG generate_solution: replacing null at offset %u (in range [%u,%u))\n", + // sol.offset, range.first, range.second); + sol.val = 'A'; // Replace null with 'A' + break; + } + } } } } + + // fprintf(stderr, "DEBUG generate_solution: finished with %zu solutions\n", solutions.size()); +} + +// 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) { + if (label < CONST_OFFSET) { + throw z3::exception("Invalid string label"); // No tainted content + } + + dfsan_label_info *info = get_label_info(label); + + // Handle Load: multi-byte load from input - create a single symbolic string + if (info->op == __dfsan::Load) { + uint32_t offset = get_label_info(info->l1)->op1.i; + 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); + + // Add dependencies for all bytes in the range + for (uint32_t i = 0; i < len; i++) { + 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()); + + // Cache string info for this label + string_info_cache_[label] = {input, offset, len}; + + return str_var; + } + + // Handle fsubstr and fstrcat: these ops cache String expressions + if (is_content_string_op(info->op)) { + // Should be cached from earlier processing + return get_cached_expr(label, deps); + } + + // Handle fstr_off: string op pointer + constant offset (from GEP) + // l1 = string op label (fstrchr, etc.), op2 = byte offset + if (info->op == __dfsan::fstr_off) { + if (info->l1 == 0) { + throw z3::exception("fstr_off with constant l1"); + } + dfsan_label_info *str_op_info = get_label_info(info->l1); + + // The string op's l1 is the base string content + if (str_op_info->l1 >= CONST_OFFSET) { + int64_t gep_offset = (int64_t)info->op2.i; + + // Get concrete values to check if we're beyond the end + // Bounds check: value_cache_ is indexed by label + if (info->l1 >= value_cache_.size()) { + throw z3::exception("fstr_off label out of value cache bounds"); + } + int64_t str_op_pos = (int64_t)value_cache_[info->l1]; // position of found char + int64_t concrete_start = str_op_pos + gep_offset; + + // Build haystack string FIRST - this populates string_info_cache_ + dfsan_label content_label = str_op_info->l1; + z3::expr haystack = build_string_from_label(content_label, deps); + + // Get string info directly from cache (populated by build_string_from_label) + auto it = string_info_cache_.find(content_label); + if (it == string_info_cache_.end()) { + throw z3::exception("string info not found in cache for fstr_off"); + } + uint32_t input_id = it->second.input_id; + uint32_t base_offset = it->second.offset; + uint32_t haystack_len = it->second.length; + + // fprintf(stderr, "DEBUG build_string_from_label fstr_off: content_label=%u, haystack_len=%u, input_id=%u, base_offset=%u, concrete_start=%ld\n", + // content_label, haystack_len, input_id, base_offset, concrete_start); + + // Check if start is beyond the end of the haystack + if (concrete_start >= (int64_t)(base_offset + haystack_len)) { + // Beyond end: create a new insertion point variable + // str---0 means "string at offset with no original content" + // fprintf(stderr, "DEBUG build_string_from_label: fstr_off beyond end, creating insertion point %s\ + -n", name); + char name[256]; + snprintf(name, sizeof(name), "str-%u-%ld-0", input_id, concrete_start); + z3::symbol symbol = context_.str_symbol(name); + z3::expr str_var = context_.constant(symbol, context_.string_sort()); + + // Don't add dependency for insertion point - it's beyond file bounds + return str_var; + } + + // Within bounds: use original suffix extraction + z3::expr idx_expr = get_cached_expr(info->l1, deps); + z3::expr suffix_start = idx_expr + (int)gep_offset; + z3::expr haystack_len_expr(context_, Z3_mk_seq_length(context_, haystack)); + z3::expr suffix_len = haystack_len_expr - idx_expr - (int)gep_offset; + + return z3::to_expr(context_, Z3_mk_seq_extract(context_, + haystack, + suffix_start, + suffix_len)); + } + throw z3::exception("invalid str_op for fstr_off"); + } + + // Handle string search ops (fstrchr, fstrrchr, fstrstr, fstrpbrk): + // These labels represent pointer results. When used as content directly + // (without GEP offset), we build content at the found position. + if (is_indexof_op(info->op)) { + if (info->l1 >= CONST_OFFSET) { + // Get the index expression for this string op (if already cached) + z3::expr idx_expr = get_cached_expr(label, deps); + + // Build the full haystack string + z3::expr haystack = build_string_from_label(info->l1, deps); + + // Create suffix starting at the found position (no offset) + // substr(haystack, idx, len-idx) - content from found position to end + z3::expr haystack_len(context_, Z3_mk_seq_length(context_, haystack)); + z3::expr suffix_len = haystack_len - idx_expr; + + return z3::to_expr(context_, Z3_mk_seq_extract(context_, + haystack, + idx_expr, + suffix_len)); + } + throw z3::exception("invalid l1 for fstr_op"); + } + + // Handle Concat: check if it's a chain of consecutive input bytes + // If so, create a single string variable for the whole range + if (info->op == __dfsan::Concat) { + // Try to find the range of consecutive input bytes + uint32_t min_offset = UINT32_MAX; + uint32_t max_offset = 0; + uint32_t input_id = UINT32_MAX; + bool is_consecutive = true; + std::vector offsets; + + // Helper lambda to collect offsets from a label + std::function collect_offsets = [&](dfsan_label label) { + if (!is_consecutive) return; + if (label < CONST_OFFSET) { + // Concrete byte in the chain - mark as non-consecutive + is_consecutive = false; + return; + } + dfsan_label_info *linfo = get_label_info(label); + if (linfo->op == 0) { + // Single input byte + uint32_t off = linfo->op1.i; + uint32_t inp = linfo->op2.i; + if (input_id == UINT32_MAX) input_id = inp; + else if (input_id != inp) { is_consecutive = false; return; } + offsets.push_back(off); + if (off < min_offset) min_offset = off; + if (off > max_offset) max_offset = off; + } else if (linfo->op == __dfsan::Concat) { + collect_offsets(linfo->l1); + collect_offsets(linfo->l2); + } else { + // Other operation - not a simple byte chain + is_consecutive = false; + } + }; + + collect_offsets(label); + + // Check if offsets are truly consecutive + if (is_consecutive && !offsets.empty() && input_id != UINT32_MAX) { + std::sort(offsets.begin(), offsets.end()); + for (size_t i = 1; i < offsets.size(); i++) { + if (offsets[i] != offsets[i-1] + 1) { + is_consecutive = false; + break; + } + } + } + + if (is_consecutive && !offsets.empty()) { + // Create a single string variable for the whole range + 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); + + // Add dependencies for all bytes + for (uint32_t off : offsets) { + 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); + + // Cache string info for this label + string_info_cache_[label] = {input_id, start_offset, len}; + + return context_.constant(symbol, context_.string_sort()); + } + + // Fall back to recursive concatenation if not consecutive + z3::expr left(context_); + z3::expr right(context_); + + if (info->l1 >= CONST_OFFSET) { + left = build_string_from_label(info->l1, deps); + } else { + char c = (char)(info->op1.i & 0xff); + left = context_.string_val(std::string(1, c)); + } + + if (info->l2 >= CONST_OFFSET) { + right = build_string_from_label(info->l2, deps); + } else { + char c = (char)(info->op2.i & 0xff); + right = context_.string_val(std::string(1, c)); + } + + // Try to cache combined string info from children for downstream lookups + // Use left child's info as base (it comes first in the concat) + if (info->l1 >= CONST_OFFSET) { + auto it = string_info_cache_.find(info->l1); + if (it != string_info_cache_.end()) { + uint32_t combined_len = it->second.length; + // Add right child's length if available + if (info->l2 >= CONST_OFFSET) { + auto it2 = string_info_cache_.find(info->l2); + if (it2 != string_info_cache_.end()) { + combined_len += it2->second.length; + } + } else { + combined_len += 1; // concrete byte + } + string_info_cache_[label] = {it->second.input_id, it->second.offset, combined_len}; + } + } + + return z3::concat(left, right); + } + + // Handle single input byte (op == 0) + if (info->op == 0) { + 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); + z3::symbol symbol = context_.str_symbol(name); + + // Cache string info for this label + string_info_cache_[label] = {input, offset, 1}; + + return context_.constant(symbol, context_.string_sort()); + } + + // Last resort: empty string + return context_.string_val(""); +} + +// Get byte expression for a specific input offset +z3::expr Z3AstParser::get_byte_expr(uint32_t input, uint32_t offset, input_dep_set_t &deps) { + deps.insert(std::make_pair(input, offset)); + char name[256]; + snprintf(name, sizeof(name), input_name_format, input, offset); + z3::symbol symbol = context_.str_symbol(name); + return context_.constant(symbol, context_.bv_sort(8)); } diff --git a/solvers/z3.cpp b/solvers/z3.cpp index 9835a63d..3c354433 100644 --- a/solvers/z3.cpp +++ b/solvers/z3.cpp @@ -42,6 +42,7 @@ static std::unordered_set __buffers; static void generate_input(symsan::Z3ParserSolver::solution_t &solutions) { + using op_t = symsan::Z3ParserSolver::solution_op_t; if (tainted.is_stdin) { // FIXME: input is stdin @@ -49,6 +50,50 @@ static void generate_input(symsan::Z3ParserSolver::solution_t &solutions) { return; } + // Build the new input in memory to handle INSERT/DELETE properly + std::vector new_input((uint8_t*)tainted.buf, + (uint8_t*)tainted.buf + tainted.size); + + // Sort solutions by offset in descending order so INSERT/DELETE don't + // invalidate subsequent offsets + std::vector order(solutions.size()); + for (uptr i = 0; i < order.size(); ++i) order[i] = i; + Sort(order.data(), order.size(), [&solutions](uptr a, uptr b) { + return solutions[a].offset > solutions[b].offset; + }); + + for (uptr idx : order) { + const auto& sol = solutions[idx]; + switch (sol.op) { + case op_t::SET: + if (sol.offset < new_input.size()) { + AOUT("SET offset %d = %x\n", sol.offset, sol.val); + new_input[sol.offset] = sol.val; + } + break; + + case op_t::INSERT: + if (sol.offset <= new_input.size()) { + AOUT("INSERT %zu bytes at offset %d\n", sol.data.size(), sol.offset); + new_input.insert(new_input.begin() + sol.offset, + sol.data.begin(), sol.data.end()); + } + break; + + case op_t::DELETE: + if (sol.offset < new_input.size()) { + uptr del_len = sol.len; + if (sol.offset + del_len > new_input.size()) + del_len = new_input.size() - sol.offset; + AOUT("DELETE %zu bytes at offset %d\n", del_len, sol.offset); + new_input.erase(new_input.begin() + sol.offset, + new_input.begin() + sol.offset + del_len); + } + break; + } + } + + // Write the new input to file char path[PATH_MAX]; internal_snprintf(path, PATH_MAX, "%s/id-%d-%d-%d", __output_dir, __instance_id, __session_id, __current_index++); @@ -58,22 +103,13 @@ static void generate_input(symsan::Z3ParserSolver::solution_t &solutions) { return; } - if (!WriteToFile(fd, tainted.buf, tainted.size)) { - AOUT("WARNING: failed to copy original input\n"); - CloseFile(fd); - return; - } - AOUT("generate #%d output\n", __current_index - 1); + AOUT("generate #%d output (size: %zu -> %zu)\n", + __current_index - 1, tainted.size, new_input.size()); - for (auto const& sol : solutions) { - uint8_t value = sol.val; - AOUT("offset %d = %x\n", sol.offset, value); - internal_lseek(fd, sol.offset, SEEK_SET); - WriteToFile(fd, &value, sizeof(value)); + if (!WriteToFile(fd, new_input.data(), new_input.size())) { + AOUT("WARNING: failed to write new input\n"); } - // FIXME: fsize - CloseFile(fd); } @@ -352,6 +388,25 @@ __taint_trace_offset(dfsan_label offset_label, int64_t offset, unsigned size) { __solved_labels.insert(offset_label); } +extern "C" SANITIZER_INTERFACE_ATTRIBUTE void +__taint_trace_memcmp(dfsan_label label) { + if (label == 0) + return; + + dfsan_label_info *info = dfsan_get_label_info(label); + + AOUT("tainted memcmp: %d, size: %d\n", label, info->size); + + // If both operands are symbolic, no concrete content to cache + if ((info->l1 != CONST_LABEL && info->l2 != CONST_LABEL) || info->size == 0) + return; + + uint8_t *content_ptr = (info->l1 == CONST_LABEL) ? (uint8_t*)info->op1.i + : (uint8_t*)info->op2.i; + // Cache the concrete content for later solving, concrete oprand is always in op1 + __z3_parser->record_memcmp(label, content_ptr, info->size); +} + extern "C" void InitializeSolver() { __output_dir = flags().output_dir; __instance_id = flags().instance_id; diff --git a/tests/atoi_test.c b/tests/atoi_test.c new file mode 100644 index 00000000..17b5577e --- /dev/null +++ b/tests/atoi_test.c @@ -0,0 +1,54 @@ +// Test: atoi constraints for extending and shrinking digit strings +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: printf '999' > %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_DONT_OPTIMIZE=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-GEN1 %s +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN2 %s + +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + char buf[64]; + FILE *f = fopen(argv[1], "r"); + if (!f) { + perror("fopen"); + return 1; + } + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + + int val = atoi(buf); + printf("atoi returned: %d\n", val); + + // Test shrinking: 999 -> 42 (need fewer digits) + if (val == 42) { + // CHECK-GEN1: SHRINK-SUCCESS + printf("SHRINK-SUCCESS: val == 42\n"); + } + + // Test extending: 999 -> 12345 (need more digits) + if (val == 12345) { + // CHECK-GEN2: EXTEND-SUCCESS + printf("EXTEND-SUCCESS: val == 12345\n"); + } + + // Original input (999) hits neither branch + if (val != 42 && val != 12345) { + // CHECK-ORIG: NEITHER + printf("NEITHER: val = %d\n", val); + } + + return 0; +} diff --git a/tests/bounds.cpp b/tests/bounds.cpp index dd111c0d..23082d6e 100644 --- a/tests/bounds.cpp +++ b/tests/bounds.cpp @@ -6,10 +6,10 @@ // RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out solve_ub=1" %fgtest %t.fg %t.bin // RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-0 2>&1 FileCheck %s --check-prefix=CHECK-A2 // RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-1 2>&1 FileCheck %s --check-prefix=CHECK-A2 +// RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-2 2>&1 FileCheck %s --check-prefix=CHECK-B-3 // RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-3 2>&1 FileCheck %s --check-prefix=CHECK-B-3 -// RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-4 2>&1 FileCheck %s --check-prefix=CHECK-B-3 -// RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-7 2>&1 FileCheck %s --check-prefix=CHECK-C-4 -// RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-8 2>&1 FileCheck %s --check-prefix=CHECK-C-4 +// RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-4 2>&1 FileCheck %s --check-prefix=CHECK-C-4 +// RUN: not env UBSAN_OPTIONS="halt_on_error=1" %t.ubsan %t.out/id-0-0-5 2>&1 FileCheck %s --check-prefix=CHECK-C-4 #include #include diff --git a/tests/concrete_haystack.c b/tests/concrete_haystack.c new file mode 100644 index 00000000..b16d688a --- /dev/null +++ b/tests/concrete_haystack.c @@ -0,0 +1,79 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("AAA:BB")' > %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-GEN1 %s +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN2 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-4 | FileCheck --check-prefix=CHECK-GEN4 %s + +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char *haystack = "deadbeef\0"; // Concrete haystack + char input[256] = {0}; + + FILE* fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t n = fread(input, 1, sizeof(input) - 1, fp); + fclose(fp); + input[n] = '\0'; + + // Test 1: strchr with concrete haystack + symbolic needle + char *pos = strchr(haystack, input[0]); + if (pos) { + // CHECK-GEN1: strchr: Found + printf("strchr: Found '%c' at position %ld\n", input[0], pos - haystack); + exit(0); + } + + // Test 2: strrchr with concrete haystack + symbolic needle + char *rpos = strrchr(haystack, input[1]); + if (rpos) { + // CHECK-GEN2: strrchr: Found + printf("strrchr: Found '%c' at position %ld\n", input[1], rpos - haystack); + exit(0); + } + + char *sep = strchr(&input[2], ':'); + if (sep) { + *sep = '\0'; // split input for strstr/strpbrk tests + } else { + printf("Missing ':' separator\n"); + exit(1); + } + + // Test 3: strstr + char *spos = strstr(haystack, &input[2]); + if (spos) { + // CHECK-GEN3: strstr: Found + printf("strstr: Found substring at position %ld\n", spos - haystack); + exit(0); + } + + // Test 4: strpbrk + char *pbrk_pos = strpbrk(haystack, sep + 1); + if (pbrk_pos) { + // CHECK-GEN4: strpbrk: Found + printf("strpbrk: Found character '%c' at position %ld\n", *pbrk_pos, pbrk_pos - haystack); + exit(0); + } + + // CHECK-ORIG: Not found + printf("Not found\n"); + + return 0; +} diff --git a/tests/cpp_string.cpp b/tests/cpp_string.cpp index c86ce968..a078125b 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-0 | FileCheck --check-prefix=CHECK-GEN %s +// RUN: %t.uninstrumented %t.out/id-0-0-2 | FileCheck --check-prefix=CHECK-GEN %s // doesn't work with in-process z3 solver @@ -16,20 +16,21 @@ #include #include #include -#include "lib.h" - int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "Usage: %s [file]\n", argv[0]); return -1; } - char buf[20]; - size_t ret; - - FILE* fp = chk_fopen(argv[1], "rb"); - chk_fread(buf, 1, sizeof(buf), fp); + char buf[256] = {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'; // if (contents.substr(0, 7) == "iamback") { // std::cout <<" hhe\n"; diff --git a/tests/memchr.c b/tests/memchr.c new file mode 100644 index 00000000..0fac69bd --- /dev/null +++ b/tests/memchr.c @@ -0,0 +1,39 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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[256] = {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); + + void *p = memchr(buf, 0x7f, n); + if (p != NULL) { + // CHECK-GEN: Found byte + printf("Found byte\n"); + } else { + // CHECK-ORIG: No byte + printf("No byte\n"); + } + return 0; +} diff --git a/tests/memchr_chain.c b/tests/memchr_chain.c new file mode 100644 index 00000000..7371b950 --- /dev/null +++ b/tests/memchr_chain.c @@ -0,0 +1,59 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration: finds first colon +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration: finds second colon using output from first +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test chained memchr with bounded length from previous result: +// t1 = memchr(buf, c1, len); t2 = memchr(buf, c2, t1-buf); +// This tests forward search (indexof) with substr constraint + +#define _GNU_SOURCE +#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[256] = {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'; + + // First memchr: find first ';' in the buffer + char *t1 = (char *)memchr(buf, ';', n); + if (t1) { + // Second memchr: find ':' that appears after the ';' + // This uses the bounded search pattern: memchr(t1, ':', n - (t1 - buf)) + size_t len_after_t1 = n - (t1 - buf); + char *t2 = (char *)memchr(t1, ':', len_after_t1); + if (t2) { + // CHECK-GEN: Found colon before semicolon + printf("Found colon before semicolon (colon at %ld, semicolon at %ld)\n", + (long)(t2 - buf), (long)(t1 - buf)); + } else { + printf("Found semicolon but no colon before it (semicolon at %ld)\n", + (long)(t1 - buf)); + } + } else { + // CHECK-ORIG: No semicolon + printf("No semicolon\n"); + } + return 0; +} diff --git a/tests/memchr_mixed_chain.c b/tests/memchr_mixed_chain.c new file mode 100644 index 00000000..d16f17d7 --- /dev/null +++ b/tests/memchr_mixed_chain.c @@ -0,0 +1,59 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration: finds last semicolon (backward search) +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration: finds colon before the semicolon (forward search with bound) +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test mixed chain: memrchr (backward) followed by memchr (forward with bound) +// t1 = memrchr(buf, ';', len); // find LAST semicolon +// t2 = memchr(buf, ':', t1-buf); // find first colon BEFORE the semicolon +// This tests combining last_indexof and indexof with substr constraint + +#define _GNU_SOURCE +#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[256] = {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'; + + // First: memrchr finds LAST ';' (backward search / last_indexof) + char *t1 = (char *)memrchr(buf, ';', n); + if (t1) { + // Second: memchr finds first ':' BEFORE the ';' (forward search with bound) + size_t len_before_t1 = t1 - buf; + char *t2 = (char *)memchr(buf, ':', len_before_t1); + if (t2) { + // CHECK-GEN: Found colon before last semicolon + printf("Found colon before last semicolon (colon at %ld, semicolon at %ld)\n", + (long)(t2 - buf), (long)(t1 - buf)); + } else { + printf("Found semicolon but no colon before it (semicolon at %ld)\n", + (long)(t1 - buf)); + } + } else { + // CHECK-ORIG: No semicolon + printf("No semicolon\n"); + } + return 0; +} diff --git a/tests/memmem.c b/tests/memmem.c new file mode 100644 index 00000000..1cfca4ff --- /dev/null +++ b/tests/memmem.c @@ -0,0 +1,44 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 + +// Test memmem: find substring with explicit lengths + +#define _GNU_SOURCE +#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[256] = {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); + + // memmem with explicit lengths (needle is "ab\0c" including null byte) + const char needle[] = "ABCD"; + void *t1 = memmem(buf, n, needle, 4); + if (t1) { + // CHECK-GEN: Found pattern + printf("Found pattern at position %ld\n", (long)((char*)t1 - buf)); + } else { + // CHECK-ORIG: Pattern not found + printf("Pattern not found\n"); + } + return 0; +} diff --git a/tests/memrchr.c b/tests/memrchr.c new file mode 100644 index 00000000..8b584f35 --- /dev/null +++ b/tests/memrchr.c @@ -0,0 +1,40 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 + +#define _GNU_SOURCE +#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[256] = {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); + + void *p = memrchr(buf, 0x7f, n); + if (p != NULL) { + // CHECK-GEN: Found byte + printf("Found byte\n"); + } else { + // CHECK-ORIG: No byte + printf("No byte\n"); + } + return 0; +} diff --git a/tests/memrchr_chain.c b/tests/memrchr_chain.c new file mode 100644 index 00000000..b296abbf --- /dev/null +++ b/tests/memrchr_chain.c @@ -0,0 +1,56 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration: finds last colon (searching from end) +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration: finds second-to-last colon using output from first +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test chained memrchr: t1 = memrchr(h, c, len); t2 = memrchr(h, c, t1-h); +// This tests reverse search (last_indexof) and pointer arithmetic + +#define _GNU_SOURCE +#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[256] = {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'; + + // First memrchr: find LAST colon (searching from end) + char *t1 = (char *)memrchr(buf, ':', n); + if (t1) { + // Second memrchr: find second-to-last colon (search from start up to t1) + size_t len_before_t1 = t1 - buf; + char *t2 = (char *)memrchr(buf, ':', len_before_t1); + if (t2) { + // CHECK-GEN: Found two colons (last at + printf("Found two colons (last at %ld, second-to-last at %ld)\n", + (long)(t1 - buf), (long)(t2 - buf)); + } else { + printf("Found one colon (at %ld)\n", (long)(t1 - buf)); + } + } else { + // CHECK-ORIG: No colons + printf("No colons\n"); + } + return 0; +} diff --git a/tests/prefixof.c b/tests/prefixof.c new file mode 100644 index 00000000..0fc00e1d --- /dev/null +++ b/tests/prefixof.c @@ -0,0 +1,69 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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-GEN1 %s +// RUN: %t.uninstrumented %t.out/id-0-0-2 | FileCheck --check-prefix=CHECK-GEN2 %s + +#include +#include +#include +#include + +// Provide simple implementations for uninstrumented build +int prefixof(const char *str, const char *prefix) { + size_t prefix_len = strlen(prefix); + size_t str_len = strlen(str); + if (str_len >= prefix_len && memcmp(str, prefix, prefix_len) == 0) { + return 1; + } + return 0; +} + +int suffixof(const char *str, const char *suffix) { + size_t suffix_len = strlen(suffix); + size_t str_len = strlen(str); + if (str_len >= suffix_len && + memcmp(str + (str_len - suffix_len), suffix, suffix_len) == 0) { + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[256] = {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'; + + if (prefixof(buf, "hello")) { + // CHECK-GEN1: Has prefix + printf("Has prefix\n"); + } else { + // CHECK-ORIG: No prefix + printf("No prefix\n"); + } + + if (suffixof(buf, "world")) { + // CHECK-GEN2: Has suffix + printf("Has suffix\n"); + } else { + // CHECK-ORIG: No suffix + printf("No suffix\n"); + } + + return 0; +} diff --git a/tests/str_mem_mixed_chain.c b/tests/str_mem_mixed_chain.c new file mode 100644 index 00000000..2a577361 --- /dev/null +++ b/tests/str_mem_mixed_chain.c @@ -0,0 +1,59 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration: finds semicolon with strchr +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration: finds last colon before semicolon with memrchr +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test mixed chain: strchr followed by memrchr with bounded length +// t1 = strchr(buf, ';'); // find first semicolon (forward, null-terminated) +// t2 = memrchr(buf, ':', t1-buf); // find LAST colon before the semicolon (backward, bounded) +// This tests combining strchr (indexof) and memrchr (last_indexof with substr) + +#define _GNU_SOURCE +#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[256] = {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'; + + // First: strchr finds first ';' (forward search, null-terminated) + char *t1 = strchr(buf, ';'); + if (t1) { + // Second: memrchr finds LAST ':' before the ';' (backward search, bounded) + size_t len_before_t1 = t1 - buf; + char *t2 = (char *)memrchr(buf, ':', len_before_t1); + if (t2) { + // CHECK-GEN: Found last colon before first semicolon + printf("Found last colon before first semicolon (colon at %ld, semicolon at %ld)\n", + (long)(t2 - buf), (long)(t1 - buf)); + } else { + printf("Found semicolon but no colon before it (semicolon at %ld)\n", + (long)(t1 - buf)); + } + } else { + // CHECK-ORIG: No semicolon + printf("No semicolon\n"); + } + return 0; +} diff --git a/tests/strcat.c b/tests/strcat.c new file mode 100644 index 00000000..f8ac510e --- /dev/null +++ b/tests/strcat.c @@ -0,0 +1,61 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A_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 enum_gep=0" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: strcat concatenates two parts of tainted input, then compare + +#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[256] = {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 prefix[20] = {0}; + char suffix[20] = {0}; + + char *pos = strchr(buf, '_'); + if (pos) { + size_t prefix_len = pos - buf; + strncpy(prefix, buf, prefix_len); + prefix[prefix_len] = '\0'; + strcpy(suffix, pos + 1); + } else { + printf("No _ found in input\n"); + return -1; + } + + // Concatenate the two parts + char result[256] = {0}; + strcpy(result, prefix); + strcat(result, suffix); + + // Compare concatenated result + if (strcmp(result, "deadbeef") == 0) { + // CHECK-GEN: Match found + printf("Match found: %s\n", result); + } else { + // CHECK-ORIG: No match + printf("No match: %s\n", result); + } + return 0; +} diff --git a/tests/strcat_mixed.c b/tests/strcat_mixed.c new file mode 100644 index 00000000..24b5d837 --- /dev/null +++ b/tests/strcat_mixed.c @@ -0,0 +1,60 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A_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 enum_gep=0 debug=1" %fgtest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: strcat concatenates two parts of tainted input, then compare + +#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[256] = {0}; + FILE* fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t n = fread(buf, 1, 20, fp); + fclose(fp); + buf[n] = '\0'; + + char prefix[20] = {0}; + char suffix[20] = {0}; + + char *pos = strchr(buf, '_'); + if (pos) { + *pos = '\0'; + strcpy(prefix, buf); + strcpy(suffix, pos + 1); + } else { + printf("No _ found in input\n"); + return -1; + } + + // Concatenate the two parts + char result[256] = {0}; + strcpy(result, prefix); + strcat(result, suffix); + + // Compare concatenated result + if (strcmp(result, "deadbeef") == 0) { + // CHECK-GEN: Match found + printf("Match found: %s\n", result); + } else { + // CHECK-ORIG: No match + printf("No match: %s\n", result); + } + return 0; +} diff --git a/tests/strchr.c b/tests/strchr.c new file mode 100644 index 00000000..fbac21f7 --- /dev/null +++ b/tests/strchr.c @@ -0,0 +1,40 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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[256] = {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 != NULL) { + // CHECK-GEN: Found colon + printf("Found colon\n"); + } else { + // CHECK-ORIG: No colon + printf("No colon\n"); + } + return 0; +} diff --git a/tests/strchr_chain.c b/tests/strchr_chain.c new file mode 100644 index 00000000..5a1e3201 --- /dev/null +++ b/tests/strchr_chain.c @@ -0,0 +1,50 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration: finds first colon +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration: finds second colon using output from first +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test chained strchr: t1 = strchr(h, c1); t2 = strchr(t1+1, c2); + +#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[256] = {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 *t1 = strchr(buf, ':'); + if (t1) { + char *t2 = strchr(t1 + 1, ':'); + if (t2) { + // CHECK-GEN: Found two colons + printf("Found two colons\n"); + } else { + printf("Found one colon\n"); + } + } else { + // CHECK-ORIG: No colons + printf("No colons\n"); + } + return 0; +} diff --git a/tests/strchr_mixed_chain.c b/tests/strchr_mixed_chain.c new file mode 100644 index 00000000..a484c6cd --- /dev/null +++ b/tests/strchr_mixed_chain.c @@ -0,0 +1,57 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration: finds first colon +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration: finds semicolon before the colon (backward search via pointer chain) +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test mixed chain: strchr (forward) followed by strrchr (backward from result) +// t1 = strrchr(buf, ':'); // find last colon +// t2 = strchr(t1, ';'); // find first semicolon after the colon +// This tests combining last_indexof (strrchr) with indexof (strchr) via pointer chain + +#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[256] = {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'; + + // First: strrchr finds last ':' (backward search) + char *t1 = strrchr(buf, ':'); + if (t1) { + // Second: strchr finds first ';' after the colon (forward search from t1) + char *t2 = strchr(t1, ';'); + if (t2) { + // CHECK-GEN: Found semicolon after colon + printf("Found semicolon after colon (colon at %ld, semicolon at %ld)\n", + (long)(t1 - buf), (long)(t2 - buf)); + } else { + printf("Found colon but no semicolon after it (colon at %ld)\n", + (long)(t1 - buf)); + } + } else { + // CHECK-ORIG: No colon + printf("No colon\n"); + } + return 0; +} diff --git a/tests/strdup.c b/tests/strdup.c new file mode 100644 index 00000000..df67cb31 --- /dev/null +++ b/tests/strdup.c @@ -0,0 +1,51 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 + +// Test: strdup duplicates tainted string, then compare the duplicate + +#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[256] = {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'; + + // Duplicate the string + char *dup = strdup(buf); + if (!dup) { + fprintf(stderr, "strdup failed\n"); + return -1; + } + + // Compare the duplicate + if (strcmp(dup, "secret") == 0) { + // CHECK-GEN: Match found + printf("Match found: %s\n", dup); + } else { + // CHECK-ORIG: No match + printf("No match: %s\n", dup); + } + + free(dup); + return 0; +} diff --git a/tests/strlen_extend.c b/tests/strlen_extend.c new file mode 100644 index 00000000..7112d447 --- /dev/null +++ b/tests/strlen_extend.c @@ -0,0 +1,37 @@ +// Test strlen extending - input needs to grow to reach target length +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: printf 'short\0' > %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 + +int main(int argc, char **argv) { + if (argc < 2) return 1; + + char buf[64]; + FILE *f = fopen(argv[1], "rb"); + if (!f) return 1; + + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + fclose(f); + + size_t len = strlen(buf); + printf("strlen: %zu\n", len); + + if (len == 15) { + // CHECK-GEN: SUCCESS + printf("SUCCESS: strlen == 15\n"); + } else { + // CHECK-ORIG: NOT-15 + printf("NOT-15: strlen = %zu\n", len); + } + + return 0; +} diff --git a/tests/strlen_json3.c b/tests/strlen_json3.c new file mode 100644 index 00000000..93a5fdd4 --- /dev/null +++ b/tests/strlen_json3.c @@ -0,0 +1,67 @@ +// Test: strlen constraint in JSON context +// When shrinking, DELETE should remove bytes so JSON remains valid +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: printf '{"name":"HELLO WORLD HELLO WORLD","age":25}' > %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-2 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + char buf[256]; + FILE *f = fopen(argv[1], "r"); + if (!f) { + perror("fopen"); + return 1; + } + + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + + printf("Input: %s\n", buf); + + // Find name value + char *start = strstr(buf, "\"name\":\""); + if (!start) { + printf("Field not found\n"); + return 0; + } + + start += 8; // skip "name":" + + // Find closing quote + char *end = strchr(start, '"'); + if (!end) { + printf("Malformed JSON - no closing quote\n"); + return 0; + } + + // Temporarily null-terminate for strlen + *end = '\0'; + size_t len = strlen(start); + *end = '"'; + + printf("Name value: \"%.*s\" (len=%zu)\n", (int)len, start, len); + + if (len == 5) { + // CHECK-GEN: SUCCESS + printf("SUCCESS: Found name with exactly 5 chars!\n"); + } else { + // CHECK-ORIG: NOT-5 + printf("NOT-5: len = %zu\n", len); + } + + return 0; +} diff --git a/tests/strlen_null_from_input.c b/tests/strlen_null_from_input.c new file mode 100644 index 00000000..c389d29b --- /dev/null +++ b/tests/strlen_null_from_input.c @@ -0,0 +1,40 @@ +// Test strlen with null terminator from input file +// The input has embedded null, so strlen stops there +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: printf 'HELLO WORLD\0extra' > %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 + +int main(int argc, char **argv) { + if (argc < 2) return 1; + + char buf[64]; + FILE *f = fopen(argv[1], "rb"); + if (!f) return 1; + + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + // Don't add null - rely on the null from input + fclose(f); + + // Only call strlen if we know there's a null in the buffer + size_t len = strlen(buf); + printf("strlen: %zu\n", len); + + if (len == 5) { + // CHECK-GEN: SUCCESS + printf("SUCCESS: strlen == 5\n"); + } else { + // CHECK-ORIG: NOT-5 + printf("NOT-5: strlen = %zu\n", len); + } + + return 0; +} diff --git a/tests/strlen_shrink.c b/tests/strlen_shrink.c new file mode 100644 index 00000000..3ee923f0 --- /dev/null +++ b/tests/strlen_shrink.c @@ -0,0 +1,52 @@ +// Test: shrinking strlen by deleting bytes +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: printf 'HELLO WORLD' > %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 + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + char buf[64]; + FILE *f = fopen(argv[1], "r"); + if (!f) { + perror("fopen"); + return 1; + } + + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + + // This is the strlen we're solving + size_t len = strlen(buf); + printf("strlen returned: %zu\n", len); + + // Show what bytes are actually in the buffer + printf("Buffer contents (hex): "); + for (size_t i = 0; i < 15 && i < n; i++) { + printf("%02x ", (unsigned char)buf[i]); + } + printf("\n"); + + if (len == 5) { + // CHECK-GEN: SUCCESS + printf("SUCCESS: Found input with strlen=5\n"); + } else { + // CHECK-ORIG: NOT-5 + printf("NOT-5: strlen=%zu\n", len); + } + + return 0; +} diff --git a/tests/strlen_test.c b/tests/strlen_test.c new file mode 100644 index 00000000..d22b4012 --- /dev/null +++ b/tests/strlen_test.c @@ -0,0 +1,48 @@ +// Test: strlen constraints for various length comparisons +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: printf 'HELLO WORLD' > %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_DONT_OPTIMIZE=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-GEN1 %s +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN2 %s + +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + char buf[64]; + FILE *f = fopen(argv[1], "r"); + if (!f) { + perror("fopen"); + return 1; + } + size_t n = fread(buf, 1, 63, f); + buf[n] = '\0'; + fclose(f); + + size_t len = strlen(buf); + printf("strlen = %zu\n", len); + + if (len > 10) { + // CHECK-ORIG: Long string + printf("Long string (> 10)!\n"); + } + if (len == 5) { + // CHECK-GEN2: Exact length 5 + printf("Exact length 5!\n"); + } + if (len < 3) { + // CHECK-GEN1: Short string + printf("Short string (< 3)!\n"); + } + return 0; +} diff --git a/tests/strncat.c b/tests/strncat.c new file mode 100644 index 00000000..9ba8ace2 --- /dev/null +++ b/tests/strncat.c @@ -0,0 +1,60 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration finds colon +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: strncat with symbolic length from strchr result +// Pattern: find delimiter, append prefix to base, compare result + +#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[256] = {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'; + + // Find colon delimiter - this makes len symbolic + char *sep = strchr(buf, ':'); + if (sep) { + size_t len = sep - buf; // symbolic length + + // Start with a base string + char result[256] = "prefix_"; + + // Append first 'len' bytes of buf to result + // This uses strncat with symbolic n! + strncat(result, buf, len); + + // Compare the concatenated result + if (strcmp(result, "prefix_key") == 0) { + // CHECK-GEN: Match found + printf("Match found: %s\n", result); + } else { + printf("No match: %s (len=%zu)\n", result, len); + } + } else { + // CHECK-ORIG: No colon + printf("No colon found\n"); + } + return 0; +} diff --git a/tests/strncpy_simple.c b/tests/strncpy_simple.c new file mode 100644 index 00000000..527ab27e --- /dev/null +++ b/tests/strncpy_simple.c @@ -0,0 +1,62 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration finds colon, second iteration solves prefix constraint +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-1-1 output_dir=%t.out session_id=2 enum_gep=0" %fgtest %t.fg %t.out/id-0-1-1 +// RUN: %t.uninstrumented %t.out/id-0-2-2 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: strchr to find delimiter, strncpy prefix, check first byte + +#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[256] = {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'; + + // Find colon delimiter + char *sep = strchr(buf, ':'); + if (sep) { + // Extract prefix before the colon + size_t len = sep - buf; + char prefix[20]; + strncpy(prefix, buf, len); + prefix[len] = '\0'; + + if (len > 1) { + // Simple check: first byte equals 'X' + if (prefix[0] == 'X') { + // CHECK-GEN: Found X prefix + printf("Found X prefix before colon\n"); + } else { + // CHECK-COLON: First char is + printf("First char is '%c' (0x%02x), not 'X'\n", prefix[0], (unsigned char)prefix[0]); + } + } else { + printf("Prefix too short\n"); + } + } else { + // CHECK-ORIG: No colon + printf("No colon found\n"); + } + return 0; +} diff --git a/tests/strncpy_substr.c b/tests/strncpy_substr.c new file mode 100644 index 00000000..dc81806c --- /dev/null +++ b/tests/strncpy_substr.c @@ -0,0 +1,75 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration sees colon, solves key='username' +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration solves value='password' +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1 enum_gep=0" %fgtest %t.fg %t.out/id-0-0-0 +// Third iteration checks both key and value constraints +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-1-1 output_dir=%t.out session_id=2 enum_gep=0" %fgtest %t.fg %t.out/id-0-1-1 +// RUN: %t.uninstrumented %t.out/id-0-2-2 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: key-value parsing pattern "key:value" +// strchr finds the delimiter, strncpy extracts key, pointer arithmetic extracts value +// Expectations are placed on both key and value to test symbolic strncpy length + +#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[256] = {0}; + FILE* fp = fopen(argv[1], "rb"); + if (!fp) { + fprintf(stderr, "Failed to open\n"); + return -1; + } + size_t nread = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + buf[nread] = '\0'; + size_t buflen = strlen(buf); + + // Find colon delimiter (key:value separator) + char *sep = strchr(buf, ':'); + if (sep) { + // Extract key (prefix before the colon) + size_t len = sep - buf; + char key[20] = {0}; // Initialize to avoid kInitializingLabel + char value[20] = {0}; + strncpy(key, buf, len); + key[len] = '\0'; + strcpy(value, sep + 1); + + // Check if there's a value part after the colon + // size_t sep_offset = sep - buf; + // if (sep_offset + 1 < buflen) { + // // Safe to copy value part + // size_t value_len = buflen - (sep_offset + 1); + // strncpy(value, sep + 1, value_len); + // value[value_len] = '\0'; + // } + // else: value remains empty string + + // This tests constraints on both parts of the key:value pattern + if (strcmp(key, "username") == 0 && strcmp(value, "password") == 0) { + // CHECK-GEN: Found + printf("Found\n"); + } else { + // BAD + printf("Key='%s' (len=%zu), Value='%s'\n", key, len, value); + } + } else { + // CHECK-ORIG: No colon found + printf("No colon found\n"); + } + return 0; +} diff --git a/tests/strndup.c b/tests/strndup.c new file mode 100644 index 00000000..2f783912 --- /dev/null +++ b/tests/strndup.c @@ -0,0 +1,65 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 +// First iteration finds colon +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %fgtest %t.fg %t.bin +// Second iteration solves key constraint +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: strndup with symbolic length from strchr result +// Pattern: find delimiter, duplicate prefix using strndup, compare + +#define _GNU_SOURCE +#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[256] = {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'; + + // Find colon delimiter - this makes len symbolic + char *sep = strchr(buf, ':'); + if (sep) { + size_t len = sep - buf; // symbolic length + + // Duplicate first 'len' bytes using strndup + // This uses strndup with symbolic n! + char *key = strndup(buf, len); + if (!key) { + fprintf(stderr, "strndup failed\n"); + return -1; + } + + // Compare the duplicated key + if (strcmp(key, "user") == 0) { + // CHECK-GEN: Match found + printf("Match found: key=%s\n", key); + } else { + printf("No match: key=%s (len=%zu)\n", key, len); + } + + free(key); + } else { + // CHECK-ORIG: No colon found + printf("No colon found\n"); + } + return 0; +} diff --git a/tests/strnstr.c b/tests/strnstr.c new file mode 100644 index 00000000..22bacda7 --- /dev/null +++ b/tests/strnstr.c @@ -0,0 +1,58 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %t.bin +// RUN: clang -o %t.uninstrumented %s -lbsd +// 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 +// First iteration finds delimiter +// RUN: env TAINT_OPTIONS="taint_file=%t.out/id-0-0-0 output_dir=%t.out session_id=1" %fgtest %t.fg %t.out/id-0-0-0 +// RUN: %t.uninstrumented %t.out/id-0-1-1 | FileCheck --check-prefix=CHECK-GEN %s + +// Test: strnstr with symbolic length from strchr result +// Pattern: find delimiter, then search for pattern only within prefix before delimiter + +#include +#include +#include +#include + +extern char *strnstr(const char *haystack, const char *needle, size_t len); + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[256] = {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'; + + // Find delimiter - this makes len symbolic + char *sep = strchr(buf, ':'); + if (sep) { + size_t len = sep - buf; // symbolic length + + // Search for "key" only within the first 'len' bytes (before delimiter) + // This tests strnstr with symbolic n parameter + char *found = strnstr(buf, "key", len); + + if (found != NULL) { + // CHECK-GEN: Found key before delimiter + printf("Found key before delimiter at position %ld\n", found - buf); + } else { + printf("No key in first %zu bytes\n", len); + } + } else { + // CHECK-ORIG: No delimiter + printf("No delimiter found\n"); + } + return 0; +} diff --git a/tests/strpbrk.c b/tests/strpbrk.c new file mode 100644 index 00000000..f90089a9 --- /dev/null +++ b/tests/strpbrk.c @@ -0,0 +1,43 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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 + +// Test strpbrk: find first character from a set + +#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[256] = {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'; + + // Find first occurrence of any of ':' or ';' or '=' + char *t1 = strpbrk(buf, ":;="); + if (t1) { + // CHECK-GEN: Found delimiter + printf("Found delimiter '%c' at position %ld\n", *t1, (long)(t1 - buf)); + } else { + // CHECK-ORIG: No delimiter + printf("No delimiter\n"); + } + return 0; +} diff --git a/tests/strrchr.c b/tests/strrchr.c new file mode 100644 index 00000000..a707b8a3 --- /dev/null +++ b/tests/strrchr.c @@ -0,0 +1,40 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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[256] = {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 = strrchr(buf, '/'); + if (p != NULL) { + // CHECK-GEN: Found slash + printf("Found slash\n"); + } else { + // CHECK-ORIG: No slash + printf("No slash\n"); + } + return 0; +} diff --git a/tests/strstr.c b/tests/strstr.c new file mode 100644 index 00000000..fde870ee --- /dev/null +++ b/tests/strstr.c @@ -0,0 +1,39 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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[256] = {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'; + + if (strstr(buf, "magic") != NULL) { + // CHECK-GEN: Found magic + printf("Found magic\n"); + } else { + // CHECK-ORIG: No magic + printf("No magic\n"); + } + return 0; +} diff --git a/tests/strsub.c b/tests/strsub.c new file mode 100644 index 00000000..e9478b89 --- /dev/null +++ b/tests/strsub.c @@ -0,0 +1,74 @@ +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'print("A"*20)' > %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-3 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include + +// Implementation matching xmlStrsub signature +// Extracts substring starting at 'start' with length 'len' +char *xmlStrsub(const char *str, int start, int len) { + if (str == NULL) return NULL; + if (start < 0) return NULL; + if (len < 0) return NULL; + + // Skip to start position + int i; + for (i = 0; i < start; i++) { + if (*str == 0) return NULL; + str++; + } + if (*str == 0) return NULL; + + // Duplicate len characters (like xmlStrndup) + size_t actual_len = strlen(str); + if ((size_t)len > actual_len) len = actual_len; + + char *ret = (char *)malloc(len + 1); + if (ret == NULL) return NULL; + memcpy(ret, str, len); + ret[len] = '\0'; + return ret; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[256] = {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'; + + // Extract substring from position 5, length 5 + // If input is "xxxxxhello...", substr should be "hello" + char *sub = xmlStrsub(buf, 5, 5); + if (sub != NULL) { + if (strcmp(sub, "hello") == 0) { + // CHECK-GEN: Found hello + printf("Found hello\n"); + } else { + // CHECK-ORIG: No match + printf("No match\n"); + } + free(sub); + } else { + printf("Substr failed\n"); + } + + return 0; +}