diff --git a/compiler/ko_clang.c b/compiler/ko_clang.c index 47aa6fc8..2ddc4d2e 100644 --- a/compiler/ko_clang.c +++ b/compiler/ko_clang.c @@ -259,9 +259,11 @@ static void add_taint_pass() { alloc_printf("-taint-abilist=%s/zlib_abilist.txt", obj_path); } - if (getenv("KO_TRACE_FP")) { + // Floating-point tracing is on by default (ClTraceFP defaults to true). + // KO_NO_TRACE_FP explicitly disables it. + if (getenv("KO_NO_TRACE_FP")) { cc_params[cc_par_cnt++] = "-mllvm"; - cc_params[cc_par_cnt++] = "-taint-trace-float-pointer"; + cc_params[cc_par_cnt++] = "-taint-trace-float-pointer=false"; } if (getenv("KO_NO_TRACE_BOUND")) { diff --git a/driver/CMakeLists.txt b/driver/CMakeLists.txt index 0a2b4c07..1c80629a 100644 --- a/driver/CMakeLists.txt +++ b/driver/CMakeLists.txt @@ -18,6 +18,35 @@ target_link_libraries(FGTest PRIVATE ) install (TARGETS FGTest DESTINATION ${SYMSAN_BIN_DIR}) +## standalone driver for testing the RGD (out-of-process) solver path +add_executable(AFLTest afltest.cpp) +set_target_properties(AFLTest PROPERTIES OUTPUT_NAME "afltest" CXX_STANDARD 17) +target_include_directories(AFLTest PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime +) +target_link_libraries(AFLTest PRIVATE + launcher + rgd-parser + rgd-solver + ${Z3_LIBRARY} + rt +) +install (TARGETS AFLTest DESTINATION ${SYMSAN_BIN_DIR}) + +## standalone SMT-LIB2 front-end for the RGD jigsaw solver (bridges smtlib2 -> +## rgd::SearchTask directly, no target execution / launcher / parser needed) +add_executable(SMTTest smttest.cpp) +set_target_properties(SMTTest PROPERTIES OUTPUT_NAME "smttest" CXX_STANDARD 17) +target_include_directories(SMTTest PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime +) +target_link_libraries(SMTTest PRIVATE + rgd-solver + ${Z3_LIBRARY} + rt +) +install (TARGETS SMTTest DESTINATION ${SYMSAN_BIN_DIR}) + if (DEFINED AFLPP_PATH) add_subdirectory(aflpp) endif() diff --git a/driver/afltest.cpp b/driver/afltest.cpp new file mode 100644 index 00000000..e6537bbd --- /dev/null +++ b/driver/afltest.cpp @@ -0,0 +1,360 @@ +// Standalone driver for testing the RGD (out-of-process) solver path. +// +// This is the RGD-path counterpart to fgtest.cpp: where fgtest drives the +// in-process z3 stack (parse-z3.h / z3-ts.cpp via symsan::Z3ParserSolver), this +// driver exercises the RGD stack that the AFL++ custom mutator (driver/aflpp) +// uses -- parsers/rgd-parser.cpp (rgd::RGDAstParser -> rgd::AstNode) feeding the +// rgd::Solver chain (I2SSolver, optional JITSolver, optional Z3Solver). It lets +// us validate the RGD path deterministically on a single input, without the +// noisy afl-fuzz search loop. +// +// Usage: afltest target input +// TAINT_OPTIONS="taint_file= output_dir=" +// SYMSAN_USE_JIGSAW=1 add the jigsaw JIT solver to the chain +// SYMSAN_USE_Z3=1 add the z3 solver to the chain (needed for FP) +// SYMSAN_USE_NESTED=1 enable nested constraint solving in the parser +// +// Solved inputs are written to /id---, one per +// solved task, mirroring fgtest's output naming so the lit tests can reuse the +// same CHECK-GEN pattern. + +#include "defs.h" +#include "debug.h" +#include "version.h" + +#include "dfsan/dfsan.h" + +#include "ast.h" +#include "task.h" +#include "solver.h" + +extern "C" { +#include "launch.h" +} + +#include "parse-rgd.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace __dfsan; + +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) + +#undef AOUT +# define AOUT(...) \ + do { \ + printf(__VA_ARGS__); \ + } while(false) + +// for input +static char *input_buf; +static size_t input_size; + +// for output +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 disabled by default + +// the mapped union table (shared with the launched target) +static dfsan_label_info *__dfsan_label_info; +static const size_t MAX_LABEL = uniontable_size / sizeof(dfsan_label_info); + +// the RGD parser and the solver chain (matching driver/aflpp/symsan.cpp) +static rgd::RGDAstParser *__parser = nullptr; +static std::vector> __solvers; + +// output buffer for solved inputs (RGD solvers write the full mutated buffer) +static uint8_t *__output_buf = nullptr; +static size_t __output_buf_size = 0; + +// the i2s solver calls the global __dfsan::get_label_info, so define it here +dfsan_label_info* __dfsan::get_label_info(dfsan_label label) { + if (unlikely(label >= MAX_LABEL)) { + throw std::out_of_range("label too large " + std::to_string(label)); + } + return &__dfsan_label_info[label]; +} + +static void generate_input(const uint8_t *buf, size_t size) { + 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 | O_TRUNC, S_IRUSR | S_IWUSR); + if (fd == -1) { + AOUT("failed to open new input file for write\n"); + return; + } + + AOUT("generate #%d output (size: %zu -> %zu)\n", + __current_index - 1, input_size, size); + + if (write(fd, buf, size) == -1) { + AOUT("failed to write new input\n"); + } + + close(fd); +} + +// run a solving task through the solver chain; on the first SAT result write +// out the mutated input. Mirrors the per-solver fall-through in aflpp's +// afl_custom_fuzz (i2s -> jigsaw -> z3), but drives it synchronously. +static void solve_task(rgd::task_t task, void *addr) { + if (!task) return; + for (auto &solver : __solvers) { + size_t new_size = 0; + auto ret = solver->solve(task, (uint8_t*)input_buf, input_size, + __output_buf, new_size); + if (ret == rgd::SOLVER_SAT) { + AOUT("task solved\n"); + generate_input(__output_buf, new_size); + return; + } else if (ret == rgd::SOLVER_UNSAT) { + // deemed unsolvable, no point trying the remaining solvers + AOUT("task not solvable @%p\n", addr); + return; + } + // SOLVER_TIMEOUT / SOLVER_ERROR: fall through to the next solver + } + AOUT("task not solved @%p\n", addr); +} + +static void __solve_cond(dfsan_label label, uint8_t r, bool add_nested, void *addr) { + + AOUT("solving label %d = %d, add_nested: %d\n", label, r, add_nested); + std::vector tasks; + if (__parser->parse_cond(label, r != 0, add_nested, tasks)) { + AOUT("WARNING: failed to parse condition %d @%p\n", label, addr); + return; + } + + for (auto id : tasks) { + auto task = __parser->retrieve_task(id); + solve_task(task, addr); + } +} + +static void __handle_gep(dfsan_label ptr_label, uptr ptr, + dfsan_label index_label, int64_t index, + uint64_t num_elems, uint64_t elem_size, + int64_t current_offset, void* addr) { + + AOUT("tainted GEP index: %ld = %d, ne: %ld, es: %ld, offset: %ld\n", + index, index_label, num_elems, elem_size, current_offset); + + std::vector tasks; + if (__parser->parse_gep(ptr_label, ptr, index_label, index, num_elems, + elem_size, current_offset, __enum_gep, tasks)) { + AOUT("WARNING: failed to parse gep %d @%p\n", index_label, addr); + return; + } + + for (auto id : tasks) { + auto task = __parser->retrieve_task(id); + solve_task(task, addr); + } +} + +int main(int argc, char* const argv[]) { + + if (argc != 3) { + fprintf(stderr, "Usage: %s target input\n", argv[0]); + exit(1); + } + + char *program = argv[1]; + char *input = argv[2]; + + int is_stdin = 0; + int debug = 0; + char *options = getenv("TAINT_OPTIONS"); + if (options) { + // setup output dir + char *output = strstr(options, "output_dir="); + if (output) { + output += 11; // skip "output_dir=" + char *end = strchr(output, ':'); // try ':' first, then ' ' + if (end == NULL) end = strchr(output, ' '); + size_t n = end == NULL? strlen(output) : (size_t)(end - output); + __output_dir = strndup(output, n); + } + + // check if input is stdin + char *taint_file = strstr(options, "taint_file="); + if (taint_file) { + taint_file += strlen("taint_file="); // skip "taint_file=" + char *end = strchr(taint_file, ':'); + if (end == NULL) end = strchr(taint_file, ' '); + size_t n = end == NULL? strlen(taint_file) : (size_t)(end - taint_file); + if (n == 5 && !strncmp(taint_file, "stdin", 5)) + is_stdin = 1; + } + + // check for debug + char *debug_opt = strstr(options, "debug="); + if (debug_opt) { + debug_opt += strlen("debug="); // skip "debug=" + if (strncmp(debug_opt, "1", 1) == 0 || strncmp(debug_opt, "true", 4) == 0) + debug = 1; + } + } + + // enable nested solving in the parser? + bool nested = getenv("SYMSAN_USE_NESTED") != nullptr; + + // load input file + struct stat st; + int input_fd = open(input, O_RDONLY); + if (input_fd == -1) { + fprintf(stderr, "Failed to open input file: %s\n", strerror(errno)); + exit(1); + } + fstat(input_fd, &st); + input_size = st.st_size; + input_buf = (char *)mmap(NULL, input_size, PROT_READ, MAP_PRIVATE, input_fd, 0); + if (input_buf == (void *)-1) { + fprintf(stderr, "Failed to map input file: %s\n", strerror(errno)); + exit(1); + } + + // allocate the output buffer. RGD solvers write the full (possibly grown) + // input; give some headroom for INSERT-style solutions. + __output_buf_size = input_size + 4096; + __output_buf = (uint8_t *)malloc(__output_buf_size); + if (!__output_buf) { + fprintf(stderr, "Failed to alloc output buffer\n"); + exit(1); + } + + // setup launcher + void *shm_base = symsan_init(program, uniontable_size); + if (shm_base == (void *)-1) { + fprintf(stderr, "Failed to map shm: %s\n", strerror(errno)); + exit(1); + } + __dfsan_label_info = (dfsan_label_info *)shm_base; + + if (symsan_set_input(is_stdin ? "stdin" : input) != 0) { + fprintf(stderr, "Failed to set input\n"); + exit(1); + } + + char* args[3]; + args[0] = program; + args[1] = input; + args[2] = NULL; + if (symsan_set_args(2, args) != 0) { + fprintf(stderr, "Failed to set args\n"); + exit(1); + } + + symsan_set_debug(debug); + symsan_set_bounds_check(1); + + // launch the target + int ret = symsan_run(input_fd); + if (ret < 0) { + fprintf(stderr, "Failed to launch target: %s\n", strerror(errno)); + exit(1); + } else if (ret > 0) { + fprintf(stderr, "SymSan launch error %d\n", ret); + exit(1); + } + close(input_fd); + + // setup the RGD parser and the solver chain (matching driver/aflpp) + __parser = new rgd::RGDAstParser(shm_base, uniontable_size, nested); + std::vector inputs; + inputs.push_back({(uint8_t*)input_buf, input_size}); + if (__parser->restart(inputs) != 0) { + fprintf(stderr, "Failed to restart parser\n"); + exit(1); + } + + // always use the simple i2s solver first + __solvers.emplace_back(std::make_shared()); + if (getenv("SYMSAN_USE_JIGSAW")) + __solvers.emplace_back(std::make_shared()); + if (getenv("SYMSAN_USE_Z3")) + __solvers.emplace_back(std::make_shared()); + + pipe_msg msg; + gep_msg gmsg; + dfsan_label_info *info; + size_t msg_size; + memcmp_msg *mmsg = nullptr; + + while (symsan_read_event(&msg, sizeof(msg), 0) > 0) { + // solve constraints + switch (msg.msg_type) { + case cond_type: + __solve_cond(msg.label, msg.result, msg.flags & F_ADD_CONS, (void*)msg.addr); + break; + case gep_type: + if (symsan_read_event(&gmsg, sizeof(gmsg), 0) != sizeof(gmsg)) { + fprintf(stderr, "Failed to receive gep msg: %s\n", strerror(errno)); + break; + } + // double check + if (msg.label != gmsg.index_label) { + fprintf(stderr, "Incorrect gep msg: %d vs %d\n", msg.label, gmsg.index_label); + break; + } + __handle_gep(gmsg.ptr_label, gmsg.ptr, gmsg.index_label, gmsg.index, + gmsg.num_elems, gmsg.elem_size, gmsg.current_offset, (void*)msg.addr); + break; + case memcmp_type: + if (msg.label == 0 || msg.label >= MAX_LABEL) { + fprintf(stderr, "Invalid memcmp label: %d\n", msg.label); + break; + } + info = get_label_info(msg.label); + // if both operands are symbolic, no content to be read + if (info->l1 != CONST_LABEL && info->l2 != CONST_LABEL) + break; + msg_size = sizeof(memcmp_msg) + msg.result; + mmsg = (memcmp_msg*)malloc(msg_size); + if (symsan_read_event(mmsg, msg_size, 0) != msg_size) { + fprintf(stderr, "Failed to receive memcmp msg: %s\n", strerror(errno)); + free(mmsg); + break; + } + // double check + if (msg.label != mmsg->label) { + fprintf(stderr, "Incorrect memcmp msg: %d vs %d\n", msg.label, mmsg->label); + free(mmsg); + break; + } + // save the content + __parser->record_memcmp(msg.label, mmsg->content, msg.result); + free(mmsg); + break; + case add_constraint_type: + __parser->add_constraints(msg.label, msg.result); + break; + default: + break; + } + } + + // destroy the solvers (and their z3 solver members) here, while the global + // z3 context in z3-solver.cpp is still alive -- relying on static destruction + // order across translation units would free the context first and crash. + __solvers.clear(); + + symsan_destroy(); + exit(0); +} diff --git a/driver/smttest.cpp b/driver/smttest.cpp new file mode 100644 index 00000000..82d0a761 --- /dev/null +++ b/driver/smttest.cpp @@ -0,0 +1,1454 @@ +// Standalone SMT-LIB2 front-end for the RGD jigsaw (JIT + gradient-descent) solver. +// +// SymSan's jigsaw solver is normally fed by the concolic engine: the runtime +// records a dataflow graph, parsers/rgd-parser.cpp lifts it into rgd::AstNode / +// rgd::SearchTask, and solvers/jit-solver.cpp JITs each constraint and runs +// gradient descent (solvers/jigsaw/gd.cc). This driver bypasses the runtime and +// builds the very same SearchTask directly from an SMT-LIB2 file, so jigsaw can be +// exercised as a standalone solver. +// +// Jigsaw is an INCOMPLETE local-search solver: it can find a model for a +// satisfiable conjunction of constraints (gradient descent drives every +// constraint's distance to 0), but it can NEVER prove unsatisfiability and it +// rejects boolean structure it cannot turn into a conjunction of comparisons. +// This driver is therefore honest about its limits: +// - it prints "sat" + a model only when a sound solver returns SOLVER_SAT; +// - it prints "unknown" for everything else (timeout, unsupported input, or a +// genuinely unsatisfiable instance -- we cannot tell these apart). +// It never prints "unsat". +// +// Supported fragment (QF_BV / QF_FP / QF_BVFP satisfiable subset): +// - sorts: (_ BitVec N), (_ FloatingPoint 8 24)=Float32, (_ FloatingPoint 11 53) +// =Float64 (and the Float32/Float64 aliases); +// - commands: set-*/declare-const/declare-fun/define-fun (0-arg)/assert/ +// check-sat/get-model/exit; +// - assertions: a comparison, (and ...) of comparisons, (not ), and +// let-bindings; anything else (or / => / xor / ite / nested boolean) makes the +// whole query "unknown"; +// - BV ops: bvadd/sub/mul/udiv/sdiv/urem/srem/neg/not/and/or/xor/shl/lshr/ashr, +// concat, (_ extract i j), (_ zero_extend k), (_ sign_extend k); +// - FP ops: fp.add/sub/mul/div/rem/neg/abs/sqrt/min/max/roundToIntegral; +// - predicates: = distinct bvult bvule bvugt bvuge bvslt bvsle bvsgt bvsge +// fp.eq fp.lt fp.leq fp.gt fp.geq (= on FP sorts is bitwise equality). +// +// Usage: smttest [--z3] [--no-jigsaw] [--time] file.smt2 +// --z3 also try the (complete, FP-aware) z3 solver after jigsaw +// --no-jigsaw skip jigsaw (useful with --z3 as a reference oracle) +// --time print a "TIME parse=.. codegen=.. jit=.. gd=.. solve=.. total=.." +// line (microseconds) to stderr; codegen/jit/gd are the jigsaw- +// internal split of solve. +// Env SMT_USE_Z3 / SMT_NO_JIGSAW / SMT_TIME mirror the flags. + +#include "ast.h" +#include "task.h" +#include "solver.h" + +// Phase-0 spike entry (defined in solvers/jigsaw/jit.cc); forward-declared here +// to avoid pulling the jigsaw JIT headers into the front-end. +namespace rgd { int spike_fp_rounding(); } + +#include "dfsan/dfsan.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace rgd; + +#define DEBUG 0 +#if DEBUG +#define DBG(...) do { fprintf(stderr, __VA_ARGS__); } while (0) +#else +#define DBG(...) do { } while (0) +#endif + +// The i2s solver (part of librgd-solver) references the global +// __dfsan::get_label_info. This driver never runs i2s (jigsaw builds its AST +// directly from SMT-LIB2, not from a union table), but the symbol must resolve +// for the link to succeed; provide a stub that fails loudly if ever reached. +namespace __dfsan { +dfsan_label_info* get_label_info(dfsan_label label) { + (void)label; + throw std::runtime_error("get_label_info is not available in smttest"); +} +} // namespace __dfsan + +namespace { + +// ------------------------------------------------------------------------- +// S-expression reader +// ------------------------------------------------------------------------- + +struct SExpr { + bool is_atom = false; + std::string atom; + std::vector list; +}; + +// thrown for any construct outside the supported fragment; caught at the top and +// turned into an "unknown" result. +struct Unsupported { + std::string msg; +}; + +struct ParseError { + std::string msg; +}; + +class SexpReader { +public: + explicit SexpReader(const std::string &s) : s_(s) {} + + bool eof() { + skip_ws(); + return pos_ >= s_.size(); + } + + SExpr read() { + skip_ws(); + if (pos_ >= s_.size()) throw ParseError{"unexpected end of input"}; + char c = s_[pos_]; + if (c == '(') { + ++pos_; + SExpr e; + e.is_atom = false; + while (true) { + skip_ws(); + if (pos_ >= s_.size()) throw ParseError{"unterminated list"}; + if (s_[pos_] == ')') { ++pos_; break; } + e.list.push_back(read()); + } + return e; + } else if (c == ')') { + throw ParseError{"unexpected ')'"}; + } else if (c == '|') { + // quoted symbol: everything up to the matching '|' + size_t start = ++pos_; + while (pos_ < s_.size() && s_[pos_] != '|') ++pos_; + if (pos_ >= s_.size()) throw ParseError{"unterminated | symbol"}; + SExpr e; + e.is_atom = true; + e.atom = s_.substr(start, pos_ - start); + ++pos_; // consume closing '|' + return e; + } else { + size_t start = pos_; + while (pos_ < s_.size()) { + char d = s_[pos_]; + if (std::isspace((unsigned char)d) || d == '(' || d == ')' || d == ';') + break; + ++pos_; + } + SExpr e; + e.is_atom = true; + e.atom = s_.substr(start, pos_ - start); + return e; + } + } + +private: + void skip_ws() { + while (pos_ < s_.size()) { + char c = s_[pos_]; + if (c == ';') { // line comment + while (pos_ < s_.size() && s_[pos_] != '\n') ++pos_; + } else if (std::isspace((unsigned char)c)) { + ++pos_; + } else { + break; + } + } + } + + const std::string &s_; + size_t pos_ = 0; +}; + +// ------------------------------------------------------------------------- +// numeric-literal helpers +// ------------------------------------------------------------------------- + +// parse a #x.. / #b.. bit-vector literal into (value, width_bits). Values wider +// than 64 bits are rejected (the jigsaw input array stores one uint64 per slot). +static bool parse_bits_literal(const std::string &tok, uint64_t &value, + uint32_t &width) { + if (tok.size() < 3 || tok[0] != '#') return false; + if (tok[1] == 'x') { + width = (uint32_t)(tok.size() - 2) * 4; + if (width > 64) throw Unsupported{"bit-vector literal wider than 64 bits"}; + value = 0; + for (size_t i = 2; i < tok.size(); ++i) { + char c = tok[i]; + uint64_t d; + if (c >= '0' && c <= '9') d = c - '0'; + else if (c >= 'a' && c <= 'f') d = 10 + (c - 'a'); + else if (c >= 'A' && c <= 'F') d = 10 + (c - 'A'); + else throw ParseError{"bad hex literal " + tok}; + value = (value << 4) | d; + } + return true; + } else if (tok[1] == 'b') { + width = (uint32_t)(tok.size() - 2); + if (width > 64) throw Unsupported{"bit-vector literal wider than 64 bits"}; + value = 0; + for (size_t i = 2; i < tok.size(); ++i) { + char c = tok[i]; + if (c != '0' && c != '1') throw ParseError{"bad binary literal " + tok}; + value = (value << 1) | (uint64_t)(c - '0'); + } + return true; + } + return false; +} + +static bool parse_uint(const std::string &tok, uint64_t &out) { + if (tok.empty()) return false; + uint64_t v = 0; + for (char c : tok) { + if (c < '0' || c > '9') return false; + v = v * 10 + (uint64_t)(c - '0'); + } + out = v; + return true; +} + +// reinterpret a double/float as its IEEE-754 bit pattern of the given width. +static uint64_t fp_to_bits(double d, uint32_t width) { + if (width == 32) { + float f = (float)d; + uint32_t u; + std::memcpy(&u, &f, sizeof(u)); + return u; + } + uint64_t u; + std::memcpy(&u, &d, sizeof(u)); + return u; +} + +// ------------------------------------------------------------------------- +// SMT-LIB2 -> RGD SearchTask translator +// ------------------------------------------------------------------------- + +struct VarInfo { + uint32_t offset; // byte offset in the virtual input buffer + uint32_t bits; // declared width + bool is_fp; +}; + +struct TermInfo { + uint16_t bits; + bool is_fp; +}; + +using Env = std::map; + +// A boolean formula is normalized to DNF: a disjunction of clauses, where each +// clause is a conjunction of relational Literals. This mirrors the RGD parser's +// to_nnf + to_dnf pipeline (parsers/rgd-parser.cpp): negations are pushed to the +// comparison leaves (carried in Literal::negate) and every disjunction becomes a +// separate clause, i.e. a separate SearchTask. The formula is SAT iff ANY clause +// (task) is SAT; UNSAT iff EVERY clause is proven UNSAT. +struct Literal { + const SExpr *e; // the relational sub-expression (a comparison) + bool negate; // whether this occurrence is negated + Env env; // let-binding environment active at this occurrence +}; +using Clause = std::vector; // conjunction of literals +using Formula = std::vector; // disjunction of clauses (DNF) + +// Guard against DNF blow-up from many conjoined disjunctions (cartesian product). +static constexpr size_t MAX_CLAUSES = 4096; + +class Translator { +public: + std::map vars; + std::vector var_order; // declaration order, for model printing + std::map defines; // 0-arg define-fun bodies + std::map sort_defines; // 0-arg define-sort aliases + uint32_t total_bytes = 0; + + task_t task = std::make_shared(); + // running DNF of the conjunction of all assertions; starts as {{}} == true. + Formula pending_ = Formula{Clause{}}; + std::vector tasks; // one SearchTask per DNF clause (built at the end) + bool trivial_sat = false; // an empty clause appeared: unconditionally SAT + + // process a top-level command; returns false to stop (exit) + void run_command(const SExpr &cmd) { + if (cmd.is_atom || cmd.list.empty() || !cmd.list[0].is_atom) + throw ParseError{"malformed command"}; + const std::string &head = cmd.list[0].atom; + if (head == "declare-const") { + // (declare-const name sort) + if (cmd.list.size() != 3) throw ParseError{"bad declare-const"}; + declare_var(cmd.list[1].atom, cmd.list[2]); + } else if (head == "declare-fun") { + // (declare-fun name () sort) + if (cmd.list.size() != 4) throw ParseError{"bad declare-fun"}; + if (!cmd.list[2].is_atom && !cmd.list[2].list.empty()) + throw Unsupported{"uninterpreted function with arguments"}; + declare_var(cmd.list[1].atom, cmd.list[3]); + } else if (head == "define-fun") { + // (define-fun name () sort body) -- only 0-arg macros are supported + if (cmd.list.size() != 5) throw ParseError{"bad define-fun"}; + if (!(cmd.list[2].is_atom) && !cmd.list[2].list.empty()) + throw Unsupported{"define-fun with arguments"}; + defines[cmd.list[1].atom] = cmd.list[4]; + } else if (head == "define-sort") { + // (define-sort name () sort) -- only 0-arg sort aliases are supported + if (cmd.list.size() != 4) throw ParseError{"bad define-sort"}; + if (!(cmd.list[2].is_atom) && !cmd.list[2].list.empty()) + throw Unsupported{"define-sort with parameters"}; + sort_defines[cmd.list[1].atom] = cmd.list[3]; + } else if (head == "assert") { + if (cmd.list.size() != 2) throw ParseError{"bad assert"}; + Env env; + // Normalize the assertion to DNF and conjoin it with the running formula. + Formula f = to_dnf(cmd.list[1], env, /*negate=*/false); + pending_ = and_formula(pending_, f); + } + // set-logic / set-info / set-option / check-sat / get-model / exit / push / + // pop are all no-ops here: we solve once, after reading the whole file. + } + + // Build one SearchTask per DNF clause from the accumulated formula. Sets + // trivial_sat if some clause is empty (unconditionally true). Populates + // `tasks` with the finalized, non-trivial tasks. Returns false if there is + // nothing to solve (no clauses at all, or everything trivial). + bool build_tasks() { + if (pending_.empty()) return false; // formula reduced to false + for (const auto &clause : pending_) { + if (clause.empty()) { trivial_sat = true; continue; } // true disjunct + task = std::make_shared(); + for (const auto &lit : clause) + emit_comparison(*lit.e, lit.env, lit.negate); + if (task->empty()) { trivial_sat = true; continue; } + task->finalize(); + tasks.push_back(task); + } + return trivial_sat || !tasks.empty(); + } + +private: + // ---- declarations ------------------------------------------------------ + + void declare_var(const std::string &name, const SExpr &sort) { + uint32_t bits; + bool is_fp; + parse_sort(sort, bits, is_fp); + VarInfo vi; + vi.offset = total_bytes; + vi.bits = bits; + vi.is_fp = is_fp; + vars[name] = vi; + var_order.push_back(name); + total_bytes += (bits + 7) / 8; + DBG("declare %s @%u bits=%u fp=%d\n", name.c_str(), vi.offset, bits, is_fp); + } + + void parse_sort(const SExpr &sort, uint32_t &bits, bool &is_fp) { + if (sort.is_atom) { + if (sort.atom == "Float32") { bits = 32; is_fp = true; return; } + if (sort.atom == "Float64") { bits = 64; is_fp = true; return; } + if (sort.atom == "RoundingMode") + throw Unsupported{"RoundingMode sort"}; + // resolve a 0-arg define-sort alias, then re-parse the aliased sort + auto it = sort_defines.find(sort.atom); + if (it != sort_defines.end()) { parse_sort(it->second, bits, is_fp); return; } + throw Unsupported{"sort " + sort.atom}; + } + if (sort.list.size() >= 1 && sort.list[0].is_atom && sort.list[0].atom == "_") { + const std::string &k = sort.list[1].atom; + if (k == "BitVec") { + uint64_t n; + if (!parse_uint(sort.list[2].atom, n)) throw ParseError{"bad BitVec width"}; + bits = (uint32_t)n; + is_fp = false; + return; + } + if (k == "FloatingPoint") { + uint64_t eb, sb; + if (!parse_uint(sort.list[2].atom, eb) || !parse_uint(sort.list[3].atom, sb)) + throw ParseError{"bad FloatingPoint sort"}; + if (eb == 8 && sb == 24) { bits = 32; is_fp = true; return; } + if (eb == 11 && sb == 53) { bits = 64; is_fp = true; return; } + throw Unsupported{"FloatingPoint width other than 32/64"}; + } + } + throw Unsupported{"compound sort"}; + } + + // ---- assertion (boolean) layer ---------------------------------------- + + // Resolve let/define atom substitutions, following the environment. + const SExpr *resolve(const SExpr *e, const Env &env) { + while (e->is_atom) { + auto it = env.find(e->atom); + if (it != env.end()) { e = it->second; continue; } + auto d = defines.find(e->atom); + if (d != defines.end()) { e = &d->second; continue; } + break; + } + return e; + } + + Env bind_let(const SExpr &lets, const Env &env) { + // (let ((a e1) (b e2) ...) body) -- bindings are simultaneous in SMT-LIB, + // but since we only substitute names to sub-expressions (no evaluation), + // extending a copy of the parent env is sufficient. + Env e2 = env; + for (const auto &b : lets.list) { + if (b.list.size() != 2 || !b.list[0].is_atom) throw ParseError{"bad let binding"}; + e2[b.list[0].atom] = &b.list[1]; + } + return e2; + } + + // Conjoin two DNF formulas: (A) AND (B) distributes into the cross product of + // their clauses (mirrors to_dnf's LAnd case in rgd-parser.cpp). + Formula and_formula(const Formula &a, const Formula &b) { + Formula out; + for (const auto &ca : a) { + for (const auto &cb : b) { + Clause c; + c.reserve(ca.size() + cb.size()); + c.insert(c.end(), ca.begin(), ca.end()); + c.insert(c.end(), cb.begin(), cb.end()); + out.push_back(std::move(c)); + if (out.size() > MAX_CLAUSES) throw Unsupported{"DNF too large"}; + } + } + return out; + } + + // Normalize a boolean expression to DNF, pushing negation to the leaves (NNF) + // as we go (the `negate` flag). Boolean connectives and/or/not/=>/xor/ite are + // expanded; everything else is a relational leaf captured as a Literal. This + // is the S-expression analogue of RGDAstParser::to_nnf + to_dnf. + Formula to_dnf(const SExpr &e0, const Env &env, bool negate) { + const SExpr *ep = resolve(&e0, env); + const SExpr &e = *ep; + + if (e.is_atom) { + if (e.atom == "true") + return negate ? Formula{} : Formula{Clause{}}; + if (e.atom == "false") + return negate ? Formula{Clause{}} : Formula{}; + // a bare (0-arg) boolean symbol we can't interpret as a comparison + throw Unsupported{"boolean variable " + e.atom}; + } + if (e.list.empty() || !e.list[0].is_atom) + throw Unsupported{"boolean application"}; + const std::string &op = e.list[0].atom; + + if (op == "not") { + if (e.list.size() != 2) throw ParseError{"bad not"}; + return to_dnf(e.list[1], env, !negate); + } + if (op == "let") { + if (e.list.size() != 3) throw ParseError{"bad let"}; + Env e2 = bind_let(e.list[1], env); + return to_dnf(e.list[2], e2, negate); + } + if (op == "and" || op == "or") { + if (e.list.size() < 2) throw ParseError{"empty and/or"}; + // De Morgan: under negation, and<->or. + bool conj = (op == "and") ^ negate; + if (conj) { // conjunction: cross product of children + Formula acc{Clause{}}; // == true + for (size_t i = 1; i < e.list.size(); ++i) + acc = and_formula(acc, to_dnf(e.list[i], env, negate)); + return acc; + } else { // disjunction: union of children's clauses + Formula acc; // == false + for (size_t i = 1; i < e.list.size(); ++i) { + Formula f = to_dnf(e.list[i], env, negate); + acc.insert(acc.end(), f.begin(), f.end()); + if (acc.size() > MAX_CLAUSES) throw Unsupported{"DNF too large"}; + } + return acc; + } + } + if (op == "=>") { + // (=> a1 a2 ... an) == (or (not a1) ... (not a_{n-1}) an) + if (e.list.size() < 3) throw ParseError{"bad =>"}; + size_t n = e.list.size(); + if (!negate) { // OR of the negated premises and the (positive) conclusion + Formula acc; + for (size_t i = 1; i + 1 < n; ++i) { + Formula f = to_dnf(e.list[i], env, true); + acc.insert(acc.end(), f.begin(), f.end()); + } + Formula last = to_dnf(e.list[n - 1], env, false); + acc.insert(acc.end(), last.begin(), last.end()); + if (acc.size() > MAX_CLAUSES) throw Unsupported{"DNF too large"}; + return acc; + } else { // not(=>) == a1 and ... and a_{n-1} and (not an) + Formula acc{Clause{}}; + for (size_t i = 1; i + 1 < n; ++i) + acc = and_formula(acc, to_dnf(e.list[i], env, false)); + acc = and_formula(acc, to_dnf(e.list[n - 1], env, true)); + return acc; + } + } + if (op == "xor") { + // support 2-ary xor only (n-ary parity blows up without formula negation). + if (e.list.size() != 3) throw Unsupported{"n-ary xor"}; + // xor(a,b) == (a and not b) or (not a and b) + // not xor(a,b) == (a and b) or (not a and not b) + bool nb = !negate; // sign of b in the first conjunct + Formula t1 = and_formula(to_dnf(e.list[1], env, false), + to_dnf(e.list[2], env, nb)); + Formula t2 = and_formula(to_dnf(e.list[1], env, true), + to_dnf(e.list[2], env, !nb)); + t1.insert(t1.end(), t2.begin(), t2.end()); + if (t1.size() > MAX_CLAUSES) throw Unsupported{"DNF too large"}; + return t1; + } + if (op == "ite") { + // boolean ite(c,t,e) == (c and t) or (not c and e) + // not ite(c,t,e) == (c and not t) or (not c and not e) + if (e.list.size() != 4) throw ParseError{"bad ite"}; + Formula br1 = and_formula(to_dnf(e.list[1], env, false), + to_dnf(e.list[2], env, negate)); + Formula br2 = and_formula(to_dnf(e.list[1], env, true), + to_dnf(e.list[3], env, negate)); + br1.insert(br1.end(), br2.begin(), br2.end()); + if (br1.size() > MAX_CLAUSES) throw Unsupported{"DNF too large"}; + return br1; + } + + // otherwise: a relational leaf. Capture it (with its negation sign and the + // active let-environment) as a single-literal clause. + return Formula{Clause{Literal{&e, negate, env}}}; + } + + // map a predicate name to its AstKind (integer relational or FP relational). + // returns Bool for "not a predicate". + uint16_t predicate_kind(const std::string &op, bool operands_fp) { + if (op == "=") return operands_fp ? Equal : Equal; // bitwise equality either way + if (op == "distinct") return Distinct; + if (op == "bvult") return Ult; + if (op == "bvule") return Ule; + if (op == "bvugt") return Ugt; + if (op == "bvuge") return Uge; + if (op == "bvslt") return Slt; + if (op == "bvsle") return Sle; + if (op == "bvsgt") return Sgt; + if (op == "bvsge") return Sge; + if (op == "fp.eq") return FOeq; + if (op == "fp.lt") return FOlt; + if (op == "fp.leq") return FOle; + if (op == "fp.gt") return FOgt; + if (op == "fp.geq") return FOge; + return Bool; + } + + void emit_comparison(const SExpr &e0, const Env &env, bool negate) { + const SExpr *ep = resolve(&e0, env); + const SExpr &e = *ep; + if (e.is_atom || e.list.empty() || !e.list[0].is_atom) + throw Unsupported{"non-comparison in boolean position"}; + const std::string &op = e.list[0].atom; + if (op == "not") { // double negation + if (e.list.size() != 2) throw ParseError{"bad not"}; + emit_comparison(e.list[1], env, !negate); + return; + } + if (op == "let") { + if (e.list.size() != 3) throw ParseError{"bad let"}; + Env e2 = bind_let(e.list[1], env); + // re-dispatch the body as a comparison (still under the same negate flag) + if (negate) { + // wrap: translate body as comparison, negated + emit_comparison(e.list[2], e2, true); + } else { + emit_comparison(e.list[2], e2, false); + } + return; + } + + // determine whether the operands are FP (affects '=' meaning only cosmetically) + bool operands_fp = false; + if (e.list.size() >= 2) operands_fp = looks_fp(e.list[1], env); + + uint16_t kind = predicate_kind(op, operands_fp); + if (kind == Bool) throw Unsupported{"predicate '" + op + "'"}; + + // n-ary '=' / 'distinct' expand to pairwise 2-operand comparisons. + size_t nargs = e.list.size() - 1; + if (nargs < 2) throw Unsupported{"comparison with <2 operands"}; + + if (op == "=" || op == "distinct") { + // (= a b c ...) => a=b, b=c, ... ; (distinct a b c ...) => all pairs. + // Under negation, De Morgan turns a conjunction into a disjunction, which + // jigsaw cannot represent -- reject unless it is a simple 2-operand form. + if (nargs > 2 && negate) + throw Unsupported{"negated n-ary =/distinct"}; + uint16_t base = (op == "=") ? Equal : Distinct; + if (negate) base = negate_cmp(base); + // Structural FP equality (SMT-LIB '=') differs from IEEE fp.eq: all NaNs + // are one value and +0 != -0. Bitwise Equal soundly UNDER-approximates + // structural '=' (identical bits ==> same value or same-bits NaN ==> + // structurally equal). But bitwise Distinct does NOT imply structural + // disequality: two NaNs with different bit patterns (e.g. sqrt(neg) -> -nan + // 0xFFF8.. vs the canonical NaN 0x7FF8..) are bit-distinct yet structurally + // equal, which would yield an unsound 'sat'. We cannot express the + // required "... and not both NaN" guard in the jigsaw AST, so reject + // structural FP disequality (-> unknown) rather than risk a wrong answer. + if (operands_fp && base == Distinct) + throw Unsupported{"structural FP disequality (NaN-unsound in jigsaw)"}; + if (op == "=") { + for (size_t i = 1; i + 1 < e.list.size(); ++i) + make_constraint(base, e.list[i], e.list[i + 1], env); + } else { // distinct: all unordered pairs + for (size_t i = 1; i < e.list.size(); ++i) + for (size_t j = i + 1; j < e.list.size(); ++j) + make_constraint(base, e.list[i], e.list[j], env); + } + return; + } + + if (nargs != 2) throw Unsupported{"n-ary comparison"}; + uint16_t k = negate ? negate_cmp(kind) : kind; + if (k == Bool) throw Unsupported{"cannot negate predicate '" + op + "'"}; + make_constraint(k, e.list[1], e.list[2], env); + } + + // cheap check whether a term is FP-sorted (used only to disambiguate '='). + bool looks_fp(const SExpr &e0, const Env &env) { + const SExpr *ep = resolve(&e0, env); + const SExpr &e = *ep; + if (e.is_atom) { + auto it = vars.find(e.atom); + if (it != vars.end()) return it->second.is_fp; + return false; + } + if (e.list.empty() || !e.list[0].is_atom) return false; + const std::string &op = e.list[0].atom; + return op == "fp" || op.rfind("fp.", 0) == 0 || + op == "_"; // (_ +zero ..) etc handled elsewhere + } + + // ---- constraint construction ------------------------------------------ + + void make_constraint(uint16_t pred_kind, const SExpr &lhs, const SExpr &rhs, + const Env &env) { + uint32_t budget = 4 + count_nodes(lhs, env) + count_nodes(rhs, env); + auto c = std::make_shared(budget); + AstNode *root = c->ast.get(); + root->set_kind(pred_kind); + root->set_bits(1); + + AstNode *left = root->add_children(); + if (!left) throw Unsupported{"AST too large"}; + TermInfo li = build_term(lhs, env, c, left); + + AstNode *right = root->add_children(); + if (!right) throw Unsupported{"AST too large"}; + TermInfo ri = build_term(rhs, env, c, right); + (void)li; (void)ri; + + c->ops[pred_kind] = true; + // jigsaw recomputes the comparison operands from the JIT'd function, so the + // Constraint's op1/op2 are unused on this path; leave them 0. + c->op1 = 0; + c->op2 = 0; + + uint32_t khash = isRelationalKind(pred_kind) ? (uint32_t)Bool : (uint32_t)pred_kind; + root->set_hash(xxhash(left->hash(), (khash << 16) | 1, right->hash())); + + task->add_constraint(c, pred_kind); + DBG("constraint kind=%u budget=%u nodes\n", pred_kind, budget); + } + + // upper bound on the number of AstNodes a term expands to (lets are followed so + // multiply-referenced bindings are counted per use, matching build_term). + uint32_t count_nodes(const SExpr &e0, const Env &env) { + const SExpr *ep = resolve(&e0, env); + const SExpr &e = *ep; + if (e.is_atom) { + if (vars.count(e.atom)) return 2; // Read (+ optional Extract) + return 1; // constant / rounding-mode / other + } + if (!e.list.empty() && e.list[0].is_atom && e.list[0].atom == "let") { + Env e2 = bind_let(e.list[1], env); + return count_nodes(e.list[2], e2); + } + uint32_t s = 2; // this node (+ slack for Extract wrappers) + for (const auto &c : e.list) s += count_nodes(c, env); + return s; + } + + // Fill a Read node (and register its bytes) exactly like rgd-parser's map_arg: + // each input byte gets its own local slot; the first byte's slot indexes the + // node hash and shape. The JIT reads `bits/8` consecutive slots. + void fill_read(AstNode *node, const std::shared_ptr &c, + uint32_t offset, uint32_t bits) { + uint32_t length = bits / 8; + uint32_t first_arg = 0; + uint32_t off = offset; + for (uint32_t k = 0; k < length; ++k, ++off) { + uint32_t ai; + auto it = c->local_map.find(off); + if (it == c->local_map.end()) { + ai = (uint32_t)c->input_args.size(); + c->inputs.insert({off, 0}); // all-zero seed + c->local_map[off] = ai; + c->input_args.push_back(std::make_pair(true, 0)); // gidx filled by finalize + } else { + ai = it->second; + } + if (k == 0) { + c->shapes[off] = length; + first_arg = ai; + } else { + c->shapes[off] = 0; + } + } + node->set_kind(Read); + node->set_bits(bits); + node->set_index(offset); + node->set_hash(xxhash(length * 8, Read, first_arg)); + } + + void fill_const(AstNode *node, const std::shared_ptr &c, + uint64_t value, uint32_t bits) { + if (bits > 64) throw Unsupported{"constant wider than 64 bits"}; + uint32_t ai = (uint32_t)c->input_args.size(); + node->set_kind(Constant); + node->set_bits(bits); + node->set_index(ai); + c->input_args.push_back(std::make_pair(false, value)); + c->const_num += 1; + node->set_hash(xxhash(bits, Constant, ai)); + } + + // Build a variable reference: a byte-aligned Read, optionally wrapped in an + // Extract to trim to a non-byte-multiple declared width. + TermInfo build_var(const VarInfo &vi, const std::shared_ptr &c, + AstNode *ret) { + if (vi.bits % 8 == 0) { + fill_read(ret, c, vi.offset, vi.bits); + } else { + uint32_t abits = ((vi.bits + 7) / 8) * 8; + ret->set_kind(Extract); + ret->set_bits(vi.bits); + ret->set_index(0); // low bit + AstNode *rd = ret->add_children(); + if (!rd) throw Unsupported{"AST too large"}; + fill_read(rd, c, vi.offset, abits); + ret->set_hash(xxhash(vi.bits, Extract, rd->hash())); + } + return {(uint16_t)vi.bits, vi.is_fp}; + } + + // Build an arbitrary BV/FP term into `ret`. + TermInfo build_term(const SExpr &e0, const Env &env, + const std::shared_ptr &c, AstNode *ret) { + const SExpr *ep = resolve(&e0, env); + const SExpr &e = *ep; + + if (e.is_atom) { + auto vit = vars.find(e.atom); + if (vit != vars.end()) return build_var(vit->second, c, ret); + uint64_t v; + uint32_t w; + if (parse_bits_literal(e.atom, v, w)) { + fill_const(ret, c, v, w); + return {(uint16_t)w, false}; + } + throw Unsupported{"atom '" + e.atom + "'"}; + } + + if (e.list.empty()) throw ParseError{"empty application"}; + const SExpr &h = e.list[0]; + + if (!h.is_atom) { + // indexed operator application: ((_ ...) args...) + return build_indexed_app(e, env, c, ret); + } + + const std::string &op = h.atom; + + if (op == "let") { + if (e.list.size() != 3) throw ParseError{"bad let"}; + Env e2 = bind_let(e.list[1], env); + return build_term(e.list[2], e2, c, ret); + } + if (op == "_") { + return build_indexed_atom(e, c, ret); + } + if (op == "fp") { + return build_fp_literal(e, c, ret); + } + + // concat needs special ordering: RGD's Concat(child0, child1) stores child0 + // in the LOW bits (c1 | (c2 << c1.bits)), whereas SMT-LIB (concat a b) puts + // `a` in the HIGH bits. Build it separately so the bit order is correct. + if (op == "concat") { + std::vector operands; + for (size_t i = 1; i < e.list.size(); ++i) operands.push_back(&e.list[i]); + if (operands.size() < 2) throw ParseError{"bad concat"}; + return build_concat(operands, 0, operands.size() - 1, env, c, ret); + } + + // ---- BV binary / n-ary (left-associative) ---- + struct BinMap { const char *name; uint16_t kind; }; + static const BinMap bvbin[] = { + {"bvadd", Add}, {"bvsub", Sub}, {"bvmul", Mul}, {"bvudiv", UDiv}, + {"bvsdiv", SDiv}, {"bvurem", URem}, {"bvsrem", SRem}, {"bvand", And}, + {"bvor", Or}, {"bvxor", Xor}, {"bvshl", Shl}, {"bvlshr", LShr}, + {"bvashr", AShr}, + }; + for (const auto &m : bvbin) { + if (op == m.name) { + std::vector operands; + for (size_t i = 1; i < e.list.size(); ++i) operands.push_back(&e.list[i]); + if (operands.size() < 2) throw ParseError{std::string("bad ") + m.name}; + return build_fold(m.kind, operands, operands.size() - 1, env, c, ret); + } + } + + // ---- BV unary ---- + if (op == "bvneg" || op == "bvnot") { + if (e.list.size() != 2) throw ParseError{"bad " + op}; + ret->set_kind(op == "bvneg" ? Neg : Not); + AstNode *ch = ret->add_children(); + if (!ch) throw Unsupported{"AST too large"}; + TermInfo ci = build_term(e.list[1], env, c, ch); + ret->set_bits(ci.bits); + c->ops[ret->kind()] = true; + // unary hash mirrors rgd-parser's unary path + ret->set_hash(xxhash(ci.bits, ret->kind(), ch->hash())); + return {ci.bits, false}; + } + + // ---- FP arithmetic ---- + return build_fp_op(e, env, c, ret); + } + + // build a left-associative fold of `kind` over operands[0..hi]. + TermInfo build_fold(uint16_t kind, const std::vector &operands, + size_t hi, const Env &env, + const std::shared_ptr &c, AstNode *ret) { + ret->set_kind(kind); + AstNode *left = ret->add_children(); + if (!left) throw Unsupported{"AST too large"}; + TermInfo li; + if (hi == 1) { + li = build_term(*operands[0], env, c, left); + } else { + li = build_fold(kind, operands, hi - 1, env, c, left); + } + AstNode *right = ret->add_children(); + if (!right) throw Unsupported{"AST too large"}; + TermInfo ri = build_term(*operands[hi], env, c, right); + + uint16_t bits; + if (kind == Concat) bits = (uint16_t)(li.bits + ri.bits); + else bits = li.bits; // arithmetic / bitwise / shift: result width = lhs width + ret->set_bits(bits); + c->ops[kind] = true; + uint32_t khash = isRelationalKind(kind) ? (uint32_t)Bool : (uint32_t)kind; + ret->set_hash(xxhash(left->hash(), (khash << 16) | bits, right->hash())); + return {bits, false}; + } + + // Build an SMT-LIB (concat operands[lo..hi]) with operands[lo] most + // significant. RGD's Concat(child0, child1) treats child0 as the LOW part, so + // we recurse with the remaining (lower) operands as child0 and the current + // (highest) operand as child1. + TermInfo build_concat(const std::vector &operands, size_t lo, + size_t hi, const Env &env, + const std::shared_ptr &c, AstNode *ret) { + if (lo == hi) return build_term(*operands[lo], env, c, ret); + ret->set_kind(Concat); + // child0 = low part = concat(operands[lo+1..hi]) + AstNode *low = ret->add_children(); + if (!low) throw Unsupported{"AST too large"}; + TermInfo li = build_concat(operands, lo + 1, hi, env, c, low); + // child1 = high part = operands[lo] + AstNode *high = ret->add_children(); + if (!high) throw Unsupported{"AST too large"}; + TermInfo hi_ti = build_term(*operands[lo], env, c, high); + uint16_t bits = (uint16_t)(li.bits + hi_ti.bits); + ret->set_bits(bits); + c->ops[Concat] = true; + ret->set_hash(xxhash(low->hash(), ((uint32_t)Concat << 16) | bits, high->hash())); + return {bits, false}; + } + + // ((_ extract i j) x), ((_ zero_extend k) x), ((_ sign_extend k) x), + // ((_ to_fp eb sb) rm x) + TermInfo build_indexed_app(const SExpr &e, const Env &env, + const std::shared_ptr &c, AstNode *ret) { + const SExpr &idx = e.list[0]; // (_ name args...) + if (idx.list.size() < 2 || !idx.list[1].is_atom) + throw ParseError{"bad indexed op"}; + const std::string &name = idx.list[1].atom; + + if (name == "extract") { + uint64_t hi, lo; + if (!parse_uint(idx.list[2].atom, hi) || !parse_uint(idx.list[3].atom, lo)) + throw ParseError{"bad extract"}; + ret->set_kind(Extract); + ret->set_bits((uint16_t)(hi - lo + 1)); + ret->set_index((uint32_t)lo); + AstNode *ch = ret->add_children(); + if (!ch) throw Unsupported{"AST too large"}; + build_term(e.list[1], env, c, ch); + c->ops[Extract] = true; + ret->set_hash(xxhash(ret->bits(), Extract, ch->hash())); + return {ret->bits(), false}; + } + if (name == "zero_extend" || name == "sign_extend") { + uint64_t k; + if (!parse_uint(idx.list[2].atom, k)) throw ParseError{"bad extend"}; + uint16_t kind = (name == "zero_extend") ? ZExt : SExt; + ret->set_kind(kind); + AstNode *ch = ret->add_children(); + if (!ch) throw Unsupported{"AST too large"}; + TermInfo ci = build_term(e.list[1], env, c, ch); + ret->set_bits((uint16_t)(ci.bits + k)); + c->ops[kind] = true; + ret->set_hash(xxhash(ret->bits(), kind, ch->hash())); + return {ret->bits(), false}; + } + if (name == "to_fp") { + return build_to_fp(e, env, c, ret, /*unsigned_src=*/false); + } + if (name == "to_fp_unsigned") { + return build_to_fp(e, env, c, ret, /*unsigned_src=*/true); + } + throw Unsupported{"indexed op '" + name + "'"}; + } + + // (_ bvN W) as a term, or FP special constants (_ +zero eb sb) etc. + TermInfo build_indexed_atom(const SExpr &e, + const std::shared_ptr &c, AstNode *ret) { + if (e.list.size() < 2 || !e.list[1].is_atom) throw ParseError{"bad (_ ..)"}; + const std::string &name = e.list[1].atom; + if (name.rfind("bv", 0) == 0) { + uint64_t v; + if (!parse_uint(name.substr(2), v)) throw ParseError{"bad (_ bvN W)"}; + uint64_t w; + if (!parse_uint(e.list[2].atom, w)) throw ParseError{"bad (_ bvN W) width"}; + fill_const(ret, c, v, (uint32_t)w); + return {(uint16_t)w, false}; + } + // FP specials: (_ +zero eb sb) / -zero / +oo / -oo / NaN + uint64_t eb, sb; + if (e.list.size() >= 4 && parse_uint(e.list[2].atom, eb) && + parse_uint(e.list[3].atom, sb)) { + uint32_t width = (uint32_t)(eb + sb); + if (width != 32 && width != 64) throw Unsupported{"FP special width"}; + uint32_t ew = (uint32_t)eb; + uint32_t sw = (uint32_t)(sb - 1); // stored significand bits + uint64_t expmask = (ew >= 64) ? ~0ull : ((1ull << ew) - 1); + uint64_t sigmask = (sw >= 64) ? ~0ull : ((1ull << sw) - 1); + uint64_t bits = 0; + if (name == "+zero") bits = 0; + else if (name == "-zero") bits = 1ull << (ew + sw); + else if (name == "+oo") bits = expmask << sw; + else if (name == "-oo") bits = (1ull << (ew + sw)) | (expmask << sw); + else if (name == "NaN") bits = (expmask << sw) | (1ull << (sw - 1)); // qNaN + else throw Unsupported{"(_ " + name + " ..)"}; + (void)sigmask; + fill_const(ret, c, bits, width); + return {(uint16_t)width, true}; + } + throw Unsupported{"(_ " + name + " ..)"}; + } + + // (fp #b ) literal + TermInfo build_fp_literal(const SExpr &e, + const std::shared_ptr &c, AstNode *ret) { + if (e.list.size() != 4) throw ParseError{"bad fp literal"}; + uint64_t sgn, exp, sig; + uint32_t sw, ew, gw; + if (!parse_bits_literal(e.list[1].atom, sgn, sw) || + !parse_bits_literal(e.list[2].atom, exp, ew) || + !parse_bits_literal(e.list[3].atom, sig, gw)) + throw ParseError{"bad fp literal fields"}; + uint32_t width = sw + ew + gw; + if (width != 32 && width != 64) throw Unsupported{"fp literal width"}; + uint64_t bits = (sgn << (ew + gw)) | (exp << gw) | sig; + fill_const(ret, c, bits, width); + return {(uint16_t)width, true}; + } + + // to_fp in its several SMT-LIB overloads: + // ((_ to_fp eb sb) ) -- bit-pattern REINTERPRET (no rm) + // ((_ to_fp eb sb) rm ) -- FP -> FP convert (FpExt / FpTrunc) + // ((_ to_fp eb sb) rm ) -- signed machine int -> FP (SiToFp) + // ((_ to_fp_unsigned eb sb) rm ) -- unsigned machine int -> FP (UiToFp) + // ((_ to_fp eb sb) rm ) -- real literal -> FP constant + TermInfo build_to_fp(const SExpr &e, const Env &env, + const std::shared_ptr &c, AstNode *ret, + bool unsigned_src) { + const SExpr &idx = e.list[0]; + uint64_t eb, sb; + if (idx.list.size() < 4 || !parse_uint(idx.list[2].atom, eb) || + !parse_uint(idx.list[3].atom, sb)) + throw ParseError{"bad to_fp"}; + uint32_t width = (uint32_t)(eb + sb); + if (width != 32 && width != 64) throw Unsupported{"to_fp width"}; + + // Form 1: ((_ to_fp eb sb) ) with NO rounding mode -- a bit-pattern + // reinterpretation of a width-matching bitvector. In our bit-pattern FP + // model an FP value IS its iN encoding, so this is a no-op: build the source + // in place and re-tag it as FP. + if (!unsigned_src && e.list.size() == 2) { + TermInfo ci = build_term(e.list[1], env, c, ret); + if (ci.bits != width) + throw Unsupported{"to_fp bit-reinterpret width mismatch"}; + return {(uint16_t)width, true}; + } + + // Remaining forms carry a rounding mode: e.list[1] = rm, e.list[2] = source. + if (e.list.size() != 3) throw Unsupported{"to_fp form"}; + const SExpr &src = e.list[2]; + + // Decimal / real literal source -> constant (rounding mode ignored: the + // literal already names the exact value; nearest is fine for our purposes). + double d; + if (!unsigned_src && eval_real(src, d)) { + fill_const(ret, c, fp_to_bits(d, width), width); + return {(uint16_t)width, true}; + } + + // Determine whether the (symbolic) source is FP or a bitvector. looks_fp + // treats every (_ ..) as FP, but (_ bvN W) is a bitvector constant, so + // override that case. + bool src_fp = !unsigned_src && looks_fp(src, env); + const SExpr *sr = resolve(&src, env); + if (!sr->is_atom && !sr->list.empty() && sr->list[0].is_atom && + sr->list[0].atom == "_" && sr->list.size() >= 2 && sr->list[1].is_atom && + sr->list[1].atom.rfind("bv", 0) == 0) + src_fp = false; // (_ bvN W) is a bitvector constant, not FP + + AstNode *ch = ret->add_children(); + if (!ch) throw Unsupported{"AST too large"}; + TermInfo ci = build_term(src, env, c, ch); + + uint16_t kind; + if (src_fp) { + // FP -> FP: widen with FpExt, narrow with FpTrunc. Equal width would be a + // redundant no-op cast; jigsaw's FpExt/FpTrunc require differing widths. + if (width > ci.bits) kind = FpExt; + else if (width < ci.bits) kind = FpTrunc; + else throw Unsupported{"redundant to_fp (FP->FP same width)"}; + } else { + // machine integer -> FP. + kind = unsigned_src ? UiToFp : SiToFp; + } + ret->set_kind(kind); + ret->set_bits((uint16_t)width); + c->ops[kind] = true; + ret->set_hash(xxhash((uint16_t)width, kind, ch->hash())); + return {(uint16_t)width, true}; + } + + bool eval_real(const SExpr &e, double &out) { + if (e.is_atom) { + char *end = nullptr; + double d = std::strtod(e.atom.c_str(), &end); + if (end && *end == '\0' && end != e.atom.c_str()) { out = d; return true; } + return false; + } + if (!e.list.empty() && e.list[0].is_atom) { + const std::string &op = e.list[0].atom; + if (op == "-" && e.list.size() == 2) { + double d; + if (eval_real(e.list[1], d)) { out = -d; return true; } + } + if (op == "/" && e.list.size() == 3) { + double a, b; + if (eval_real(e.list[1], a) && eval_real(e.list[2], b) && b != 0) { + out = a / b; + return true; + } + } + } + return false; + } + + // Map an SMT-LIB rounding-mode token to the fp_rounding_mode selector + // (0=rna, 1=rne, 2=rtp, 3=rtn, 4=rtz), matching jit.cc / z3-solver.cpp. + static uint32_t parse_rm(const std::string &rm) { + if (rm == "roundNearestTiesToAway" || rm == "RNA") return 0; + if (rm == "roundNearestTiesToEven" || rm == "RNE") return 1; + if (rm == "roundTowardPositive" || rm == "RTP") return 2; + if (rm == "roundTowardNegative" || rm == "RTN") return 3; + if (rm == "roundTowardZero" || rm == "RTZ") return 4; + throw Unsupported{"rounding mode " + rm}; + } + + // FP arithmetic ops. fp.add/sub/mul/div/sqrt/roundToIntegral carry a leading + // rounding-mode argument. We parse it into the fp_rounding_mode selector and + // stash it in the node's index(), so the JIT (jit.cc) can emit rounding-mode- + // correct arithmetic and the z3 backends use the matching Z3_mk_fpa rounding. + // The selector is folded into the node hash (below) because the JIT'd-function + // cache (fCache) keys on the AST hash and isEqualAst does NOT compare index() + // -- otherwise e.g. (fp.mul RNE ...) and (fp.mul RTN ...) would collide and + // one mode's compiled code would be wrongly reused for the other. + TermInfo build_fp_op(const SExpr &e, const Env &env, + const std::shared_ptr &c, AstNode *ret) { + const std::string &op = e.list[0].atom; + + auto build_unary = [&](uint16_t kind, size_t argidx) -> TermInfo { + ret->set_kind(kind); + AstNode *ch = ret->add_children(); + if (!ch) throw Unsupported{"AST too large"}; + TermInfo ci = build_term(e.list[argidx], env, c, ch); + ret->set_bits(ci.bits); + c->ops[kind] = true; + ret->set_hash(xxhash(ci.bits, kind, ch->hash())); + return {ci.bits, true}; + }; + auto build_binary = [&](uint16_t kind, size_t a, size_t b) -> TermInfo { + ret->set_kind(kind); + AstNode *l = ret->add_children(); + if (!l) throw Unsupported{"AST too large"}; + TermInfo li = build_term(e.list[a], env, c, l); + AstNode *r = ret->add_children(); + if (!r) throw Unsupported{"AST too large"}; + TermInfo ri = build_term(e.list[b], env, c, r); + (void)ri; + ret->set_bits(li.bits); + c->ops[kind] = true; + ret->set_hash(xxhash(l->hash(), ((uint32_t)kind << 16) | li.bits, r->hash())); + return {li.bits, true}; + }; + // rounding-mode-carrying variants: parse rm at rm_idx, set_index(sel), and + // mix sel into the hash so distinct modes get distinct cache keys. + // rna (ties-to-away) is rejected on FP arithmetic: x86 has no MXCSR + // representation for it, so the JIT can't honor it (jit.cc bails on it too). + // The runtime (parsers/rgd-parser.cpp) leaves index()==0 on FP-arith nodes + // to mean "RNE default", so jit.cc/z3 treat selector 0 and 1 both as RNE; + // that makes rna==0 indistinguishable from the default, hence the bail here. + auto build_binary_rm = [&](uint16_t kind, size_t rm_idx, size_t a, size_t b) + -> TermInfo { + if (e.list.size() <= b) throw ParseError{"bad " + op}; + uint32_t sel = parse_rm(e.list[rm_idx].atom); + if (sel == 0) throw Unsupported{"rna rounding on FP arithmetic"}; + TermInfo ti = build_binary(kind, a, b); + ret->set_index(sel); + ret->set_hash(xxhash(ret->hash(), sel, kind)); + return ti; + }; + auto build_unary_rm = [&](uint16_t kind, size_t rm_idx, size_t argidx) + -> TermInfo { + if (e.list.size() <= argidx) throw ParseError{"bad " + op}; + uint32_t sel = parse_rm(e.list[rm_idx].atom); + if (sel == 0) throw Unsupported{"rna rounding on FP arithmetic"}; + TermInfo ti = build_unary(kind, argidx); + ret->set_index(sel); + ret->set_hash(xxhash(ret->hash(), sel, kind)); + return ti; + }; + + // rounding-mode-carrying binary ops: (fp.add rm a b) + if (op == "fp.add") return build_binary_rm(FAdd, 1, 2, 3); + if (op == "fp.sub") return build_binary_rm(FSub, 1, 2, 3); + if (op == "fp.mul") return build_binary_rm(FMul, 1, 2, 3); + if (op == "fp.div") return build_binary_rm(FDiv, 1, 2, 3); + // no rounding mode + if (op == "fp.rem") return build_binary(FRem, 1, 2); + if (op == "fp.min") return build_binary(FpMin, 1, 2); + if (op == "fp.max") return build_binary(FpMax, 1, 2); + // rounding-mode-carrying unary: (fp.sqrt rm x) + if (op == "fp.sqrt") return build_unary_rm(FpSqrt, 1, 2); + // no rounding mode + if (op == "fp.neg") return build_unary(FNeg, 1); + if (op == "fp.abs") return build_unary(FpFabs, 1); + + if (op == "fp.roundToIntegral") { + // (fp.roundToIntegral rm x) -- map rm to the fp_rounding_mode selector. + if (e.list.size() != 3) throw ParseError{"bad roundToIntegral"}; + uint32_t sel = parse_rm(e.list[1].atom); + ret->set_kind(FpRound); + AstNode *ch = ret->add_children(); + if (!ch) throw Unsupported{"AST too large"}; + TermInfo ci = build_term(e.list[2], env, c, ch); + ret->set_bits(ci.bits); + ret->set_index(sel); + c->ops[FpRound] = true; + // fold the rounding selector into the hash: jit.cc emits a different + // intrinsic per mode (floor/ceil/trunc/...), so two roundToIntegral nodes + // that differ only in rm must not share a cached compiled function. + ret->set_hash(xxhash(xxhash(ci.bits, FpRound, ch->hash()), sel, FpRound)); + return {ci.bits, true}; + } + + throw Unsupported{"operator '" + op + "'"}; + } +}; + +// ------------------------------------------------------------------------- +// model decoding / printing +// ------------------------------------------------------------------------- + +static void print_model(const Translator &tr, const uint8_t *buf, size_t size) { + printf("(\n"); + for (const auto &name : tr.var_order) { + const VarInfo &vi = tr.vars.at(name); + uint32_t wbytes = (vi.bits + 7) / 8; + // little-endian read of the (possibly wide) value + std::vector bytes(wbytes, 0); + for (uint32_t k = 0; k < wbytes; ++k) { + size_t off = vi.offset + k; + bytes[k] = (off < size) ? buf[off] : 0; + } + if (vi.is_fp) { + // (fp #b #b #b) + uint32_t ew = (vi.bits == 32) ? 8 : 11; + uint32_t sw = (vi.bits == 32) ? 23 : 52; + uint64_t v = 0; + for (uint32_t k = 0; k < wbytes; ++k) v |= (uint64_t)bytes[k] << (8 * k); + auto bin = [](uint64_t val, uint32_t nbits) { + std::string s; + for (int i = (int)nbits - 1; i >= 0; --i) s.push_back(((val >> i) & 1) ? '1' : '0'); + return s; + }; + uint64_t sign = (v >> (ew + sw)) & 1; + uint64_t exp = (v >> sw) & ((ew >= 64) ? ~0ull : ((1ull << ew) - 1)); + uint64_t sig = v & ((sw >= 64) ? ~0ull : ((1ull << sw) - 1)); + printf(" (define-fun %s () %s (fp #b%s #b%s #b%s))\n", name.c_str(), + vi.bits == 32 ? "Float32" : "Float64", bin(sign, 1).c_str(), + bin(exp, ew).c_str(), bin(sig, sw).c_str()); + } else { + // BV value as a width-exact #b binary literal (works for any width) + std::string s; + for (int i = (int)vi.bits - 1; i >= 0; --i) { + uint8_t byte = bytes[i / 8]; + s.push_back(((byte >> (i % 8)) & 1) ? '1' : '0'); + } + printf(" (define-fun %s () (_ BitVec %u) #b%s)\n", name.c_str(), vi.bits, + s.c_str()); + } + } + printf(")\n"); +} + +} // namespace + +int main(int argc, char **argv) { + bool use_z3 = getenv("SMT_USE_Z3") != nullptr; + bool use_jigsaw = getenv("SMT_NO_JIGSAW") == nullptr; + bool report_time = getenv("SMT_TIME") != nullptr; + const char *path = nullptr; + + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "--spike-fp-rounding") { + // Phase-0 feasibility spike (throwaway): does the JIT honor directed FP + // rounding? Construct a JITSolver to initialize the LLVM/ORC JIT, then + // run the self-contained check in solvers/jigsaw/jit.cc. + JITSolver init; // initializes the native target + JIT global + return rgd::spike_fp_rounding(); + } + else if (a == "--z3") use_z3 = true; + else if (a == "--no-jigsaw") use_jigsaw = false; + else if (a == "--time") report_time = true; + else if (a == "--seed" || a.rfind("--seed=", 0) == 0) { + // Fix the jigsaw PRNG seed for deterministic strategy comparison. + // Accepts "--seed N" or "--seed=N"; propagated to MutInput via JIGSAW_SEED. + const char *val = nullptr; + if (a == "--seed") { if (i + 1 < argc) val = argv[++i]; } + else val = a.c_str() + 7; + if (!val) { + fprintf(stderr, "--seed requires a value\n"); + return 2; + } + setenv("JIGSAW_SEED", val, 1); + } + else if (a == "--budget" || a.rfind("--budget=", 0) == 0) { + // Override the jigsaw per-task attempt budget (default MAX_EXEC_TIMES). + // Accepts "--budget N" or "--budget=N"; propagated via JIGSAW_MAX_EXEC. + const char *val = nullptr; + if (a == "--budget") { if (i + 1 < argc) val = argv[++i]; } + else val = a.c_str() + 9; + if (!val) { + fprintf(stderr, "--budget requires a value\n"); + return 2; + } + setenv("JIGSAW_MAX_EXEC", val, 1); + } + else if (a == "-h" || a == "--help") { + fprintf(stderr, "Usage: %s [--z3] [--no-jigsaw] [--time] [--seed N] [--budget N] file.smt2\n", argv[0]); + return 2; + } else { + path = argv[i]; + } + } + if (!path) { + fprintf(stderr, "Usage: %s [--z3] [--no-jigsaw] [--time] [--seed N] [--budget N] file.smt2\n", argv[0]); + return 2; + } + + // read the whole file + std::ifstream ifs(path, std::ios::binary); + if (!ifs) { + fprintf(stderr, "failed to open %s\n", path); + return 2; + } + std::stringstream ss; + ss << ifs.rdbuf(); + std::string content = ss.str(); + + // Phase timers (microseconds). parse = SMT-LIB read + DNF translation + + // SearchTask construction; solve = wall time of the whole solver loop; the + // codegen/jit/gd breakdown below is the jigsaw-internal split of solve time. + using Clock = std::chrono::steady_clock; + auto us_since = [](Clock::time_point t) { + return std::chrono::duration(Clock::now() - t).count(); + }; + double parse_us = 0, solve_us = 0; + JITSolver *jit_ptr = nullptr; + auto emit_time = [&]() { + if (!report_time) return; + uint64_t codegen = 0, jit = 0, gd = 0; + if (jit_ptr) { + codegen = jit_ptr->get_codegen_time(); + jit = jit_ptr->get_jit_time(); + gd = jit_ptr->get_solving_time(); + } + fprintf(stderr, + "TIME parse=%.0f codegen=%lu jit=%lu gd=%lu solve=%.0f total=%.0f (us)\n", + parse_us, codegen, jit, gd, solve_us, parse_us + solve_us); + }; + + auto t_start = Clock::now(); + Translator tr; + bool has_constraints = false; + try { + SexpReader reader(content); + // Read every command first and keep them alive for the whole run. DNF + // Literals capture bare `const SExpr *` pointers into these command trees + // (the assertion sub-expressions) and are consumed later in build_tasks(), + // so the parsed SExprs must outlive command processing. All commands are + // read (and any vector reallocation happens) before run_command takes a + // single pointer, so the trees are stable once we start processing them. + std::vector cmds; + while (!reader.eof()) { + SExpr cmd = reader.read(); + cmds.push_back(std::move(cmd)); + } + for (const auto &cmd : cmds) + tr.run_command(cmd); + has_constraints = tr.build_tasks(); + parse_us = us_since(t_start); + } catch (const Unsupported &u) { + fprintf(stderr, "unsupported: %s\n", u.msg.c_str()); + printf("unknown\n"); + return 0; + } catch (const ParseError &p) { + fprintf(stderr, "parse error: %s\n", p.msg.c_str()); + printf("unknown\n"); + return 0; + } catch (const std::exception &ex) { + fprintf(stderr, "error: %s\n", ex.what()); + printf("unknown\n"); + return 0; + } + + // virtual input buffer (all-zero seed) + size_t in_size = tr.total_bytes ? tr.total_bytes : 1; + std::vector in_buf(in_size, 0); + std::vector out_buf(in_size, 0); + + if (tr.trivial_sat) { + // some DNF clause is unconditionally true -> the formula is satisfiable + printf("sat\n"); + print_model(tr, in_buf.data(), in_buf.size()); + emit_time(); + return 0; + } + if (!has_constraints) { + // no clauses at all -> the formula reduced to false; jigsaw cannot prove + // unsat, so report unknown (z3, if asked, handles per-clause unsat below). + printf("unknown\n"); + emit_time(); + return 0; + } + + // build the solver chain (jigsaw first for speed; optional z3 as a complete, + // FP-aware fallback). i2s is intentionally excluded: it solves one constraint + // at a time and would falsely report SAT on a multi-constraint conjunction. + std::vector> solvers; + if (use_jigsaw) { + auto jit = std::make_shared(); + jit_ptr = jit.get(); // for the codegen/jit/gd timing breakdown + solvers.emplace_back(std::move(jit)); + } + if (use_z3) solvers.emplace_back(std::make_shared()); + if (solvers.empty()) { + fprintf(stderr, "no solver selected\n"); + printf("unknown\n"); + emit_time(); + return 0; + } + + // The formula (a DNF) is SAT iff ANY clause-task is SAT; it is UNSAT iff EVERY + // clause-task is proven UNSAT (only z3 can do that). Solve each task with the + // chain; the first SAT wins. Track whether all tasks were shown unsat. + auto t_solve = Clock::now(); + bool all_unsat = true; + for (auto &clause_task : tr.tasks) { + bool this_unsat = false; + for (auto &solver : solvers) { + size_t out_size = 0; + solver_result_t r; + try { + r = solver->solve(clause_task, in_buf.data(), in_buf.size(), + out_buf.data(), out_size); + } catch (const std::exception &ex) { + fprintf(stderr, "solver error: %s\n", ex.what()); + continue; + } + if (r == SOLVER_SAT) { + solve_us = us_since(t_solve); + // JIGSAW_DUMP_MODEL=: write the offset-indexed model buffer so a + // known-good assignment (e.g. from z3 via --z3 --no-jigsaw) can be fed + // back to jigsaw as JIGSAW_TARGET for the search trace (gd.cc). + if (const char *mp = getenv("JIGSAW_DUMP_MODEL")) { + size_t n = out_size ? out_size : out_buf.size(); + FILE *mf = fopen(mp, "wb"); + if (mf) { fwrite(out_buf.data(), 1, n, mf); fclose(mf); } + } + printf("sat\n"); + print_model(tr, out_buf.data(), out_size ? out_size : out_buf.size()); + emit_time(); + return 0; + } else if (r == SOLVER_UNSAT) { + this_unsat = true; // this clause is unsat; try the next clause + break; + } + // SOLVER_TIMEOUT / SOLVER_ERROR: try the next solver for this clause + } + if (!this_unsat) all_unsat = false; // clause neither SAT nor proven UNSAT + } + solve_us = us_since(t_solve); + if (all_unsat) { + // every clause proven unsat -> the whole DNF (formula) is unsat + printf("unsat\n"); + emit_time(); + return 0; + } + printf("unknown\n"); + emit_time(); + return 0; +} diff --git a/include/ast.h b/include/ast.h index 47278bc8..60d38659 100644 --- a/include/ast.h +++ b/include/ast.h @@ -58,6 +58,68 @@ namespace rgd { Memcmp, //37 MemcmpN, // 38 + // Floating-point arithmetic (operands & result are IEEE-754 bit-vectors). + // The out-of-process RGD path lifts BV children to the fpa theory in the + // z3 solver; jigsaw JIT and i2s stay integer-only and reject these. + FAdd, // 39 + FSub, // 40 + FMul, // 41 + FDiv, // 42 + FRem, // 43 + FNeg, // 44 + + // FP casts + FpToUi, // 45 + FpToSi, // 46 + UiToFp, // 47 + SiToFp, // 48 + FpTrunc, // 49 + FpExt, // 50 + + // FP intrinsics / libcalls + FpFabs, // 51 + FpSqrt, // 52 + FpRound, // 53 rounding-mode selector carried in AstNode index() + FpMin, // 54 + FpMax, // 55 + FpCopysign, // 56 + FpIsNan, // 57 + FpIsInf, // 58 + FpIsFinite, // 59 + FpSignbit, // 60 + FpLrint, // 61 + + // FP transcendentals (exp/log/pow family). z3's fpa theory has no way to + // invert these and jigsaw is integer-only, so those solvers reject them + // (see below); the i2s solver instead computes the numeric libm inverse + // (e.g. log for exp) and VERIFIES it, so it can flip these guards. Kept + // inside the [FAdd, FUne] range so isFloatingPointKind() covers them. + FpExp, // 62 + FpExp2, // 63 + FpLog, // 64 + FpLog2, // 65 + FpLog10, // 66 + FpLog1p, // 67 + FpPow, // 68 binary: base and exponent (one is a constant for i2s) + + // FP comparisons (LLVM FCmp predicates 1..14; FALSE/TRUE are constants). + // Kept OUTSIDE the isRelationalKind() range on purpose so that the + // integer-only jigsaw/i2s solvers cleanly reject FP tasks (fall back to z3). + FOeq, // 69 + FOgt, // 70 + FOge, // 71 + FOlt, // 72 + FOle, // 73 + FOne, // 74 + FOrd, // 75 + FUno, // 76 + FUeq, // 77 + FUgt, // 78 + FUge, // 79 + FUlt, // 80 + FUle, // 81 + FUne, // 82 + // Last LastOp }; @@ -102,6 +164,50 @@ namespace rgd { "Load", "Memcmp", "MemcmpN", + "FAdd", + "FSub", + "FMul", + "FDiv", + "FRem", + "FNeg", + "FpToUi", + "FpToSi", + "UiToFp", + "SiToFp", + "FpTrunc", + "FpExt", + "FpFabs", + "FpSqrt", + "FpRound", + "FpMin", + "FpMax", + "FpCopysign", + "FpIsNan", + "FpIsInf", + "FpIsFinite", + "FpSignbit", + "FpLrint", + "FpExp", + "FpExp2", + "FpLog", + "FpLog2", + "FpLog10", + "FpLog1p", + "FpPow", + "FOeq", + "FOgt", + "FOge", + "FOlt", + "FOle", + "FOne", + "FOrd", + "FUno", + "FUeq", + "FUgt", + "FUge", + "FUlt", + "FUle", + "FUne", }; static inline bool isRelationalKind(uint16_t kind) { @@ -111,6 +217,32 @@ namespace rgd { return false; } + // Signed integer relational kinds (bvslt/bvsle/bvsgt/bvsge). These are + // distinguished from the unsigned/equality relations because the jigsaw JIT + // SIGN-extends a signed comparison's operands to 64-bit (so gd.cc's + // (int64_t) distance is correct), while unsigned/equality comparisons + // ZERO-extend. A signed and an unsigned comparison over identical operands + // therefore compile to DIFFERENT native functions and must NOT share a + // JIT'ed function (see isEqualAstRecursive) -- otherwise a signed constraint + // could reuse an unsigned (zero-extending) function and report an unsound SAT. + static inline bool isSignedRelationalKind(uint16_t kind) { + if (kind >= Slt && kind <= Sge) + return true; + else + return false; + } + + // Floating-point relational kinds are deliberately kept out of the + // isRelationalKind() range: the integer-only jigsaw JIT and i2s solvers + // dispatch on isRelationalKind(), so excluding FP makes them reject FP + // tasks and fall back to the (FP-aware) z3 solver. + static inline bool isFPRelationalKind(uint16_t kind) { + if (kind >= FOeq && kind <= FUne) + return true; + else + return false; + } + static inline bool isBinaryOperation(uint16_t kind) { if (kind >= Add && kind <= AShr && kind != Neg && kind != Not) return true; @@ -118,6 +250,20 @@ namespace rgd { return false; } + // Any floating-point op (FP arithmetic, casts, intrinsics/libcalls, and FP + // comparisons) lives contiguously in [FAdd, FUne]. The integer-only solvers + // (jigsaw JIT, i2s input-to-state) cannot reason about these: input bytes + // reaching a comparison *through* an FP op (e.g. (long)x == 42, lrint(x) == 42) + // no longer appear literally, so copying the constant into the input produces + // a bogus solution. Such solvers must reject a constraint whose ops bitset + // intersects this range and fall back to the FP-aware z3 solver. + static inline bool isFloatingPointKind(uint16_t kind) { + if (kind >= FAdd && kind <= FUne) + return true; + else + return false; + } + static inline uint16_t negate_cmp(uint16_t kind) { switch (kind) { case Equal: return Distinct; @@ -130,6 +276,21 @@ namespace rgd { case Sle: return Sgt; case Sgt: return Sle; case Sge: return Slt; + // FP predicate negations (LLVM's ordered<->unordered complement pairs). + case FOeq: return FUne; + case FUne: return FOeq; + case FOgt: return FUle; + case FUle: return FOgt; + case FOge: return FUlt; + case FUlt: return FOge; + case FOlt: return FUge; + case FUge: return FOlt; + case FOle: return FUgt; + case FUgt: return FOle; + case FOne: return FUeq; + case FUeq: return FOne; + case FOrd: return FUno; + case FUno: return FOrd; default: return Bool; } } @@ -266,8 +427,12 @@ namespace rgd { if (lhs.kind() != rhs.kind()) { // to maximize the reuse of JIT'ed functions, jigsaw does not // care about which relational operator is used, as long as - // they are both relational operators - if (isRelationalKind(lhs.kind()) && isRelationalKind(rhs.kind())) { + // they are both relational operators -- EXCEPT that signed and + // unsigned comparisons extend their operands differently in the + // JIT (sign- vs zero-extend), so they must stay in separate reuse + // classes; sharing across the boundary yields an unsound SAT. + if (isRelationalKind(lhs.kind()) && isRelationalKind(rhs.kind()) + && isSignedRelationalKind(lhs.kind()) == isSignedRelationalKind(rhs.kind())) { // do nothing, fall through to compare operands } else { return false; diff --git a/include/solver.h b/include/solver.h index a8098db9..98bccda7 100644 --- a/include/solver.h +++ b/include/solver.h @@ -48,6 +48,9 @@ class Z3Solver : public Solver { z3::context &context_; z3::solver solver_; + // auxiliary range constraints emitted while serializing partial FP casts + // (fpa.to_sbv/to_ubv); collected during serialize() and added before check(). + std::vector aux_constraints_; }; class JITSolver : public Solver { @@ -57,6 +60,13 @@ class JITSolver : public Solver { const uint8_t *in_buf, size_t in_size, uint8_t *out_buf, size_t &out_size) override; void print_stats(int fd) override; + // timing accessors (microseconds), cumulative across solve() calls: + // codegen = AST -> LLVM IR (addFunction) + // jit = LLVM IR -> native code (performJit) + // solving = gradient-descent search (gd_entry) + uint64_t get_codegen_time() const { return process_time.load(); } + uint64_t get_jit_time() const { return jit_time.load(); } + uint64_t get_solving_time() const { return solving_time.load(); } private: std::atomic_ulong uuid; std::atomic_ulong cache_hits; @@ -79,12 +89,31 @@ class I2SSolver : public Solver { uint64_t matches; uint64_t mismatches; std::bitset binop_mask; + // bits for the FP op kinds that input-to-state cannot invert (FRem, FNeg, and + // all FP casts and intrinsics/libcalls). A constraint touching any of these + // is rejected and falls back to z3. A "direct" FCmp (input bytes -> FCmp + // against a constant) sets no bit here and is handled by solve_fcmp. + std::bitset fp_ops_mask; + // bits for the invertible FP binops (FAdd/FSub/FMul/FDiv, plus FpPow) that + // solve_fcmp can reverse against a constant operand (x + C K -> write + // K-C into input). These are deliberately NOT in fp_ops_mask so such a + // constraint reaches solve_fcmp instead of being rejected. + std::bitset fp_arith_mask; + // bits for the invertible unary FP transcendentals (exp/exp2/log/log2/log10/ + // log1p) that solve_fcmp reverses via the numeric libm inverse (log for exp, + // ...) and verifies. Also kept out of fp_ops_mask so they reach solve_fcmp. + std::bitset fp_trans_mask; solver_result_t solve_icmp(std::shared_ptr const& c, std::unique_ptr const& cm, uint32_t comparison, const uint8_t *in_buf, size_t in_size, uint8_t *out_buf, size_t &out_size); + solver_result_t solve_fcmp(std::shared_ptr const& c, + std::unique_ptr const& cm, + uint32_t comparison, + const uint8_t *in_buf, size_t in_size, + uint8_t *out_buf, size_t &out_size); solver_result_t solve_memcmp(std::shared_ptr const& c, std::unique_ptr const& cm, const uint8_t *in_buf, size_t in_size, diff --git a/instrumentation/TaintPass.cpp b/instrumentation/TaintPass.cpp index 615928dc..64e3ace5 100644 --- a/instrumentation/TaintPass.cpp +++ b/instrumentation/TaintPass.cpp @@ -165,11 +165,33 @@ static cl::opt ClTraceGEPOffset( cl::desc("Trace GEP offset for solving."), cl::Hidden, cl::init(true)); -// Experimental feature, trace floating point operations +// Trace floating point operations (FP arithmetic, casts, FCmp, and common FP +// intrinsics). Reconstructed and solved by the z3 solver via the fpa theory. static cl::opt ClTraceFP( "taint-trace-float-pointer", cl::desc("Propagate taint for floating pointer instructions."), - cl::Hidden, cl::init(false)); + cl::Hidden, cl::init(true)); + +// Self-defined FP op codes. These MUST match the __dfsan::operators enum in +// runtime/dfsan/dfsan.h (last_llvm_op = 67 on LLVM 18). This file does not +// include dfsan.h, so — like the bswap Extract/Concat codes — they are hardcoded. +enum { + DfsanFpNeg = 89, // fp_neg (last_llvm_op + 22) + DfsanFpFabs = 90, // fp_fabs + DfsanFpSqrt = 91, // fp_sqrt + DfsanFpRound = 92, // fp_round (rounding selector in op1) + DfsanFpMin = 93, // fp_min + DfsanFpMax = 94, // fp_max + DfsanFpCopysign = 95, // fp_copysign +}; +// Rounding-mode selector (must match __dfsan::fp_rounding_mode in dfsan.h). +enum { + DfsanFpRmRna = 0, // round nearest, ties to away (llvm.round) + DfsanFpRmRne = 1, // round nearest, ties to even (llvm.rint/nearbyint) + DfsanFpRmRtp = 2, // round toward +inf (llvm.ceil) + DfsanFpRmRtn = 3, // round toward -inf (llvm.floor) + DfsanFpRmRtz = 4, // round toward zero (llvm.trunc) +}; static cl::opt ClTraceLoop( "taint-trace-loop", @@ -679,7 +701,7 @@ class TaintVisitor : public InstVisitor { return TF.F->getParent()->getDataLayout(); } - //void visitUnaryOperator(UnaryOperator &UO); + void visitUnaryOperator(UnaryOperator &UO); void visitBinaryOperator(BinaryOperator &BO); void visitCastInst(CastInst &CI); void visitCmpInst(CmpInst &CI); @@ -3446,11 +3468,23 @@ void TaintVisitor::visitStoreInst(StoreInst &SI) { TF.storeShadow(SI.getPointerOperand(), VT, Size, SI.getAlign(), Shadow, &SI); } -//void TaintVisitor::visitUnaryOperator(UnaryOperator &UO) { -//} +void TaintVisitor::visitUnaryOperator(UnaryOperator &UO) { + // The only unary operator in LLVM IR is FNeg. LLVM's FNeg opcode is a unary + // instruction which is not part of the __dfsan::operators enum (only binary, + // memory, cast and other insts are expanded from Instruction.def), so we map + // it to the self-defined __dfsan::fp_neg. + if (UO.getOpcode() != Instruction::FNeg) return; + if (!ClTraceFP) return; + Value *Shadow1 = TF.getShadow(UO.getOperand(0)); + // combineShadows reads UO.getOperand(0) directly and bitcasts the FP operand + // to an integer before the union call; the second operand stays zero. + Value *CombinedShadow = + TF.combineShadows(Shadow1, TF.TT.ZeroPrimitiveShadow, DfsanFpNeg, &UO); + TF.setShadow(&UO, CombinedShadow); +} void TaintVisitor::visitBinaryOperator(BinaryOperator &BO) { - if (BO.getType()->isFloatingPointTy()) return; + if (BO.getType()->isFloatingPointTy() && !ClTraceFP) return; Value *CombinedShadow = TF.combineBinaryOperatorShadows(&BO, BO.getOpcode()); TF.setShadow(&BO, CombinedShadow); @@ -4228,6 +4262,197 @@ bool TaintVisitor::visitWrappedCallBase(Function *F, CallBase &CB) { void TaintVisitor::visitIntrinsicCallBase(Function *F, CallBase &CB) { // filter some obvious ones StringRef FN = F->getName(); + + // Constrained FP intrinsics (llvm.experimental.constrained.*) carry an + // explicit rounding-mode operand. Default (non-strict) compilation never + // emits them -- plain fadd/fmul/... are round-to-nearest -- but targets built + // with strict FP / FENV_ACCESS (e.g. code that calls fesetround) do. Capture + // them HERE, before the blanket "llvm.experimental" filter below drops all + // taint, so the solver sees rounding-mode-correct arithmetic. The rounding + // selector is packed into the high byte of `op` (the same slot cmp uses for + // its predicate; FP arithmetic never carries a predicate). A compile-time + // constant mode packs a constant; the common round.dynamic case reads the live + // MXCSR via @llvm.get.rounding at runtime and ORs the mapped selector into + // `op` -- so `op` is a runtime value there, not a constant. This mirrors what + // the SMT-LIB benchmark path (driver/smttest.cpp) already carries in + // AstNode::index(); the read sides are parsers/rgd-parser.cpp (RGD/jigsaw) and + // solvers/z3-ts.cpp (fgtest union-table path). + if (ClTraceFP) { + Intrinsic::ID CId = F->getIntrinsicID(); + + // Constrained fcmp/fcmps: comparisons don't round, but strict FP lowers even + // `a < b` to these intrinsics, so without capturing them the branch loses all + // taint. Model exactly like a regular fcmp (combineCmpInstShadows): op = + // FCmp with the LLVM predicate in the high byte, op1/op2 = operand bit + // patterns, size = operand width. (fcmps is the signaling variant; the + // quiet/signaling distinction is a NaN-exception detail, irrelevant here.) + if (CId == Intrinsic::experimental_constrained_fcmp || + CId == Intrinsic::experimental_constrained_fcmps) { + Value *S1 = TF.getShadow(CB.getArgOperand(0)); + Value *S2 = TF.getShadow(CB.getArgOperand(1)); + if (TF.TT.isZeroShadow(S1) && TF.TT.isZeroShadow(S2)) + return; + Type *OpTy = CB.getArgOperand(0)->getType(); + if (OpTy->getScalarType()->getPrimitiveSizeInBits() <= 64) { + IRBuilder<> IRB(&CB); + auto &DL = CB.getModule()->getDataLayout(); + uint64_t Size = DL.getTypeSizeInBits(OpTy); + uint16_t Pred = + (uint16_t)cast(&CB)->getPredicate(); + uint16_t OpV = (uint16_t)Instruction::FCmp | (Pred << 8); + auto FpToInt = [&](Value *V) -> Value * { + Type *Ty = V->getType(); + if (Ty->isHalfTy()) V = IRB.CreateBitCast(V, TF.TT.Int16Ty); + else if (Ty->isFloatTy()) V = IRB.CreateBitCast(V, TF.TT.Int32Ty); + else if (Ty->isDoubleTy()) V = IRB.CreateBitCast(V, TF.TT.Int64Ty); + return IRB.CreateZExtOrTrunc(V, TF.TT.Int64Ty); + }; + CallInst *C = IRB.CreateCall( + TF.TT.TaintUnionFn, + {S1, S2, ConstantInt::get(TF.TT.Int16Ty, OpV), + ConstantInt::get(TF.TT.Int16Ty, Size), + FpToInt(CB.getArgOperand(0)), FpToInt(CB.getArgOperand(1))}); + C->addRetAttr(Attribute::ZExt); + C->addParamAttr(0, Attribute::ZExt); + C->addParamAttr(1, Attribute::ZExt); + TF.setShadow(&CB, C); + return; + } + } + } + + if (ClTraceFP && CB.getType()->isFloatingPointTy() && + CB.getType()->getScalarType()->getPrimitiveSizeInBits() <= 64) { + Intrinsic::ID CId = F->getIntrinsicID(); + uint16_t CFpOp = 0; // base opcode (LLVM opcode for arith; fp_sqrt) + bool CBinary = false, CIsSqrt = false, CTernary = false; + switch (CId) { + case Intrinsic::experimental_constrained_fadd: + CFpOp = Instruction::FAdd; CBinary = true; break; + case Intrinsic::experimental_constrained_fsub: + CFpOp = Instruction::FSub; CBinary = true; break; + case Intrinsic::experimental_constrained_fmul: + CFpOp = Instruction::FMul; CBinary = true; break; + case Intrinsic::experimental_constrained_fdiv: + CFpOp = Instruction::FDiv; CBinary = true; break; + case Intrinsic::experimental_constrained_sqrt: + CFpOp = DfsanFpSqrt; CIsSqrt = true; break; + case Intrinsic::experimental_constrained_fmuladd: + CTernary = true; break; // a*b + c, decomposed to FMul then FAdd + default: break; + } + if (CFpOp != 0 || CTernary) { + // Only the FP operands carry taint; the trailing metadata operands + // (rounding mode + exception behavior) never do, so check just those. + unsigned NumFp = CTernary ? 3 : (CBinary ? 2 : 1); + bool NeedInst = false; + for (unsigned I = 0; I < NumFp; ++I) { + if (!TF.TT.isZeroShadow(TF.getShadow(CB.getArgOperand(I)))) { + NeedInst = true; + break; + } + } + if (!NeedInst) + return; + + IRBuilder<> IRB(&CB); + auto &DL = CB.getModule()->getDataLayout(); + uint64_t Size = DL.getTypeSizeInBits(CB.getType()); + // FP operands are bitcast to same-width integers before the union call, + // matching combineShadows(). + auto FpToInt = [&](Value *V) -> Value * { + Type *Ty = V->getType(); + if (Ty->isHalfTy()) V = IRB.CreateBitCast(V, TF.TT.Int16Ty); + else if (Ty->isFloatTy()) V = IRB.CreateBitCast(V, TF.TT.Int32Ty); + else if (Ty->isDoubleTy()) V = IRB.CreateBitCast(V, TF.TT.Int64Ty); + return IRB.CreateZExtOrTrunc(V, TF.TT.Int64Ty); + }; + // Map an LLVM compile-time RoundingMode to the dfsan fp_rounding_mode + // selector, or -1 when it is round.dynamic / unknown (resolve at runtime). + auto StaticSel = [&](std::optional RM) -> int { + if (!RM.has_value()) + return -1; + switch (*RM) { + case RoundingMode::NearestTiesToEven: return DfsanFpRmRne; // 1 + case RoundingMode::TowardPositive: return DfsanFpRmRtp; // 2 + case RoundingMode::TowardNegative: return DfsanFpRmRtn; // 3 + case RoundingMode::TowardZero: return DfsanFpRmRtz; // 4 + case RoundingMode::NearestTiesToAway: return DfsanFpRmRna; // 0 + default: return -1; // Dynamic/Invalid + } + }; + std::optional RM = + cast(&CB)->getRoundingMode(); + int Sel = StaticSel(RM); + // Build the packed `op` value (i16) for a given base opcode, folding the + // rounding selector into the high byte. + auto PackedOp = [&](uint16_t Base) -> Value * { + if (Sel >= 0) + return ConstantInt::get(TF.TT.Int16Ty, + Base | (uint16_t(Sel) << 8)); + // round.dynamic: read the live rounding mode (FLT_ROUNDS encoding) and + // map it to our selector, then OR into the high byte at runtime. + // FLT_ROUNDS: 0=toward-zero, 1=to-nearest, 2=toward+inf, 3=toward-inf, + // 4=to-nearest-away. Default (incl. -1/indeterminate) -> RNE. + Value *Fr = IRB.CreateIntrinsic(Intrinsic::get_rounding, {}, {}); + Type *Ity = Fr->getType(); + auto C = [&](int v) { return ConstantInt::get(Ity, v); }; + Value *S = C(DfsanFpRmRne); + S = IRB.CreateSelect(IRB.CreateICmpEQ(Fr, C(4)), C(DfsanFpRmRna), S); + S = IRB.CreateSelect(IRB.CreateICmpEQ(Fr, C(3)), C(DfsanFpRmRtn), S); + S = IRB.CreateSelect(IRB.CreateICmpEQ(Fr, C(2)), C(DfsanFpRmRtp), S); + S = IRB.CreateSelect(IRB.CreateICmpEQ(Fr, C(0)), C(DfsanFpRmRtz), S); + Value *S16 = IRB.CreateZExtOrTrunc(S, TF.TT.Int16Ty); + Value *Hi = IRB.CreateShl(S16, ConstantInt::get(TF.TT.Int16Ty, 8)); + return IRB.CreateOr(Hi, ConstantInt::get(TF.TT.Int16Ty, Base)); + }; + auto MakeUnion = [&](Value *L1, Value *L2, Value *Op16, + Value *O1, Value *O2) -> Value * { + CallInst *C = IRB.CreateCall( + TF.TT.TaintUnionFn, + {L1, L2, Op16, ConstantInt::get(TF.TT.Int16Ty, Size), O1, O2}); + C->addRetAttr(Attribute::ZExt); + C->addParamAttr(0, Attribute::ZExt); + C->addParamAttr(1, Attribute::ZExt); + return C; + }; + Value *Zero64 = ConstantInt::get(TF.TT.Int64Ty, 0); + if (CBinary) { + Value *Res = MakeUnion(TF.getShadow(CB.getArgOperand(0)), + TF.getShadow(CB.getArgOperand(1)), PackedOp(CFpOp), + FpToInt(CB.getArgOperand(0)), + FpToInt(CB.getArgOperand(1))); + TF.setShadow(&CB, Res); + return; + } + if (CIsSqrt) { + // unary; l2 = 0. The operand value is unused (an instrumented unary + // intrinsic always has a symbolic operand), but pass it for symmetry. + Value *Res = MakeUnion(TF.getShadow(CB.getArgOperand(0)), + TF.TT.ZeroPrimitiveShadow, PackedOp(DfsanFpSqrt), + FpToInt(CB.getArgOperand(0)), Zero64); + TF.setShadow(&CB, Res); + return; + } + if (CTernary) { + // constrained fmuladd: a*b + c. Decompose into FMul then FAdd, both + // carrying the rounding selector (double-rounds vs a true fused op, but + // the <=1-ULP difference is immaterial for branch flipping), matching + // the non-constrained fma/fmuladd handling below. + Value *SA = TF.getShadow(CB.getArgOperand(0)); + Value *SB = TF.getShadow(CB.getArgOperand(1)); + Value *SC = TF.getShadow(CB.getArgOperand(2)); + Value *Mul = MakeUnion(SA, SB, PackedOp(Instruction::FMul), + FpToInt(CB.getArgOperand(0)), + FpToInt(CB.getArgOperand(1))); + Value *Res = MakeUnion(Mul, SC, PackedOp(Instruction::FAdd), Zero64, + FpToInt(CB.getArgOperand(2))); + TF.setShadow(&CB, Res); + return; + } + } + } + if ((FN).starts_with("llvm.va_") || // varabile length (FN).starts_with("llvm.gc") || // garbaage collection (FN).starts_with("llvm.experimental") || @@ -4297,6 +4522,124 @@ void TaintVisitor::visitIntrinsicCallBase(Function *F, CallBase &CB) { return; } + // Floating-point intrinsics: map to the self-defined FP ops so the z3 solver + // can reconstruct them via the fpa theory. Only modeled under ClTraceFP. + if (ClTraceFP) { + // fma / fmuladd compute a*b + c. A label node holds only two operands, so + // model the ternary by decomposition into FMul then FAdd, reusing the + // existing FP-arith solver support. This double-rounds relative to a true + // fused multiply-add, but the (at most 1-ULP) difference is immaterial for + // branch flipping. This path is essential, not optional: clang contracts + // the extremely common source pattern `a*b±c` into @llvm.fmuladd by default + // (-ffp-contract=on) even at -O0, so without this those branches vanish. + if ((IId == Intrinsic::fma || IId == Intrinsic::fmuladd) && + CB.getType()->isFloatingPointTy()) { + IRBuilder<> IRB(&CB); + auto &DL = CB.getModule()->getDataLayout(); + uint64_t Size = DL.getTypeSizeInBits(CB.getType()); + Value *SA = TF.getShadow(CB.getArgOperand(0)); + Value *SB = TF.getShadow(CB.getArgOperand(1)); + Value *SC = TF.getShadow(CB.getArgOperand(2)); + if (Size <= 64 && + !(TF.TT.isZeroShadow(SA) && TF.TT.isZeroShadow(SB) && + TF.TT.isZeroShadow(SC))) { + auto FpToInt = [&](Value *V) -> Value * { + Type *Ty = V->getType(); + if (Ty->isHalfTy()) V = IRB.CreateBitCast(V, TF.TT.Int16Ty); + else if (Ty->isFloatTy()) V = IRB.CreateBitCast(V, TF.TT.Int32Ty); + else if (Ty->isDoubleTy()) V = IRB.CreateBitCast(V, TF.TT.Int64Ty); + return IRB.CreateZExtOrTrunc(V, TF.TT.Int64Ty); + }; + auto MakeUnion = [&](Value *L1, Value *L2, uint16_t Op, + Value *O1, Value *O2) -> Value * { + CallInst *C = IRB.CreateCall( + TF.TT.TaintUnionFn, + {L1, L2, ConstantInt::get(TF.TT.Int16Ty, Op), + ConstantInt::get(TF.TT.Int16Ty, Size), O1, O2}); + C->addRetAttr(Attribute::ZExt); + C->addParamAttr(0, Attribute::ZExt); + C->addParamAttr(1, Attribute::ZExt); + return C; + }; + Value *Zero64 = ConstantInt::get(TF.TT.Int64Ty, 0); + // mul = a * b (LLVM opcodes match the __dfsan operators enum) + Value *Mul = MakeUnion(SA, SB, Instruction::FMul, + FpToInt(CB.getArgOperand(0)), + FpToInt(CB.getArgOperand(1))); + // result = mul + c. The mul result is symbolic, so its op1 slot is + // zeroed by the runtime anyway; the solver recomputes it from value_cache. + Value *Res = MakeUnion(Mul, SC, Instruction::FAdd, Zero64, + FpToInt(CB.getArgOperand(2))); + TF.setShadow(&CB, Res); + return; + } + } + uint16_t FpOp = 0; + uint64_t RoundingMode = 0; // rounding selector for fp_round (carried in op1) + bool IsBinary = false; + switch (IId) { + case Intrinsic::fabs: FpOp = DfsanFpFabs; break; + case Intrinsic::sqrt: FpOp = DfsanFpSqrt; break; + case Intrinsic::floor: FpOp = DfsanFpRound; RoundingMode = DfsanFpRmRtn; break; + case Intrinsic::ceil: FpOp = DfsanFpRound; RoundingMode = DfsanFpRmRtp; break; + case Intrinsic::trunc: FpOp = DfsanFpRound; RoundingMode = DfsanFpRmRtz; break; + case Intrinsic::round: FpOp = DfsanFpRound; RoundingMode = DfsanFpRmRna; break; + case Intrinsic::rint: FpOp = DfsanFpRound; RoundingMode = DfsanFpRmRne; break; + case Intrinsic::nearbyint: FpOp = DfsanFpRound; RoundingMode = DfsanFpRmRne; break; + case Intrinsic::minnum: FpOp = DfsanFpMin; IsBinary = true; break; + case Intrinsic::maxnum: FpOp = DfsanFpMax; IsBinary = true; break; + case Intrinsic::copysign: FpOp = DfsanFpCopysign; IsBinary = true; break; + // fma / fmuladd (3 operands) are handled above by decomposition. + default: break; + } + if (FpOp != 0 && CB.getType()->isFloatingPointTy()) { + IRBuilder<> IRB(&CB); + auto &DL = CB.getModule()->getDataLayout(); + uint64_t Size = DL.getTypeSizeInBits(CB.getType()); + // FP operands are bitcast to same-width integers before the union call, + // matching combineShadows(). + auto FpToInt = [&](Value *V) -> Value * { + Type *Ty = V->getType(); + if (Ty->isHalfTy()) V = IRB.CreateBitCast(V, TF.TT.Int16Ty); + else if (Ty->isFloatTy()) V = IRB.CreateBitCast(V, TF.TT.Int32Ty); + else if (Ty->isDoubleTy()) V = IRB.CreateBitCast(V, TF.TT.Int64Ty); + return IRB.CreateZExtOrTrunc(V, TF.TT.Int64Ty); + }; + Value *Op = ConstantInt::get(TF.TT.Int16Ty, FpOp); + Value *SizeV = ConstantInt::get(TF.TT.Int16Ty, Size); + Value *Shadow1 = TF.getShadow(CB.getArgOperand(0)); + Value *Result = nullptr; + if (IsBinary) { + Value *Shadow2 = TF.getShadow(CB.getArgOperand(1)); + Value *Op1 = FpToInt(CB.getArgOperand(0)); + Value *Op2 = FpToInt(CB.getArgOperand(1)); + CallInst *C = IRB.CreateCall(TF.TT.TaintUnionFn, + {Shadow1, Shadow2, Op, SizeV, Op1, Op2}); + C->addRetAttr(Attribute::ZExt); + C->addParamAttr(0, Attribute::ZExt); + C->addParamAttr(1, Attribute::ZExt); + Result = C; + } else { + // unary; l2 = 0. For fp_round the rounding selector is carried in op1 + // (the operand value is unused: an instrumented unary intrinsic always + // has a symbolic operand, so the solver rebuilds it from l1). + Value *Op1 = (FpOp == DfsanFpRound) + ? ConstantInt::get(TF.TT.Int64Ty, RoundingMode) + : FpToInt(CB.getArgOperand(0)); + Value *Op2 = ConstantInt::get(TF.TT.Int64Ty, 0); + CallInst *C = IRB.CreateCall( + TF.TT.TaintUnionFn, + {Shadow1, TF.TT.ZeroPrimitiveShadow, Op, SizeV, Op1, Op2}); + C->addRetAttr(Attribute::ZExt); + C->addParamAttr(0, Attribute::ZExt); + C->addParamAttr(1, Attribute::ZExt); + Result = C; + } + TF.setShadow(&CB, Result); + return; + } + } + // Other intrinsics: symbolic propagation not yet implemented — skip. } diff --git a/parsers/rgd-parser.cpp b/parsers/rgd-parser.cpp index 8e607984..7e20588a 100644 --- a/parsers/rgd-parser.cpp +++ b/parsers/rgd-parser.cpp @@ -5,6 +5,8 @@ #include "union_find.h" #include "parse-rgd.h" +#include +#include #include using namespace rgd; @@ -70,6 +72,57 @@ static const std::unordered_map > OP_ {RELATIONAL_ICMP(__dfsan::bvslt), {rgd::Slt, "slt"}}, {RELATIONAL_ICMP(__dfsan::bvsle), {rgd::Sle, "sle"}}, #undef RELATIONAL_ICMP + // floating-point arithmetic (FAdd/FSub/FMul/FDiv/FRem reuse LLVM opcodes) + {__dfsan::FAdd, {rgd::FAdd, "fadd"}}, + {__dfsan::FSub, {rgd::FSub, "fsub"}}, + {__dfsan::FMul, {rgd::FMul, "fmul"}}, + {__dfsan::FDiv, {rgd::FDiv, "fdiv"}}, + {__dfsan::FRem, {rgd::FRem, "frem"}}, + {__dfsan::fp_neg, {rgd::FNeg, "fneg"}}, + // floating-point casts + {__dfsan::FPToUI, {rgd::FpToUi, "fptoui"}}, + {__dfsan::FPToSI, {rgd::FpToSi, "fptosi"}}, + {__dfsan::UIToFP, {rgd::UiToFp, "uitofp"}}, + {__dfsan::SIToFP, {rgd::SiToFp, "sitofp"}}, + {__dfsan::FPTrunc, {rgd::FpTrunc, "fptrunc"}}, + {__dfsan::FPExt, {rgd::FpExt, "fpext"}}, + // floating-point intrinsics / libcalls + {__dfsan::fp_fabs, {rgd::FpFabs, "fabs"}}, + {__dfsan::fp_sqrt, {rgd::FpSqrt, "sqrt"}}, + {__dfsan::fp_round, {rgd::FpRound, "fround"}}, + {__dfsan::fp_min, {rgd::FpMin, "fmin"}}, + {__dfsan::fp_max, {rgd::FpMax, "fmax"}}, + {__dfsan::fp_copysign, {rgd::FpCopysign, "copysign"}}, + {__dfsan::fp_is_nan, {rgd::FpIsNan, "isnan"}}, + {__dfsan::fp_is_inf, {rgd::FpIsInf, "isinf"}}, + {__dfsan::fp_is_finite, {rgd::FpIsFinite, "isfinite"}}, + {__dfsan::fp_signbit, {rgd::FpSignbit, "signbit"}}, + {__dfsan::fp_lrint, {rgd::FpLrint, "lrint"}}, + // floating-point transcendentals (i2s-only: z3/jigsaw reject them) + {__dfsan::fp_exp, {rgd::FpExp, "exp"}}, + {__dfsan::fp_exp2, {rgd::FpExp2, "exp2"}}, + {__dfsan::fp_log, {rgd::FpLog, "log"}}, + {__dfsan::fp_log2, {rgd::FpLog2, "log2"}}, + {__dfsan::fp_log10, {rgd::FpLog10, "log10"}}, + {__dfsan::fp_log1p, {rgd::FpLog1p, "log1p"}}, + {__dfsan::fp_pow, {rgd::FpPow, "pow"}}, + // floating-point comparisons (predicate encoded in the high byte, same as ICmp) +#define RELATIONAL_FCMP(cmp) (__dfsan::FCmp | (cmp << 8)) + {RELATIONAL_FCMP(1), {rgd::FOeq, "foeq"}}, + {RELATIONAL_FCMP(2), {rgd::FOgt, "fogt"}}, + {RELATIONAL_FCMP(3), {rgd::FOge, "foge"}}, + {RELATIONAL_FCMP(4), {rgd::FOlt, "folt"}}, + {RELATIONAL_FCMP(5), {rgd::FOle, "fole"}}, + {RELATIONAL_FCMP(6), {rgd::FOne, "fone"}}, + {RELATIONAL_FCMP(7), {rgd::FOrd, "ford"}}, + {RELATIONAL_FCMP(8), {rgd::FUno, "funo"}}, + {RELATIONAL_FCMP(9), {rgd::FUeq, "fueq"}}, + {RELATIONAL_FCMP(10), {rgd::FUgt, "fugt"}}, + {RELATIONAL_FCMP(11), {rgd::FUge, "fuge"}}, + {RELATIONAL_FCMP(12), {rgd::FUlt, "fult"}}, + {RELATIONAL_FCMP(13), {rgd::FUle, "fule"}}, + {RELATIONAL_FCMP(14), {rgd::FUne, "fune"}}, +#undef RELATIONAL_FCMP }; static inline bool is_rel_cmp(uint16_t op, __dfsan::predicate pred) { @@ -95,6 +148,44 @@ static inline bool eval_icmp(uint16_t op, uint64_t op1, uint64_t op2) { return false; } +// Decode an IEEE-754 bit pattern into a C double (widening 32-bit floats). +static inline double fp_decode(uint64_t bits_val, uint8_t bits) { + if (bits == 64) { + double d; memcpy(&d, &bits_val, sizeof(d)); return d; + } else if (bits == 32) { + uint32_t u = (uint32_t)bits_val; float f; memcpy(&f, &u, sizeof(f)); return (double)f; + } + // half and other widths: not decoded for concrete evaluation + return 0.0; +} + +// Concrete evaluation of an FCmp given the LLVM predicate (0..15) and the +// IEEE-754 bit patterns of the operands. Used to constant-fold a fully +// concretized comparison during root discovery (mirrors eval_icmp). +static inline bool eval_fcmp(uint16_t predicate, uint64_t val1, uint64_t val2, uint8_t bits) { + double a = fp_decode(val1, bits), b = fp_decode(val2, bits); + bool ord = !(std::isnan(a) || std::isnan(b)); + switch (predicate) { + case 0: return false; // FCMP_FALSE + case 1: return ord && a == b; // FCMP_OEQ + case 2: return ord && a > b; // FCMP_OGT + case 3: return ord && a >= b; // FCMP_OGE + case 4: return ord && a < b; // FCMP_OLT + case 5: return ord && a <= b; // FCMP_OLE + case 6: return ord && a != b; // FCMP_ONE + case 7: return ord; // FCMP_ORD + case 8: return !ord; // FCMP_UNO + case 9: return !ord || a == b; // FCMP_UEQ + case 10: return !ord || a > b; // FCMP_UGT + case 11: return !ord || a >= b; // FCMP_UGE + case 12: return !ord || a < b; // FCMP_ULT + case 13: return !ord || a <= b; // FCMP_ULE + case 14: return !ord || a != b; // FCMP_UNE + case 15: return true; // FCMP_TRUE + default: return false; + } +} + static void printAst(FILE* f, const rgd::AstNode *node, int indent) { fprintf(f, "(%s, ", rgd::AstKindName[node->kind()]); fprintf(f, "%d, ", node->label()); @@ -370,8 +461,19 @@ bool RGDAstParser::do_uta_rel(dfsan_label label, rgd::AstNode *ret, return false; } - // common ops, make sure no special ops - auto op_itr = OP_MAP.find(info->op); + // common ops, make sure no special ops. + // FP arithmetic (FAdd/FSub/FMul/FDiv) and fp_sqrt may carry a rounding-mode + // selector in the high byte of `op` (see instrumentation/TaintPass.cpp and + // driver/smttest.cpp); FRem never carries one (frem has no rounding). cmp + // ops keep their predicate packed in the high byte and have per-predicate + // OP_MAP entries, so only mask the FP-arith kinds before the lookup. + uint16_t op_lo = info->op & 0xff; + bool is_fp_arith_rm = + op_lo == __dfsan::FAdd || op_lo == __dfsan::FSub || + op_lo == __dfsan::FMul || op_lo == __dfsan::FDiv || + op_lo == __dfsan::fp_sqrt; + uint16_t lookup_op = is_fp_arith_rm ? op_lo : info->op; + auto op_itr = OP_MAP.find(lookup_op); if (op_itr == OP_MAP.end()) { WARNF("invalid op: %u\n", info->op); return false; @@ -406,7 +508,8 @@ bool RGDAstParser::do_uta_rel(dfsan_label label, rgd::AstNode *ret, visited.insert(info->l1); } else { if (unlikely(needs_concretization)) { - if (unlikely(!rgd::isRelationalKind(ret->kind()))) { + if (unlikely(!rgd::isRelationalKind(ret->kind()) && + !rgd::isFPRelationalKind(ret->kind()))) { WARNF("invalid kind for concretization %u\n", ret->kind()); return false; } @@ -439,12 +542,43 @@ bool RGDAstParser::do_uta_rel(dfsan_label label, rgd::AstNode *ret, #endif } - // unary ops + // unary ops. FP casts (FPToUI/FPToSI/UIToFP/SIToFP/FPTrunc/FPExt), FP + // unary intrinsics (fneg/fabs/sqrt/round), the FP predicate/rounding + // libcalls (isnan/isinf/finite/signbit/lrint) and the unary transcendentals + // (exp/exp2/log/log2/log10/log1p) all take a single operand in l1, so they + // short-circuit here before a (nonexistent) right child is built. (pow is + // binary and goes through the normal binary path below.) + // fp_sqrt may carry a rounding selector in the high byte, so compare on op_lo. + bool is_fp_unary = + info->op == __dfsan::FPToUI || info->op == __dfsan::FPToSI || + info->op == __dfsan::UIToFP || info->op == __dfsan::SIToFP || + info->op == __dfsan::FPTrunc || info->op == __dfsan::FPExt || + info->op == __dfsan::fp_neg || info->op == __dfsan::fp_fabs || + op_lo == __dfsan::fp_sqrt || info->op == __dfsan::fp_round || + info->op == __dfsan::fp_is_nan || info->op == __dfsan::fp_is_inf || + info->op == __dfsan::fp_is_finite || info->op == __dfsan::fp_signbit || + info->op == __dfsan::fp_lrint || + info->op == __dfsan::fp_exp || info->op == __dfsan::fp_exp2 || + info->op == __dfsan::fp_log || info->op == __dfsan::fp_log2 || + info->op == __dfsan::fp_log10 || info->op == __dfsan::fp_log1p; if (info->op == __dfsan::ZExt || info->op == __dfsan::SExt || - info->op == __dfsan::Extract || info->op == __dfsan::Trunc) { + info->op == __dfsan::Extract || info->op == __dfsan::Trunc || + is_fp_unary) { + // Extract carries a bit offset in op2; fp_round carries its rounding-mode + // selector (fp_rounding_mode) in op1; constrained fp_sqrt carries its + // selector in the high byte of op. All are stashed in index(). + uint64_t offset = info->op == __dfsan::Extract ? info->op2.i : + (info->op == __dfsan::fp_round ? info->op1.i : + (op_lo == __dfsan::fp_sqrt ? (info->op >> 8) : 0)); uint32_t hash = rgd::xxhash(info->size, ret->kind(), left->hash()); + // Fold the rounding selector into the hash for FP ops whose codegen depends + // on it (fp_round: floor/ceil/trunc; fp_sqrt: directed rounding). fCache + // buckets by hash() and confirms with isEqualAstRecursive, which ignores + // index() -- so without this, floor vs ceil (or RNE vs directed sqrt) over + // the same child would collide and reuse wrong-mode compiled code. + if (info->op == __dfsan::fp_round || op_lo == __dfsan::fp_sqrt) + hash = rgd::xxhash(hash, (uint32_t)offset, ret->kind()); ret->set_hash(hash); - uint64_t offset = info->op == __dfsan::Extract ? info->op2.i : 0; ret->set_index(offset); return true; } @@ -461,7 +595,8 @@ bool RGDAstParser::do_uta_rel(dfsan_label label, rgd::AstNode *ret, visited.insert(info->l2); } else { if (unlikely(needs_concretization)) { - if (unlikely(!rgd::isRelationalKind(ret->kind()))) { + if (unlikely(!rgd::isRelationalKind(ret->kind()) && + !rgd::isFPRelationalKind(ret->kind()))) { WARNF("invalid kind for concretization %u\n", ret->kind()); return false; } @@ -495,7 +630,7 @@ bool RGDAstParser::do_uta_rel(dfsan_label label, rgd::AstNode *ret, } // record comparison operands - if (rgd::isRelationalKind(ret->kind())) { + if (rgd::isRelationalKind(ret->kind()) || rgd::isFPRelationalKind(ret->kind())) { constraint->op1 = info->op1.i; constraint->op2 = info->op2.i; } @@ -504,6 +639,17 @@ bool RGDAstParser::do_uta_rel(dfsan_label label, rgd::AstNode *ret, // as long as the operands are the same, we can reuse the AST/function uint32_t kind = rgd::isRelationalKind(ret->kind()) ? rgd::Bool : ret->kind(); uint32_t hash = rgd::xxhash(left->hash(), (kind << 16) | ret->bits(), right->hash()); + // FP arithmetic (FAdd/FSub/FMul/FDiv; not FRem) may carry a rounding selector + // in the high byte of op. Stash it in index() so the JIT (jit.cc) and the RGD + // z3 path (z3-solver.cpp) emit rounding-mode-correct arithmetic, and fold it + // into the hash so fCache never reuses wrong-mode code (isEqualAstRecursive + // ignores index()). sqrt is unary and returned above. + if (op_lo == __dfsan::FAdd || op_lo == __dfsan::FSub || + op_lo == __dfsan::FMul || op_lo == __dfsan::FDiv) { + uint32_t sel = info->op >> 8; + ret->set_index(sel); + hash = rgd::xxhash(hash, sel, kind); + } ret->set_hash(hash); return true; @@ -515,7 +661,9 @@ RGDAstParser::constraint_t RGDAstParser::parse_constraint(dfsan_label label) { // make sure root is a comparison node // XXX: root should never go oob? dfsan_label_info *info = get_label_info(label); - if (unlikely(((info->op & 0xff) != __dfsan::ICmp) && (info->op != __dfsan::fmemcmp))) { + if (unlikely(((info->op & 0xff) != __dfsan::ICmp) && + ((info->op & 0xff) != __dfsan::FCmp) && + (info->op != __dfsan::fmemcmp))) { WARNF("invalid root node %u, non-comparison root op: %u\n", label, info->op); return nullptr; } @@ -581,8 +729,10 @@ dfsan_label RGDAstParser::strip_zext(dfsan_label label) { if (info->size == 1) { // extending a boolean value return child; - } else if ((info->op & 0xff) == __dfsan::ICmp || info->op == __dfsan::fmemcmp) { - // extending the result of icmp or memcmp + } else if ((info->op & 0xff) == __dfsan::ICmp || + (info->op & 0xff) == __dfsan::FCmp || + info->op == __dfsan::fmemcmp) { + // extending the result of icmp, fcmp or memcmp return child; } } @@ -1029,6 +1179,65 @@ int RGDAstParser::find_roots(dfsan_label label, AstNode *ret, node->set_boolvalue(eval_icmp(info->op, info->op1.i, info->op2.i)); node->clear_children(); } + } else if ((info->op & 0xff) == __dfsan::FCmp) { + // fcmp node (relational leaf). Unlike icmp, both operands are FP + // values, so an fcmp never has a nested comparison child -- the + // children_size should always be 0 here. + node->set_bits(1); + if (likely(node->children_size() == 0)) { + // check size, concretize if too large (mirror the icmp leaf path) + auto size = ast_size_cache.at(curr); + auto citr = concretize_node.find(curr); + uint8_t concretize = (citr != concretize_node.end() ? citr->second : 0); + if (size > max_ast_size_) { + DEBUGF("AST size too large: %d = %u\n", curr, size); + auto left_size = ast_size_cache.at(info->l1); + auto right_size = ast_size_cache.at(info->l2); + if (left_size > max_ast_size_) { + concretize |= 1; + size -= (left_size - 1); + } + if (right_size > max_ast_size_) { + concretize |= 2; + size -= (right_size - 1); + } + DEBUGF("new size: %d = %u\n", curr, size); + ast_size_cache[curr] = size; + concretize_node[curr] = concretize; + } + + // check for concrete ops + uint8_t concrete_ops = concretize; + concrete_ops |= info->l1 == 0 ? 1 : 0; + concrete_ops |= info->l2 == 0 ? 2 : 0; + if (concrete_ops == 3) { + // both sides concrete, constant-fold the comparison. For a cmp + // node info->size is the operand width (see TaintPass), which is + // exactly what eval_fcmp needs to decode the IEEE bit patterns. + node->set_kind(rgd::Bool); + node->set_boolvalue(eval_fcmp(info->op >> 8, info->op1.i, info->op2.i, info->size)); + } else { + auto itr = OP_MAP.find(info->op); + if (unlikely(itr == OP_MAP.end())) { + WARNF("invalid fcmp op: %d\n", info->op); + return INVALID_NODE; + } + node->set_kind(itr->second.first); + node->set_label(curr); +#ifdef DEBUG + subroots.insert(curr); +#endif + } + } else { + // unexpected nested comparison inside an FP operand; constant-fold + uint32_t opw = 64; + if (info->l1 != 0) opw = get_label_info(info->l1)->size; + else if (info->l2 != 0) opw = get_label_info(info->l2)->size; + WARNF("unexpected nested cmp under fcmp: %d\n", info->op); + node->set_kind(rgd::Bool); + node->set_boolvalue(eval_fcmp(info->op >> 8, info->op1.i, info->op2.i, opw)); + node->clear_children(); + } } else if (info->op == __dfsan::fmemcmp) { // memcmp is also considered as a root node (relational comparison) if (unlikely(node->children_size() != 0)) { @@ -1151,7 +1360,8 @@ bool RGDAstParser::scan_labels(dfsan_label label) { uint8_t nested = 0; nested += info->l1 == 0 ? 0 : nested_cmp_cache[info->l1]; nested += info->l2 == 0 ? 0 : nested_cmp_cache[info->l2]; - if (info->op == __dfsan::fmemcmp || (info->op & 0xff) == __dfsan::ICmp) + if (info->op == __dfsan::fmemcmp || (info->op & 0xff) == __dfsan::ICmp || + (info->op & 0xff) == __dfsan::FCmp) nested += 1; nested_cmp_cache.push_back(nested); } @@ -1244,7 +1454,8 @@ int RGDAstParser::to_nnf(bool expected_r, rgd::AstNode *node) { if (unlikely(ret != 0)) { return ret; } } else { // leaf node - if (rgd::isRelationalKind(node->kind())) { + if (rgd::isRelationalKind(node->kind()) || + rgd::isFPRelationalKind(node->kind())) { node->set_kind(rgd::negate_cmp(node->kind())); } else if (node->kind() == rgd::Memcmp) { // memcmp is also considered as a leaf node (relational comparison) diff --git a/runtime/dfsan/dfsan.cpp b/runtime/dfsan/dfsan.cpp index 7d7cc42c..29f79ca8 100644 --- a/runtime/dfsan/dfsan.cpp +++ b/runtime/dfsan/dfsan.cpp @@ -277,12 +277,21 @@ dfsan_label __taint_union(dfsan_label l1, dfsan_label l2, uint16_t op, 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::fmemcmp && + } else if ((op & 0xff) < __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 + (op & 0xff) != __dfsan::ICmp && + (op & 0xff) != __dfsan::FCmp) { + // mask to the base opcode: FP arithmetic (FAdd/FSub/FMul/FDiv) may pack a + // rounding-mode selector into the high byte, and must still zero op1/op2 for + // symbolic operands so identical symbolic nodes dedup (the operand values + // are irrelevant once symbolic; the selector lives in op, not op1/op2). + // Not a higher-order op and not Alloca/ICmp/FCmp/PtrToInt - zero out for + // symbolic operands. + // PtrToInt needs op1 preserved to compute base pointer for string ops. + // FCmp, like ICmp, keeps both operand bit patterns so the solver can + // validate/evaluate the comparison (FILTER_WRONG_AST) without re-deriving + // the concrete FP values. if (l1 >= CONST_OFFSET) op1 = 0; if (l2 >= CONST_OFFSET) op2 = 0; } diff --git a/runtime/dfsan/dfsan.h b/runtime/dfsan/dfsan.h index 916c64a5..0b294f55 100644 --- a/runtime/dfsan/dfsan.h +++ b/runtime/dfsan/dfsan.h @@ -203,7 +203,52 @@ enum operators { 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 flength = last_llvm_op + 21, // 88 z3::length(str_var), Int sort - LastOp = last_llvm_op + 22, // 89 + // floating-point ops. Binary FP arithmetic (FAdd/FSub/FMul/FDiv/FRem), + // FP casts (FPToUI/FPToSI/UIToFP/SIToFP/FPTrunc/FPExt) and FCmp reuse the LLVM + // opcodes directly (they are already in this enum via Instruction.def). The + // ops below are the ones LLVM does *not* give us a usable opcode for: FNeg is a + // unary instruction (Instruction.def UNARY insts are not expanded here) and the + // FP intrinsics have no opcode at all. They are placed in [Add, LastOp) so + // is_valid_op() accepts them. + fp_neg = last_llvm_op + 22, // 89 fneg (llvm FNeg is unary, not in enum) + fp_fabs = last_llvm_op + 23, // 90 llvm.fabs + fp_sqrt = last_llvm_op + 24, // 91 llvm.sqrt + fp_round = last_llvm_op + 25, // 92 round-to-integral; rounding mode in op1 + fp_min = last_llvm_op + 26, // 93 llvm.minnum + fp_max = last_llvm_op + 27, // 94 llvm.maxnum + fp_copysign = last_llvm_op + 28, // 95 llvm.copysign + // FP predicates + rounding-to-int libcalls modeled as custom wrappers (see + // done_abilist.txt / dfsan_custom.cpp). SymSan has no working "functional" + // ABI (WK_Functional is a no-op that drops taint), so these must build real + // op nodes for the solver. Predicate results are 0/1 integers; fp_lrint is + // round-to-nearest (RNE) then convert to a signed integer. + fp_is_nan = last_llvm_op + 29, // 96 isnan/isnanf + fp_is_inf = last_llvm_op + 30, // 97 isinf/isinff and __isinf/__isinff + fp_is_finite = last_llvm_op + 31, // 98 finite/finitef + fp_signbit = last_llvm_op + 32, // 99 __signbit/__signbitf + fp_lrint = last_llvm_op + 33, // 100 lrint/lrintf/llrint/llrintf + // FP transcendentals modeled as custom wrappers (see done_abilist.txt / + // dfsan_custom.cpp). z3 cannot invert them and jigsaw is integer-only, so + // only the i2s solver flips these guards (it computes the numeric libm + // inverse and verifies). fp_pow is binary (base, exponent). + fp_exp = last_llvm_op + 34, // 101 exp/expf + fp_exp2 = last_llvm_op + 35, // 102 exp2 + fp_log = last_llvm_op + 36, // 103 log/logf + fp_log2 = last_llvm_op + 37, // 104 log2/log2f + fp_log10 = last_llvm_op + 38, // 105 log10 + fp_log1p = last_llvm_op + 39, // 106 log1p/log1pf + fp_pow = last_llvm_op + 40, // 107 pow/powf + LastOp = last_llvm_op + 41, // 108 +}; + +// rounding-mode selector carried in op1 for fp_round, and used when lowering FP +// arithmetic in the solver. Values match z3::rounding_mode ordering. +enum fp_rounding_mode { + fp_rm_rna = 0, // round nearest, ties to away (llvm.round) + fp_rm_rne = 1, // round nearest, ties to even (llvm.rint/nearbyint, default) + fp_rm_rtp = 2, // round toward +inf (llvm.ceil) + fp_rm_rtn = 3, // round toward -inf (llvm.floor) + fp_rm_rtz = 4, // round toward zero (llvm.trunc) }; // Flag packed into the high bits of a fatoi label's op1 (which otherwise holds @@ -246,13 +291,21 @@ static inline uint8_t get_const_result(uint64_t c1, uint64_t c2, uint32_t predic } static inline bool is_commutative(uint16_t op) { - switch(op) { + // mask to the base opcode: cmp packs a predicate and FP arithmetic may pack a + // rounding-mode selector into the high byte (neither changes commutativity). + switch(op & 0xff) { case Not: case And: case Or: case Xor: case Add: case Mul: + // FP add/mul/min/max are commutative (NaN propagation and signed-zero + // results are symmetric), so operands may be swapped for dedup. + case FAdd: + case FMul: + case fp_min: + case fp_max: case fmemcmp: case fstrcmp: return true; diff --git a/runtime/dfsan/dfsan_custom.cpp b/runtime/dfsan/dfsan_custom.cpp index 1ed98b5c..4e5cd728 100644 --- a/runtime/dfsan/dfsan_custom.cpp +++ b/runtime/dfsan/dfsan_custom.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -1043,6 +1044,155 @@ uint64_t __dfsw___bswapdi2(uint64_t x, dfsan_label x_label, return ret; } +// --------------------------------------------------------------------------- +// Floating-point libc math wrappers. +// +// At -O0, clang keeps several math functions as libcalls (e.g. sqrt, due to +// errno) instead of lowering them to @llvm.* intrinsics, so the instrumentation +// intrinsic path never sees them. We model them here as custom wrappers that +// build the corresponding self-defined FP op node (see the fp_* ops in +// dfsan.h), matching what visitIntrinsicCallBase would have produced. The FP +// argument's IEEE-754 bit pattern is passed in op1/op2 so constant operands and +// the solver's value cache stay consistent with the BV-encoded FP labels. +// --------------------------------------------------------------------------- + +static inline uint64_t fp64_bits(double x) { + uint64_t b; + internal_memcpy(&b, &x, sizeof(b)); + return b; +} + +static inline uint32_t fp32_bits(float x) { + uint32_t b; + internal_memcpy(&b, &x, sizeof(b)); + return b; +} + +// Unary intrinsic-style math functions (double/float). +#define DFSW_FP_UNARY(name, cfn, op, sel) \ + SANITIZER_INTERFACE_ATTRIBUTE \ + double __dfsw_##name(double x, dfsan_label x_label, \ + dfsan_label *ret_label) { \ + double ret = cfn(x); \ + *ret_label = dfsan_union(x_label, 0, op, 64, (sel), 0); \ + return ret; \ + } \ + SANITIZER_INTERFACE_ATTRIBUTE \ + float __dfsw_##name##f(float x, dfsan_label x_label, \ + dfsan_label *ret_label) { \ + float ret = cfn##f(x); \ + *ret_label = dfsan_union(x_label, 0, op, 32, (sel), 0); \ + return ret; \ + } + +DFSW_FP_UNARY(sqrt, sqrt, __dfsan::fp_sqrt, 0) +DFSW_FP_UNARY(fabs, fabs, __dfsan::fp_fabs, 0) +DFSW_FP_UNARY(floor, floor, __dfsan::fp_round, __dfsan::fp_rm_rtn) +DFSW_FP_UNARY(ceil, ceil, __dfsan::fp_round, __dfsan::fp_rm_rtp) +DFSW_FP_UNARY(trunc, trunc, __dfsan::fp_round, __dfsan::fp_rm_rtz) +DFSW_FP_UNARY(round, round, __dfsan::fp_round, __dfsan::fp_rm_rna) +DFSW_FP_UNARY(rint, rint, __dfsan::fp_round, __dfsan::fp_rm_rne) +DFSW_FP_UNARY(nearbyint, nearbyint, __dfsan::fp_round, __dfsan::fp_rm_rne) + +// Transcendentals. Marked =custom (not =functional) so the operand's taint is +// preserved: z3 cannot invert these but the i2s solver flips the guard by +// computing the numeric libm inverse and verifying it. (log10/exp2 have no +// float variant in the abilist; the generated __dfsw_*f wrappers are unused.) +DFSW_FP_UNARY(exp, exp, __dfsan::fp_exp, 0) +DFSW_FP_UNARY(exp2, exp2, __dfsan::fp_exp2, 0) +DFSW_FP_UNARY(log, log, __dfsan::fp_log, 0) +DFSW_FP_UNARY(log2, log2, __dfsan::fp_log2, 0) +DFSW_FP_UNARY(log10, log10, __dfsan::fp_log10, 0) +DFSW_FP_UNARY(log1p, log1p, __dfsan::fp_log1p, 0) + +#undef DFSW_FP_UNARY + +// Binary intrinsic-style math functions (double/float). The IEEE bits of both +// operands are passed so a concrete (label-0) operand still solves correctly. +#define DFSW_FP_BINARY(name, cfn, op) \ + SANITIZER_INTERFACE_ATTRIBUTE \ + double __dfsw_##name(double a, double b, dfsan_label a_label, \ + dfsan_label b_label, dfsan_label *ret_label) { \ + double ret = cfn(a, b); \ + *ret_label = dfsan_union(a_label, b_label, op, 64, \ + fp64_bits(a), fp64_bits(b)); \ + return ret; \ + } \ + SANITIZER_INTERFACE_ATTRIBUTE \ + float __dfsw_##name##f(float a, float b, dfsan_label a_label, \ + dfsan_label b_label, dfsan_label *ret_label) { \ + float ret = cfn##f(a, b); \ + *ret_label = dfsan_union(a_label, b_label, op, 32, \ + fp32_bits(a), fp32_bits(b)); \ + return ret; \ + } + +DFSW_FP_BINARY(fmin, fmin, __dfsan::fp_min) +DFSW_FP_BINARY(fmax, fmax, __dfsan::fp_max) +DFSW_FP_BINARY(copysign, copysign, __dfsan::fp_copysign) +// pow(base, exp): i2s inverts against whichever operand is a constant (the +// concrete, label-0 operand); its IEEE bits arrive via op1/op2 above. +DFSW_FP_BINARY(pow, pow, __dfsan::fp_pow) + +#undef DFSW_FP_BINARY + +// FP predicates (double/float -> int): isnan/isinf/__isinf/finite/__signbit. +// SymSan has no working "functional" ABI, so these must build a real op node +// recording the predicate; otherwise a branch like `if (isnan(x))` drops the +// operand's taint and can never be flipped. The concrete result is computed +// with __builtin_* (the libc names are macros that would otherwise recurse). +// The node is created only when the argument is tainted; the operand's IEEE +// bits reach the solver via the child label's value cache (like DFSW_FP_UNARY). +#define DFSW_FP_PRED(name, bfn, op) \ + SANITIZER_INTERFACE_ATTRIBUTE \ + int __dfsw_##name(double x, dfsan_label x_label, \ + dfsan_label *ret_label) { \ + int ret = bfn(x); \ + *ret_label = x_label ? dfsan_union(x_label, 0, op, 32, 0, 0) : 0; \ + return ret; \ + } \ + SANITIZER_INTERFACE_ATTRIBUTE \ + int __dfsw_##name##f(float x, dfsan_label x_label, \ + dfsan_label *ret_label) { \ + int ret = bfn(x); \ + *ret_label = x_label ? dfsan_union(x_label, 0, op, 32, 0, 0) : 0; \ + return ret; \ + } + +DFSW_FP_PRED(isnan, __builtin_isnan, __dfsan::fp_is_nan) +DFSW_FP_PRED(isinf, __builtin_isinf, __dfsan::fp_is_inf) +DFSW_FP_PRED(__isinf, __builtin_isinf, __dfsan::fp_is_inf) +DFSW_FP_PRED(finite, __builtin_isfinite, __dfsan::fp_is_finite) +DFSW_FP_PRED(__signbit, __builtin_signbit, __dfsan::fp_signbit) + +#undef DFSW_FP_PRED + +// Round-to-nearest-integer libcalls (lrint/llrint, double/float). Modeled as +// fp_lrint: round with the default mode (RNE) then convert to a signed integer. +// Result width is 64 bits (long / long long on this platform). +#define DFSW_FP_LRINT(name, cfn, ret_t) \ + SANITIZER_INTERFACE_ATTRIBUTE \ + ret_t __dfsw_##name(double x, dfsan_label x_label, \ + dfsan_label *ret_label) { \ + ret_t ret = cfn(x); \ + *ret_label = x_label ? \ + dfsan_union(x_label, 0, __dfsan::fp_lrint, 64, 0, 0) : 0; \ + return ret; \ + } \ + SANITIZER_INTERFACE_ATTRIBUTE \ + ret_t __dfsw_##name##f(float x, dfsan_label x_label, \ + dfsan_label *ret_label) { \ + ret_t ret = cfn##f(x); \ + *ret_label = x_label ? \ + dfsan_union(x_label, 0, __dfsan::fp_lrint, 64, 0, 0) : 0; \ + return ret; \ + } + +DFSW_FP_LRINT(lrint, lrint, long) +DFSW_FP_LRINT(llrint, llrint, long long) + +#undef DFSW_FP_LRINT + SANITIZER_INTERFACE_ATTRIBUTE char *__dfsw_strcat(char *dest, const char *src, dfsan_label d_label, dfsan_label s_label, dfsan_label *ret_label) { diff --git a/runtime/dfsan/done_abilist.txt b/runtime/dfsan/done_abilist.txt index 649bc67e..3fe7e864 100644 --- a/runtime/dfsan/done_abilist.txt +++ b/runtime/dfsan/done_abilist.txt @@ -1,7 +1,7 @@ ########## # added by user ######### -fun:log10=functional +fun:log10=custom fun:__ctype_toupper_loc=discard fun:isatty=discard fun:longjmp=discard @@ -94,44 +94,58 @@ fun:ispunct=functional fun:isspace=functional # Functions that return a value that is data-dependent on the input. -fun:__isinf=functional -fun:__isinff=functional -fun:__signbit=functional -fun:__signbitf=functional +# +# FP predicates (isnan/isinf/finite/signbit) and round-to-int (lrint/llrint) are +# modeled as custom wrappers (see dfsan_custom.cpp / z3-ts.cpp). SymSan has no +# working "functional" ABI: WK_Functional in TaintPass.cpp is a no-op that drops +# the return shadow, so a `functional` mark silently loses taint. A custom +# wrapper instead records the actual operation so the z3 solver can flip branches +# like `if (isnan(x))` or `if (lrint(x) == 42)`. The long double (`*l`) variants +# stay `functional`: our FP lifting only supports IEEE 16/32/64-bit sorts, not +# x86_fp80. Monotonic transcendentals (exp/exp2/log/log2/log10/log1p and pow +# with a constant operand) are now `custom` too: z3 still cannot invert them and +# jigsaw is integer-only, but the i2s solver flips such guards by computing the +# numeric libm inverse (e.g. log for exp) and verifying it. Non-monotonic / +# multi-output libcalls (fmod, frexp, modf, nextafter/nexttoward) stay +# `functional` -- i2s cannot invert them either. +fun:__isinf=custom +fun:__isinff=custom +fun:__signbit=custom +fun:__signbitf=custom fun:__signbitl=functional fun:abs=functional fun:btowc=functional -fun:exp=functional -fun:exp2=functional -fun:expf=functional +fun:exp=custom +fun:exp2=custom +fun:expf=custom fun:expl=functional -fun:fabs=functional -fun:finite=functional -fun:finitef=functional +fun:fabs=custom +fun:finite=custom +fun:finitef=custom fun:finitel=functional -fun:floor=functional +fun:floor=custom fun:fmod=functional fun:frexp=functional fun:frexpf=functional fun:frexpl=functional -fun:isinf=functional -fun:isinff=functional +fun:isinf=custom +fun:isinff=custom fun:isinfl=functional -fun:isnan=functional -fun:isnanf=functional +fun:isnan=custom +fun:isnanf=custom fun:isnanl=functional -fun:lrint=functional -fun:lrintf=functional +fun:lrint=custom +fun:lrintf=custom fun:lrintl=functional -fun:llrint=functional -fun:llrintf=functional +fun:llrint=custom +fun:llrintf=custom fun:llrintl=functional -fun:log=functional -fun:log1p=functional -fun:log1pf=functional +fun:log=custom +fun:log1p=custom +fun:log1pf=custom fun:log1pl=functional -fun:log2=functional -fun:log2f=functional +fun:log2=custom +fun:log2f=custom fun:log2l=functional fun:modf=functional fun:nextafter=functional @@ -140,12 +154,32 @@ fun:nextafterl=functional fun:nexttoward=functional fun:nexttowardf=functional fun:nexttowardl=functional -fun:pow=functional -fun:powf=functional +fun:pow=custom +fun:powf=custom fun:powl=functional -fun:round=functional -fun:sqrt=functional -fun:sqrtf=functional +fun:round=custom +fun:sqrt=custom +fun:sqrtf=custom +# Floating-point math libcalls modeled as custom wrappers (see dfsan_custom.cpp). +# These build the self-defined fp_* op nodes so the z3 solver can reconstruct +# and flip FP-dependent branches even when clang keeps them as libcalls. +fun:fabsf=custom +fun:floorf=custom +fun:ceil=custom +fun:ceilf=custom +fun:trunc=custom +fun:truncf=custom +fun:roundf=custom +fun:rint=custom +fun:rintf=custom +fun:nearbyint=custom +fun:nearbyintf=custom +fun:fmin=custom +fun:fminf=custom +fun:fmax=custom +fun:fmaxf=custom +fun:copysign=custom +fun:copysignf=custom fun:wctob=functional fun:wctob=functional diff --git a/solvers/i2s-solver.cpp b/solvers/i2s-solver.cpp index 799116c2..0d3555fd 100644 --- a/solvers/i2s-solver.cpp +++ b/solvers/i2s-solver.cpp @@ -93,6 +93,245 @@ static uint64_t get_i2s_value(uint32_t comp, uint64_t v, bool rhs) { return v; } +//===----------------------------------------------------------------------===// +// Floating-point input-to-state helpers +// +// FP-typed labels carry the IEEE-754 encoding as a bit-vector, so a "direct" +// FCmp (an input value read straight into an FP compare against a constant) +// looks, at the byte level, exactly like the integer i2s case: the symbolic +// operand's bytes appear literally in the input and can be replaced wholesale. +// The only twist is that the replacement value must satisfy an FP relation, so +// we decode to a C double/float, pick a satisfying value (using nextafter for +// strict inequalities), and re-encode. Every guess is verified against the +// actual FP semantics before we claim SAT (see solve_fcmp). +//===----------------------------------------------------------------------===// + +// Decode an IEEE-754 bit pattern (4- or 8-byte) into a C double. +static inline double fp_decode(uint64_t bits, uint32_t bytes) { + if (bytes == 8) { + double d; memcpy(&d, &bits, sizeof(d)); return d; + } else { // 4 + uint32_t u = (uint32_t)bits; float f; memcpy(&f, &u, sizeof(f)); return (double)f; + } +} + +// Encode a C double into an IEEE-754 bit pattern of the given width (narrowing +// to float for 4-byte). Inverse of fp_decode. +static inline uint64_t fp_encode(double d, uint32_t bytes) { + if (bytes == 8) { + uint64_t b; memcpy(&b, &d, sizeof(b)); return b; + } else { // 4 + float f = (float)d; uint32_t u; memcpy(&u, &f, sizeof(u)); return (uint64_t)u; + } +} + +// Next representable value from x toward dir (+/-inf), at the target precision. +static inline double fp_next(double x, double dir, uint32_t bytes) { + if (bytes == 8) return nextafter(x, dir); + else return (double)nextafterf((float)x, (float)dir); +} + +// Map an rgd FP relational kind to the LLVM FCmp predicate (1..14). +static inline uint32_t fcmp_predicate(uint32_t comparison) { + return comparison - rgd::FOeq + 1; +} + +// Swap the operands of an FCmp predicate: returns the predicate P' such that +// (a P b) == (b P' a). Only the ordering-sensitive predicates change. +static inline uint32_t swap_fcmp_predicate(uint32_t pred) { + switch (pred) { + case 2: return 4; // OGT <-> OLT + case 3: return 5; // OGE <-> OLE + case 4: return 2; + case 5: return 3; + case 10: return 12; // UGT <-> ULT + case 11: return 13; // UGE <-> ULE + case 12: return 10; + case 13: return 11; + default: return pred; // OEQ, ONE, ORD, UNO, UEQ, UNE are symmetric + } +} + +// Concrete evaluation of an FCmp (LLVM predicate 1..14) on two doubles. +static inline bool i2s_eval_fcmp(uint32_t pred, double a, double b) { + bool ord = !(isnan(a) || isnan(b)); + switch (pred) { + case 1: return ord && a == b; // OEQ + case 2: return ord && a > b; // OGT + case 3: return ord && a >= b; // OGE + case 4: return ord && a < b; // OLT + case 5: return ord && a <= b; // OLE + case 6: return ord && a != b; // ONE + case 7: return ord; // ORD + case 8: return !ord; // UNO + case 9: return !ord || a == b; // UEQ + case 10: return !ord || a > b; // UGT + case 11: return !ord || a >= b; // UGE + case 12: return !ord || a < b; // ULT + case 13: return !ord || a <= b; // ULE + case 14: return !ord || a != b; // UNE + default: return false; + } +} + +// Pick a value for the symbolic operand that satisfies (sym k) when the +// symbolic operand is the lhs, or (k sym) when it is the rhs. We fold +// the rhs case into the lhs case by swapping the predicate, then choose a +// witness relative to the constant k. The result is still verified by the +// caller, so an unsatisfiable predicate (e.g. OGT against +inf) just fails +// verification and is skipped. +static inline double fp_i2s_target(uint32_t pred, double k, bool sym_is_lhs, + uint32_t bytes) { + uint32_t p = sym_is_lhs ? pred : swap_fcmp_predicate(pred); + switch (p) { + case 1: // OEQ: sym == k + case 3: // OGE: sym >= k + case 5: // OLE: sym <= k + case 7: // ORD: sym is not NaN + case 9: // UEQ: sym == k (or NaN) + case 11: // UGE + case 13: // ULE + return k; + case 2: // OGT: sym > k + case 10: // UGT + return fp_next(k, HUGE_VAL, bytes); // toward +inf + case 4: // OLT: sym < k + case 12: // ULT + return fp_next(k, -HUGE_VAL, bytes); // toward -inf + case 6: // ONE: sym != k, not NaN + case 14: // UNE + return k == 0.0 ? 1.0 : 0.0; + case 8: // UNO: sym is NaN (k is a concrete constant, assumed not NaN) + return (double)NAN; + default: + return k; + } +} + +// True for the FP binops that solve_fcmp can invert against a constant. +// FpPow is included: it is binary (base, exponent) with one constant operand, +// so it reuses the same fp_binop_eval/invert/const machinery as FAdd..FDiv. +static inline bool isFPArithKind(uint16_t kind) { + return (kind >= rgd::FAdd && kind <= rgd::FDiv) || kind == rgd::FpPow; +} + +// True for the unary FP transcendentals solve_fcmp can invert numerically +// (exp/exp2/log/log2/log10/log1p). z3 cannot invert these, but i2s computes +// the closed-form libm inverse (see fp_trans_invert) and verifies it. +static inline bool isFPTransKind(uint16_t kind) { + return kind >= rgd::FpExp && kind <= rgd::FpLog1p; +} + +// Forward-evaluate a unary FP transcendental at the target precision (float for +// 4-byte, double for 8-byte), mirroring fp_binop_eval so the structural check +// matches the runtime-recorded result bit-exactly. +static inline double fp_trans_eval(double v, uint16_t kind, uint32_t bytes) { + if (bytes == 4) { + float fv = (float)v, fr; + switch (kind) { + case rgd::FpExp: fr = expf(fv); break; + case rgd::FpExp2: fr = exp2f(fv); break; + case rgd::FpLog: fr = logf(fv); break; + case rgd::FpLog2: fr = log2f(fv); break; + case rgd::FpLog10: fr = log10f(fv); break; + case rgd::FpLog1p: fr = log1pf(fv); break; + default: fr = fv; + } + return (double)fr; + } else { + switch (kind) { + case rgd::FpExp: return exp(v); + case rgd::FpExp2: return exp2(v); + case rgd::FpLog: return log(v); + case rgd::FpLog2: return log2(v); + case rgd::FpLog10: return log10(v); + case rgd::FpLog1p: return log1p(v); + default: return v; + } + } +} + +// Invert a unary FP transcendental: return the input value x such that +// f(x) == s. A heuristic guess (FP rounding is not exactly invertible), always +// re-verified by the caller. exp<->log, exp2<->log2, log10->pow(10,.), +// log1p->expm1. +static inline double fp_trans_invert(double s, uint16_t kind) { + switch (kind) { + case rgd::FpExp: return log(s); + case rgd::FpExp2: return log2(s); + case rgd::FpLog: return exp(s); + case rgd::FpLog2: return exp2(s); + case rgd::FpLog10: return pow(10.0, s); + case rgd::FpLog1p: return expm1(s); + default: return s; + } +} + +// Forward-evaluate an FP binop in the target precision (float for 4-byte, double +// for 8-byte). const_is_rhs selects operand order for the non-commutative ops: +// true -> (v op cst), false -> (cst op v). Computing in the target precision +// lets the structural check below match the runtime-recorded result bit-exactly. +static inline double fp_binop_eval(double v, double cst, uint16_t kind, + bool const_is_rhs, uint32_t bytes) { + if (bytes == 4) { + float fv = (float)v, fc = (float)cst, fr; + switch (kind) { + case rgd::FAdd: fr = fv + fc; break; // commutative + case rgd::FSub: fr = const_is_rhs ? fv - fc : fc - fv; break; + case rgd::FMul: fr = fv * fc; break; // commutative + case rgd::FDiv: fr = const_is_rhs ? fv / fc : fc / fv; break; + // pow(base, exp): const_is_rhs -> exponent is constant (v^c), else base + // is constant (c^v). + case rgd::FpPow: fr = const_is_rhs ? powf(fv, fc) : powf(fc, fv); break; + default: fr = fv; + } + return (double)fr; + } else { + switch (kind) { + case rgd::FAdd: return v + cst; // commutative + case rgd::FSub: return const_is_rhs ? v - cst : cst - v; + case rgd::FMul: return v * cst; // commutative + case rgd::FDiv: return const_is_rhs ? v / cst : cst / v; + case rgd::FpPow: return const_is_rhs ? pow(v, cst) : pow(cst, v); + default: return v; + } + } +} + +// Invert an FP binop: return the operand value that makes the binop produce s. +// The result is a heuristic guess (FP rounding is not exactly invertible) and is +// always re-verified by the caller. +static inline double fp_binop_invert(double s, double cst, uint16_t kind, + bool const_is_rhs) { + switch (kind) { + case rgd::FAdd: return s - cst; // v = s - cst + case rgd::FSub: return const_is_rhs ? s + cst : cst - s; // v op cst / cst op v + case rgd::FMul: return s / cst; // v = s / cst + case rgd::FDiv: return const_is_rhs ? s * cst : cst / s; // v op cst / cst op v + // pow: exponent const -> v = s^(1/cst); base const -> v = log(s)/log(cst) + case rgd::FpPow: return const_is_rhs ? pow(s, 1.0 / cst) : log(s) / log(cst); + default: return s; + } +} + +// Find the Constant child of an FP binop node and return its IEEE bits plus +// which side it is on. Mirrors get_binop_value's constant lookup. +static inline bool fp_binop_const(std::shared_ptr constraint, + const AstNode &node, uint64_t &cst_bits, bool &const_is_rhs) { + auto &left = node.children(0); + auto &right = node.children(1); + if (left.kind() == Constant) { + cst_bits = constraint->input_args[left.index()].second; + const_is_rhs = false; + return true; + } else if (right.kind() == Constant) { + cst_bits = constraint->input_args[right.index()].second; + const_is_rhs = true; + return true; + } + return false; +} + static inline uint64_t _get_binop_value(uint64_t v1, uint64_t v2, uint16_t kind) { switch (kind) { case rgd::Add: return v1 + v2; @@ -211,6 +450,207 @@ I2SSolver::I2SSolver(): matches(0), mismatches(0) { // binop_mask.set(rgd::Shl); binop_mask.set(rgd::LShr); binop_mask.set(rgd::AShr); + + // FP ops that input-to-state cannot invert: FRem, FNeg, and all FP casts and + // intrinsics/libcalls, i.e. [FRem, FpLrint] -- everything from FRem up to (but + // not including) the first invertible transcendental kind (FpExp). The FP + // relational kinds are left out so a direct FCmp (input -> compare against a + // constant) is not rejected; the invertible FP binops (FAdd/FSub/FMul/FDiv), + // the transcendentals (FpExp..FpLog1p) and FpPow are also left out so an + // "f(x) K" / "x op C K" constraint reaches solve_fcmp. + for (uint16_t k = rgd::FRem; k < rgd::FpExp; ++k) + fp_ops_mask.set(k); + + // Invertible FP binops handled by solve_fcmp's arith fallback: [FAdd, FRem) + // plus FpPow (binary, one constant operand). + for (uint16_t k = rgd::FAdd; k < rgd::FRem; ++k) + fp_arith_mask.set(k); + fp_arith_mask.set(rgd::FpPow); + + // Invertible unary FP transcendentals handled by solve_fcmp: [FpExp, FpPow). + for (uint16_t k = rgd::FpExp; k < rgd::FpPow; ++k) + fp_trans_mask.set(k); +} + +solver_result_t +I2SSolver::solve_fcmp(std::shared_ptr const& c, + std::unique_ptr const& cm, + uint32_t comparison, + const uint8_t *in_buf, size_t in_size, + uint8_t *out_buf, size_t &out_size) { + + uint32_t predicate = fcmp_predicate(comparison); + + // Classify the comparison by AST structure (not by value matching): is one + // operand produced by a single invertible FP arith op against a constant, or + // is it a direct compare of an input operand? Deciding by structure avoids a + // false "direct" match when an arith result happens to equal the raw input + // bytes (e.g. 0.0 * C == 0.0 on an all-zero seed). + auto const& root = *c->get_root(); + auto const& lc = root.children(0); + auto const& rc = root.children(1); + const AstNode *arith = nullptr; + bool arith_is_lhs = false; + if (isFPArithKind(lc.kind())) { arith = &lc; arith_is_lhs = true; } + else if (isFPArithKind(rc.kind())) { arith = &rc; arith_is_lhs = false; } + const AstNode *trans = nullptr; + bool trans_is_lhs = false; + if (isFPTransKind(lc.kind())) { trans = &lc; trans_is_lhs = true; } + else if (isFPTransKind(rc.kind())) { trans = &rc; trans_is_lhs = false; } + + uint16_t arith_kind = 0; + uint16_t trans_kind = 0; + uint64_t cst_bits = 0; + bool const_is_rhs = false; + // combined set of invertible FP ops (arith incl pow, and transcendentals): + // i2s can only handle a SINGLE such op that is a direct child of the compare. + std::bitset invertible = (fp_arith_mask | fp_trans_mask) & c->ops; + DEBUGF("i2s fcmp: lc.kind=%u rc.kind=%u arith=%p(lhs=%d) trans=%p invertible.count=%zu " + "trans_any=%d arith_any=%d ncand=%zu\n", + lc.kind(), rc.kind(), (void*)arith, arith_is_lhs, (void*)trans, + invertible.count(), (int)(fp_trans_mask & c->ops).any(), + (int)(fp_arith_mask & c->ops).any(), cm->i2s_candidates.size()); + if ((fp_trans_mask & c->ops).any()) { + // f(x) K for a unary transcendental f; invert via the libm inverse. + if (trans == nullptr || invertible.count() != 1) { + return SOLVER_TIMEOUT; + } + trans_kind = trans->kind(); + } else if ((fp_arith_mask & c->ops).any()) { + // FP arithmetic is involved; i2s can only invert a SINGLE arith op that is a + // direct child of the comparison and has a constant operand. Anything else + // (nested/multiple arith, or two symbolic operands) is left for z3. + if (arith == nullptr || invertible.count() != 1 || + !fp_binop_const(c, *arith, cst_bits, const_is_rhs)) { + DEBUGF("i2s fcmp: arith branch reject arith=%p count=%zu\n", + (void*)arith, invertible.count()); + return SOLVER_TIMEOUT; + } + arith_kind = arith->kind(); + DEBUGF("i2s fcmp: arith accepted kind=%u cst_bits=0x%lx const_is_rhs=%d\n", + arith_kind, cst_bits, const_is_rhs); + } + + // Structural anchor by INPUT OFFSET. The value-only structural checks below + // can be fooled when two operands hold coincidentally-equal values. For + // example `fa == fb + C` on a seed where fa == fb: feeding fa through `+ C` + // yields the same value as fb+C, so the fa candidate passes the value check + // and the inverted result gets written to fa -- the WRONG input. When the + // operand we intend to modify is a plain Read, its AST node records the exact + // input offset it reads (Read::index()); only accept the candidate at that + // offset. If the operand is not a plain Read (transformed input, offset not + // determinable), fall back to the value-only check to preserve behavior. + auto read_offset = [](const AstNode &n, size_t &off) -> bool { + if (n.kind() == rgd::Read) { off = n.index(); return true; } + return false; + }; + size_t sym_off = 0; + bool have_sym_off = false; // arith/trans: offset of the symbolic operand + if (trans != nullptr) { + have_sym_off = read_offset(trans->children(0), sym_off); + } else if (arith != nullptr) { + have_sym_off = read_offset(arith->children(const_is_rhs ? 0 : 1), sym_off); + } + size_t lc_off = 0, rc_off = 0; // direct compare: offsets of each side + bool have_lc_off = read_offset(lc, lc_off); + bool have_rc_off = read_offset(rc, rc_off); + + for (auto const& candidate : cm->i2s_candidates) { + size_t offset = candidate.first; + uint32_t bytes = candidate.second; + // only IEEE single/double are decoded here + if (bytes != 4 && bytes != 8) { + continue; + } + uint64_t mask = (bytes == 8) ? ~0ULL : 0xffffffffULL; + uint64_t value = 0; + memcpy(&value, &in_buf[offset], bytes); + value &= mask; + // Every guess below is verified with i2s_eval_fcmp before we claim SAT, so a + // heuristic guess that does not satisfy the relation (e.g. OGT against the + // max representable value, or an FP inversion broken by rounding) is skipped + // and left for z3. + bool sym_is_lhs; + uint64_t r = 0; + if (trans != nullptr) { + // f(x) K -- invert the transcendental to recover x (e.g. exp -> log). + // anchor: only the input bytes the transcendental actually reads + if (have_sym_off && offset != sym_off) continue; + sym_is_lhs = trans_is_lhs; + double x_in = fp_decode(value, bytes); + // structural check: confirm these input bytes really produce the recorded + // transcendental-side operand value (guards against a coincidental match). + uint64_t fwd_bits = fp_encode(fp_trans_eval(x_in, trans_kind, bytes), bytes) & mask; + uint64_t trans_side = (sym_is_lhs ? c->op1 : c->op2) & mask; + if (fwd_bits != trans_side) continue; + // pick a target for the function *result*, then invert to recover the input. + double k = fp_decode((sym_is_lhs ? c->op2 : c->op1) & mask, bytes); + double s = fp_i2s_target(predicate, k, sym_is_lhs, bytes); + double x_new = fp_trans_invert(s, trans_kind); + r = fp_encode(x_new, bytes) & mask; + // verify end-to-end: re-apply the transcendental to the value we would write. + double res = fp_trans_eval(fp_decode(r, bytes), trans_kind, bytes); + double a = sym_is_lhs ? res : k; + double b = sym_is_lhs ? k : res; + if (!i2s_eval_fcmp(predicate, a, b)) continue; + } else if (arith != nullptr) { + // (x op C) K -- invert the arith op to recover x, mirroring the + // integer binop path in solve_icmp. + // anchor: only the input bytes the arith's symbolic operand actually reads + if (have_sym_off && offset != sym_off) continue; + sym_is_lhs = arith_is_lhs; + double x_in = fp_decode(value, bytes); + double cst = fp_decode(cst_bits & mask, bytes); + // structural check: confirm these input bytes really produce the recorded + // arith-side operand value (guards against a coincidental offset match). + uint64_t fwd_bits = + fp_encode(fp_binop_eval(x_in, cst, arith_kind, const_is_rhs, bytes), bytes) & mask; + uint64_t arith_side = (sym_is_lhs ? c->op1 : c->op2) & mask; + DEBUGF("i2s fcmp arith cand off=%zu bytes=%u x_in=%g cst=%g fwd=0x%lx arith_side=0x%lx\n", + offset, bytes, x_in, cst, fwd_bits, arith_side); + if (fwd_bits != arith_side) continue; + // pick a target for the arith *result*, then invert to recover the input. + double k = fp_decode((sym_is_lhs ? c->op2 : c->op1) & mask, bytes); + double s = fp_i2s_target(predicate, k, sym_is_lhs, bytes); + double x_new = fp_binop_invert(s, cst, arith_kind, const_is_rhs); + r = fp_encode(x_new, bytes) & mask; + // verify end-to-end: re-apply the arith op to the value we would write. + double res = fp_binop_eval(fp_decode(r, bytes), cst, arith_kind, const_is_rhs, bytes); + double a = sym_is_lhs ? res : k; + double b = sym_is_lhs ? k : res; + if (!i2s_eval_fcmp(predicate, a, b)) continue; + } else { + // direct FCmp: the input bytes are one of the comparison operands, and the + // other is the concrete constant we compare against. Anchor to the Read + // offset of each side (when it is a plain Read) so an unrelated candidate + // that merely holds the same value is not mistaken for the operand. + uint64_t const_bits; + if ((c->op1 & mask) == value && (!have_lc_off || offset == lc_off)) { + sym_is_lhs = true; + const_bits = c->op2 & mask; + } else if ((c->op2 & mask) == value && (!have_rc_off || offset == rc_off)) { + sym_is_lhs = false; + const_bits = c->op1 & mask; + } else { + continue; // input does not feed this comparison directly + } + double k = fp_decode(const_bits, bytes); + double s = fp_i2s_target(predicate, k, sym_is_lhs, bytes); + r = fp_encode(s, bytes) & mask; + double sv = fp_decode(r, bytes); // re-decode: verify the exact stored value + double a = sym_is_lhs ? sv : k; + double b = sym_is_lhs ? k : sv; + if (!i2s_eval_fcmp(predicate, a, b)) continue; + } + DEBUGF("i2s: fcmp pred %u @ %lu (%u bytes) sym_lhs=%d -> 0x%lx\n", + predicate, offset, bytes, sym_is_lhs, r); + if (out_size == 0) memcpy(out_buf, in_buf, in_size); // make a copy + out_size = in_size; + memcpy(&out_buf[offset], &r, bytes); + matches++; + return SOLVER_SAT; + } + return SOLVER_TIMEOUT; } solver_result_t @@ -222,6 +662,24 @@ I2SSolver::solve_icmp(std::shared_ptr const& c, uint64_t value = 0, value_r = 0; uint64_t r = 0; + // Structural anchor by INPUT OFFSET (mirrors solve_fcmp). The value-only + // direct-match checks below can be fooled when two symbolic operands hold + // coincidentally-equal values (e.g. `b + 1 == a` on a seed where a == b): the + // b candidate's raw bytes equal op2's value, so op2's inverted result would be + // written to b -- the WRONG input. When a compared side is a plain Read, its + // AST node records the exact input offset it reads (Read::index()); only accept + // a direct match at that offset. When a side is not a plain Read (e.g. a binop, + // handled structurally by the binop branch below), fall back to the value check. + auto const& root = *c->get_root(); + auto const& lc = root.children(0); + auto const& rc = root.children(1); + auto read_offset = [](const AstNode &n, size_t &off) -> bool { + if (n.kind() == rgd::Read) { off = n.index(); return true; } + return false; + }; + size_t lc_off = 0, rc_off = 0; + bool have_lc_off = read_offset(lc, lc_off); + bool have_rc_off = read_offset(rc, rc_off); for (auto const& candidate : cm->i2s_candidates) { size_t offset = candidate.first; uint32_t bytes = candidate.second; @@ -236,17 +694,17 @@ I2SSolver::solve_icmp(std::shared_ptr const& c, value_r = SWAP64(value) >> (64 - bytes * 8); DEBUGF("i2s: try %lu, length %u = 0x%016lx, 0x%016lx, comparison = %d\n", offset, bytes, value, value_r, comparison); - if (c->op1 == value) { + if (c->op1 == value && (!have_lc_off || offset == lc_off)) { matches++; r = get_i2s_value(comparison, c->op2, false); - } else if (c->op2 == value) { + } else if (c->op2 == value && (!have_rc_off || offset == rc_off)) { matches++; r = get_i2s_value(comparison, c->op1, true); - } else if (c->op1 == value_r) { + } else if (c->op1 == value_r && (!have_lc_off || offset == lc_off)) { matches++; r = get_i2s_value(comparison, c->op2, false); r = SWAP64(r) >> (64 - bytes * 8); - } else if (c->op2 == value_r) { + } else if (c->op2 == value_r && (!have_rc_off || offset == rc_off)) { matches++; r = get_i2s_value(comparison, c->op1, true); r = SWAP64(r) >> (64 - bytes * 8); @@ -557,6 +1015,19 @@ I2SSolver::solve(std::shared_ptr task, auto const& c = task->constraints(i); auto const& cm = task->consmetas(i); auto comparison = task->comparisons(i); + // If the constraint involves an FP op that i2s cannot invert (FP + // arithmetic, a cast such as FPToSI, or a libcall such as lrint()), the + // input bytes reach the comparison through that transformation and no + // longer appear literally in the compared value. Attempting input-to-state + // here would copy the constant into the raw FP bytes and yield a bogus + // "solution"; reject and let z3 handle it. A direct FCmp (input bytes + // compared against a constant) sets no bit in fp_ops_mask and is handled + // below by solve_fcmp. + if (unlikely((c->ops & fp_ops_mask).any())) { + DEBUGF("i2s: skip FP-derived constraint\n"); + mismatches++; + continue; + } if (likely(isRelationalKind(comparison))) { if (solve_icmp(c, cm, comparison, in_buf, in_size, out_buf, out_size) == SOLVER_SAT) { // be optimistic, as long as there's one match, we should try the output @@ -564,6 +1035,13 @@ I2SSolver::solve(std::shared_ptr task, } else { mismatches++; } + } else if (isFPRelationalKind(comparison)) { + if (solve_fcmp(c, cm, comparison, in_buf, in_size, out_buf, out_size) == SOLVER_SAT) { + // be optimistic, as long as there's one match, we should try the output + ret = SOLVER_SAT; + } else { + mismatches++; + } } else if (comparison == rgd::Memcmp) { if (solve_memcmp(c, cm, in_buf, in_size, out_buf, out_size) == SOLVER_SAT) { // be optimistic, as long as there's one match, we should try the output diff --git a/solvers/jigsaw/config.h b/solvers/jigsaw/config.h index 74f4b554..69b33562 100644 --- a/solvers/jigsaw/config.h +++ b/solvers/jigsaw/config.h @@ -1,5 +1,36 @@ #ifndef CONFIG_H_ #define CONFIG_H_ #define MAX_NUM_MINIMAL_OPTIMA_ROUND 32 -#define MAX_EXEC_TIMES 1000 +// Per-task attempt budget. A full-set QF_BV sweep (seed 1, 6636 files) showed +// the attempts-to-solve distribution has a long tail: raising the cap from 1000 +// to 10000 recovers ~97% of the budget-limited solves (+260 on the set) and then +// plateaus. It is nearly free for the common case -- most tasks solve in well +// under 1000 attempts and stop early -- so the higher cap only spends more on the +// hard tail. Overridable at runtime via JIGSAW_MAX_EXEC (smttest --budget). +#define MAX_EXEC_TIMES 10000 +// Compile-time switch for jigsaw's search-diagnostic scaffolding: the phase/solve +// tracing (JIGSAW_DEBUG, JIGSAW_REPORT_ITERS env vars), the step tracer +// (JIGSAW_TRACE / JIGSAW_TARGET), and the A/B search-strategy toggles +// (JIGSAW_NO_JITTER / JIGSAW_NO_STAGNATION / JIGSAW_STAG_RESTART). Default 0 so +// production builds carry NONE of these runtime getenv branches and always use +// the winning strategy; set to 1 to reproduce the strategy experiments. +#define JIGSAW_SEARCH_DEBUG 0 +// Max rounds the input-to-state pass is iterated to a fixpoint. A single i2s +// snap can newly-unsatisfy a coupled equality (e.g. X==assemble(bytes) while +// X==const pins X), which a later round can then snap. Bounded to guarantee +// termination even if lateral (non-worsening) snaps cycle. +#define I2S_MAX_ROUNDS 8 +// Max bisection probes when descend's doubling line search overshoots a minimum +// (f grew between step/2 and step). Bounded so backtracking stays cheap relative +// to the high-throughput search budget. +#define BACKTRACK_MAX 5 +// Near-miss local jitter: when a local-optimum escape happens with a small total +// distance, run a bounded local random search (small deltas / bit flips on the +// bytes of unsatisfied constraints) instead of a random restart -- it hops the +// tiny barriers that flat/misleading gradients leave GD stuck at. +#define NEAR_MISS_F0 1024 +#define NEAR_MISS_ROUNDS 64 +// Per-task cap on jitter invocations; past this the escape falls back to the +// cheap restart so jitter can't exhaust the attempt budget and starve descent. +#define NEAR_MISS_MAX_CALLS 4 #endif diff --git a/solvers/jigsaw/gd.cc b/solvers/jigsaw/gd.cc index 81c992f6..3f15bfa2 100644 --- a/solvers/jigsaw/gd.cc +++ b/solvers/jigsaw/gd.cc @@ -1,6 +1,9 @@ #include #include #include +#include +#include +#include #include "jit.h" #include "input.h" @@ -44,6 +47,105 @@ static void dump_distances(std::vector &distances) { } } +// Per-task attempt budget. Defaults to the compile-time MAX_EXEC_TIMES but can +// be overridden at runtime via JIGSAW_MAX_EXEC (e.g. smttest's --budget) so the +// search budget can be swept without rebuilding -- used to test whether a search +// strategy's "losses" are budget-limited or genuine trajectory divergence. +static uint64_t jigsaw_max_exec() { + static const uint64_t v = []() -> uint64_t { + const char *e = getenv("JIGSAW_MAX_EXEC"); + return e ? (uint64_t)strtoull(e, nullptr, 0) : (uint64_t)MAX_EXEC_TIMES; + }(); + return v; +} + +// ---- search tracing (debug only) ----------------------------------------- +// Compiled in only under JIGSAW_SEARCH_DEBUG (see config.h); production builds +// get the no-op stubs at the bottom of this block so the hot search loop carries +// no tracing branches. +// JIGSAW_TRACE=1 prints the search sequence (escapes, descend, per-round jitter +// moves) to stderr. JIGSAW_TARGET= loads a known-good assignment +// (offset-indexed raw bytes -- dump one from z3 via smttest's JIGSAW_DUMP_MODEL) +// so each line ALSO reports how far the current assignment is from the actual +// solution: it becomes obvious the moment a move steps the search AWAY from the +// model, which byte is still wrong, and whether the metric that matters is value +// distance or bit (Hamming) distance. +#if JIGSAW_SEARCH_DEBUG +static bool g_trace = false; +static bool g_have_target = false; +static std::vector g_target; // offset-indexed target model + +static void trace_init() { + g_trace = (getenv("JIGSAW_TRACE") != nullptr); + g_have_target = false; + g_target.clear(); + const char *tp = getenv("JIGSAW_TARGET"); + if (tp) { + FILE *f = fopen(tp, "rb"); + if (f) { + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n > 0) { + g_target.resize((size_t)n); + size_t rd = fread(g_target.data(), 1, (size_t)n, f); + (void)rd; + g_have_target = true; + } + fclose(f); + } + } +} + +// value / Hamming distance from the current assignment to the target model, +// plus the count of still-wrong slots (and the first few offsets). +static void trace_target_dist(MutInput &input, std::shared_ptr task, + uint64_t &vdist, uint32_t &hbits, uint32_t &nwrong, + char *wrong, size_t wrong_sz) { + vdist = 0; hbits = 0; nwrong = 0; + wrong[0] = '\0'; + size_t used = 0; + auto const &ins = task->inputs(); + for (uint32_t i = 0, n = (uint32_t)input.len(); i < n; i++) { + uint32_t off = ins[i].first; + uint8_t cur = (uint8_t)input.value[i]; + uint8_t tgt = (off < g_target.size()) ? g_target[off] : 0; + if (cur != tgt) { + nwrong++; + vdist += (cur > tgt) ? (uint64_t)(cur - tgt) : (uint64_t)(tgt - cur); + hbits += (uint32_t)__builtin_popcount((unsigned)(cur ^ tgt)); + if (used + 24 < wrong_sz) { + int w = snprintf(wrong + used, wrong_sz - used, "%s@%u(%u!=%u)", + used ? "," : "", off, cur, tgt); + if (w > 0) used += (size_t)w; + } + } + } +} + +static void trace_step(const char *label, MutInput &input, uint64_t f0, + std::shared_ptr task) { + if (!g_trace) return; + if (g_have_target) { + uint64_t vd; uint32_t hb, nw; char wrong[256]; + trace_target_dist(input, task, vd, hb, nw, wrong, sizeof(wrong)); + fprintf(stderr, + "[trace] %-14s f0=%-12lu to-model: wrong=%u/%lu vdist=%lu hbits=%u [%s]\n", + label, (unsigned long)f0, nw, (unsigned long)input.len(), + (unsigned long)vd, hb, wrong); + } else { + fprintf(stderr, "[trace] %-14s f0=%lu\n", label, (unsigned long)f0); + } +} +#else // !JIGSAW_SEARCH_DEBUG: no-op stubs so the search loop compiles unchanged +// g_trace is a compile-time constant false here, so every "if (g_trace) ..." +// diagnostic in the hot search/jitter paths is eliminated by the optimizer. +static constexpr bool g_trace = false; +static inline void trace_init() {} +static inline void trace_step(const char *, MutInput &, uint64_t, + std::shared_ptr) {} +#endif // JIGSAW_SEARCH_DEBUG + static void add_results(MutInput &input, std::shared_ptr task) { int i = 0; @@ -116,8 +218,62 @@ static uint32_t negate(uint32_t op) { } +// Distance for an FP comparison. The jitted function stores the two operands +// promoted to IEEE-754 double bit-patterns at a/b (see jit.cc FOeq..FUne case). +// We compute a NON-NEGATIVE double distance d that is exactly 0 iff the +// predicate holds, then map it to a uint64: for d>=0 the IEEE bit-pattern is +// monotonic in d and is 0 iff d==0 -- exactly what gradient descent needs. +// The raw bit-pattern of an O(1) distance is ~2^62, though, far larger than +// typical integer distances, so a mixed FP+integer task would be dominated (and +// sat_inc could saturate) by the FP term. We therefore shift off the low 32 +// bits (see the mapping below): this divides the scale by 2^32 -- an O(1) +// distance now maps to ~2^30 and the whole finite range stays below 2^31, so a +// single FP term no longer swamps integer distances -- while preserving both +// required properties (still monotonic in d, still 0 iff d==0) and the log-like +// wide dynamic range GD relies on. +static uint64_t fp_get_distance(uint32_t comp, uint64_t a, uint64_t b) { + double da, db; + memcpy(&da, &a, sizeof(da)); + memcpy(&db, &b, sizeof(db)); + bool nan = std::isnan(da) || std::isnan(db); + double d = 0.0; + // smallest positive double, used to keep strict predicates non-zero at the + // boundary (a == b) -- mirrors the integer sat_inc(a-b, 1) nudge. + const double eps = DBL_TRUE_MIN; + switch (comp) { + // ordered predicates: false (max distance sense) if either operand is NaN. + case rgd::FOeq: d = nan ? (double)INFINITY : std::fabs(da - db); break; + case rgd::FOne: d = nan ? (double)INFINITY : (da == db ? 1.0 : 0.0); break; + case rgd::FOlt: d = (!nan && da < db) ? 0.0 : (da - db) + eps; break; + case rgd::FOle: d = (!nan && da <= db) ? 0.0 : (da - db) + eps; break; + case rgd::FOgt: d = (!nan && da > db) ? 0.0 : (db - da) + eps; break; + case rgd::FOge: d = (!nan && da >= db) ? 0.0 : (db - da) + eps; break; + case rgd::FOrd: d = nan ? 1.0 : 0.0; break; + // unordered predicates: satisfied whenever either operand is NaN. + case rgd::FUno: d = nan ? 0.0 : 1.0; break; + case rgd::FUeq: d = nan ? 0.0 : std::fabs(da - db); break; + case rgd::FUne: d = nan ? 0.0 : (da == db ? 1.0 : 0.0); break; + case rgd::FUlt: d = (nan || da < db) ? 0.0 : (da - db) + eps; break; + case rgd::FUle: d = (nan || da <= db) ? 0.0 : (da - db) + eps; break; + case rgd::FUgt: d = (nan || da > db) ? 0.0 : (db - da) + eps; break; + case rgd::FUge: d = (nan || da >= db) ? 0.0 : (db - da) + eps; break; + default: + fprintf(stderr, "Non-relational FP op!\n"); + } + double m = std::fabs(d); + uint64_t u; + memcpy(&u, &m, sizeof(u)); + // Rescale the bit-pattern down by 2^32 (exponent bits give octave resolution, + // the retained high mantissa bits give within-octave resolution) so the FP + // term is comparable in scale to integer distances -- see the note above. + u >>= 32; + return (u == 0 && m > 0.0) ? 1 : u; // any nonzero distance stays strictly positive +} + static uint64_t get_distance(uint32_t comp, uint64_t a, uint64_t b) { uint64_t dis = 0; + if (rgd::isFPRelationalKind(comp)) + return fp_get_distance(comp, a, b); switch (comp) { case rgd::Equal: if (a >= b) dis = a - b; @@ -234,7 +390,7 @@ static uint64_t distance(MutInput &input, std::vector &distances, std: add_results(input, task); } task->attempts++; - if (task->attempts > MAX_EXEC_TIMES) { + if (task->attempts > jigsaw_max_exec()) { task->stopped = true; task->solved = false; } @@ -249,10 +405,15 @@ static void partial_derivative(MutInput &orig_input, const uint32_t index, uint6 uint64_t f_plus = 0, f_minus = 0; uint64_t single_dis; - // calculate f(x+delta) + // calculate f(x+delta). The input is NOT restored between delta probes, so add + // only the increment (delta - added) to land exactly at orig+delta rather than the + // cumulative orig+1+4+16... This is a clean finite difference at f(x+delta) -- as + // the comment below intends -- at the same eval count (no extra restore/reprobe). + uint64_t added = 0; for (delta = 1; delta < 256; delta = delta << 1) { task->plus_distances = task->min_distances; - orig_input.update(index, true, delta); + orig_input.update(index, true, delta - added); + added = delta; single_dis = single_distance(orig_input, task->plus_distances, task, index); if (single_dis == 0) { // well, we got lucky and found a solution *sign = true; @@ -265,7 +426,7 @@ static void partial_derivative(MutInput &orig_input, const uint32_t index, uint6 f_plus = sat_inc(f_plus, task->plus_distances[i]); task->attempts += 1; - if (task->attempts > MAX_EXEC_TIMES) + if (task->attempts > jigsaw_max_exec()) task->stopped = true; if (task->stopped) { *val = 0; return; } @@ -277,10 +438,12 @@ static void partial_derivative(MutInput &orig_input, const uint32_t index, uint6 } orig_input.value[index] = orig_val; // restore the original value - // calculate f(x-delta) + // calculate f(x-delta) -- same clean-increment trick as the plus loop above + added = 0; for (delta = 1; delta < 256; delta = delta << 1) { task->minus_distances = task->min_distances; - orig_input.update(index, false, delta); + orig_input.update(index, false, delta - added); + added = delta; single_dis = single_distance(orig_input, task->minus_distances, task, index); if (single_dis == 0) { // well, we got lucky and found a solution *sign = false; @@ -293,7 +456,7 @@ static void partial_derivative(MutInput &orig_input, const uint32_t index, uint6 f_minus = sat_inc(f_minus, task->minus_distances[i]); task->attempts += 1; - if (task->attempts > MAX_EXEC_TIMES) + if (task->attempts > jigsaw_max_exec()) task->stopped = true; if (task->stopped) { *val = 0; return;} @@ -357,6 +520,12 @@ static void compute_delta_all(MutInput &input, Grad &grad, size_t step) { static void cal_gradient(MutInput &input, uint64_t f0, Grad &grad, std::shared_ptr task) { + // #4: skip probing bytes that touch no currently-unsatisfied constraint. Their + // partial derivative is definitionally 0 (f0 = sum of constraint distances; a + // byte that only feeds already-satisfied (distance-0) constraints cannot lower + // any term, only keep it 0 or raise it), so partial_derivative would burn ~16 + // probes just to conclude val=0. Skipping produces a bit-identical gradient + // vector while reclaiming that budget for the MAX_EXEC_TIMES-capped search. uint64_t max = 0; uint32_t index = 0; for (auto &gradu : grad.get_value()) { @@ -368,6 +537,17 @@ static void cal_gradient(MutInput &input, uint64_t f0, Grad &grad, std::shared_p bool is_linear = false; uint64_t val = 0; + bool relevant = false; + for (size_t cons_id : task->cmap(index)) { + if (task->min_distances[cons_id]) { relevant = true; break; } + } + if (!relevant) { + gradu.sign = false; + gradu.val = 0; + index++; + continue; + } + partial_derivative(input, index, f0, &sign, &is_linear, &val, task); if (val > max) { max = val; @@ -435,7 +615,7 @@ static uint64_t descend(MutInput &input_min, MutInput &input, uint64_t f0, Grad for (int i = 0, n = task->size(); i < n; i++) f_new = sat_inc(f_new, task->distances[i]); task->attempts += 1; - if (task->attempts > MAX_EXEC_TIMES) + if (task->attempts > jigsaw_max_exec()) task->stopped = true; if (single_dis == 0) { // if we're doing delta and the single distance is 0 @@ -456,6 +636,49 @@ static uint64_t descend(MutInput &input_min, MutInput &input, uint64_t f0, Grad return 0; } else if (f_new > f_last) { // use > to give the next larger step a chance //if (f_new == UINTMAX_MAX) + // #5: the doubling line search jumped from the last-accepted point + // (== input_min, f_last) straight to a worse point at `step`, stepping + // over a possible minimum in between. Instead of abandoning it, bisect a + // few times back toward input_min along the same gradient to recover a + // better point before falling through to coordinate descent. + if (step > 1) { + size_t bstep = step >> 1; + for (int bt = 0; bt < BACKTRACK_MAX && bstep >= 1; bt++) { + if (task->stopped) + break; + input = input_min; + uint64_t fb = 0; + if (doDelta) { + double movement = grad.get_value()[deltaIdx].pct * (double)bstep; + input.update(deltaIdx, grad.get_value()[deltaIdx].sign, (uint64_t)movement); + single_distance(input, task->distances, task, deltaIdx); + for (int i = 0, n = task->size(); i < n; i++) + fb = sat_inc(fb, task->distances[i]); + task->attempts += 1; + if (task->attempts > jigsaw_max_exec()) + task->stopped = true; + } else { + compute_delta_all(input, grad, bstep); + fb = distance(input, task->distances, task); + } + if (fb == 0) { + task->stopped = true; + task->solved = true; + add_results(input, task); + return 0; + } + if (fb < f_last) { + input_min = input; + task->min_distances = task->distances; + f_last = fb; + break; // recovered a better point; stop bisecting + } + bstep >>= 1; + } + // restore best-known state for the next coordinate phase + input = input_min; + task->distances = task->min_distances; + } break; } @@ -509,6 +732,41 @@ static uint64_t get_i2s_value(uint32_t comp, uint64_t v, bool rhs) { } +// FP analogue of get_i2s_value. v is the CONSTANT operand's value; rhs==true +// means the input is the LEFT operand (op1) and v is op2, rhs==false means the +// input is the RIGHT operand (op2) and v is op1. Returns the value to assign to +// the input side so that (op1 op2) holds. Strict inequalities nudge by +// one ULP in the correct precision (nextafterf for float) toward the satisfying +// side; the caller VERIFIES via fp_get_distance == 0, so a wrong guess is simply +// rejected. FOrd/FUno depend on NaN-ness (not i2s-able) -> return v (rejected). +static double get_i2s_fp_value(uint32_t comp, double v, bool rhs, bool is_float) { + auto up = [&](double x) { + return is_float ? (double)std::nextafterf((float)x, INFINITY) + : std::nextafter(x, INFINITY); + }; + auto down = [&](double x) { + return is_float ? (double)std::nextafterf((float)x, -INFINITY) + : std::nextafter(x, -INFINITY); + }; + switch (comp) { + case rgd::FOeq: case rgd::FUeq: + case rgd::FOle: case rgd::FUle: // op1<=op2 satisfied by equality + case rgd::FOge: case rgd::FUge: // op1>=op2 satisfied by equality + return v; + case rgd::FOlt: case rgd::FUlt: // op1 < op2 + return rhs ? down(v) // input=op1 -> just below op2 + : up(v); // input=op2 -> just above op1 + case rgd::FOgt: case rgd::FUgt: // op1 > op2 + return rhs ? up(v) // input=op1 -> just above op2 + : down(v); // input=op2 -> just below op1 + case rgd::FOne: case rgd::FUne: // op1 != op2 + return up(v); + default: + return v; + } +} + + static uint64_t try_new_i2s_value(std::shared_ptr const& c, uint32_t comparison, uint64_t value, std::shared_ptr task) { int i = 0; for (auto const& [offset, lidx] : c->local_map) { @@ -528,7 +786,44 @@ static uint64_t try_new_i2s_value(std::shared_ptr const& c, ui } +// FP variant of try_new_i2s_value. Unlike the integer helper (which writes the +// candidate's `value` across the whole local_map -- fine only for single-chunk +// constraints), this seeds EVERY arg from the current input and overrides just +// the candidate's `size` bytes, so other symbolic operands (e.g. y in x==y+1.0) +// keep their current values instead of being clobbered. +static uint64_t try_new_i2s_fp_value(std::shared_ptr const& c, + std::unique_ptr const& cm, MutInput &input_min, uint32_t comparison, + size_t offset, uint32_t size, uint64_t value, std::shared_ptr task) { + int arg_idx = 0; + for (auto const& arg : cm->input_args) { + if (arg.first) // symbolic: keep the current input value + task->scratch_args[RET_OFFSET + arg_idx] = input_min.get(arg.second); + else + task->scratch_args[RET_OFFSET + arg_idx] = arg.second; + ++arg_idx; + } + // override only the candidate bytes with the target value + int i = 0; + for (size_t off = offset; off < offset + size; off++) { + const uint32_t lidx = c->local_map.at(off); + task->scratch_args[RET_OFFSET + lidx] = ((value >> i) & 0xff); + i += 8; + } + c->fn(task->scratch_args); + return get_distance(comparison, task->scratch_args[0], task->scratch_args[1]); +} + + static uint64_t try_i2s(MutInput &input_min, MutInput &temp_input, uint64_t f0, std::shared_ptr task) { + // Iterate the input-to-state pass to a (bounded) fixpoint. A single snap can + // turn a previously-satisfied coupled equality unsatisfied -- e.g. snapping X to + // a constant to satisfy `X == C` breaks `X == assemble(bytes)` by exactly the same + // amount, leaving the global distance unchanged (a lateral move). The strict + // improvement gate alone would revert such a move and deadlock, so we ALSO accept + // lateral (equal-f) snaps that shift WHICH constraints are satisfied; a later round + // then snaps the now-unsatisfied side (the assembly bytes) to a strict improvement. + // Bounded by I2S_MAX_ROUNDS so any lateral cycle terminates. + for (int round = 0; round < I2S_MAX_ROUNDS; round++) { temp_input = input_min; bool updated = false; for (int k = 0; k < task->size(); k++) { @@ -609,6 +904,86 @@ static uint64_t try_i2s(MutInput &input_min, MutInput &temp_input, uint64_t f0, break; } } // end foreach candidate + } else if (rgd::isFPRelationalKind(cm->comparison)) { + // FP input-to-state. The jitted fn stores both compare operands as + // IEEE-754 *double* bit-patterns (see jit.cc), so detect a candidate + // input chunk whose FP value equals one operand, then snap it to the + // value that satisfies the predicate against the other (constant) + // operand. This lets jigsaw hit exact FP equalities (e.g. x == y with + // two symbolic operands, or x == C) that gradient descent alone cannot. + double op1d, op2d; + memcpy(&op1d, &cm->op1, sizeof(op1d)); + memcpy(&op2d, &cm->op2, sizeof(op2d)); + bool fp_done = false; + for (auto const& candidate : cm->i2s_candidates) { + const size_t c_off = candidate.first; + const uint32_t c_size = candidate.second; + // A candidate is a maximal run of *consecutive* symbolic input bytes, + // so two adjacent FP operands (e.g. x@0 and y@8 in `x == y + 1.0`) + // merge into one oversized run. Rather than require the whole run to + // be exactly a float/double, slide an FP-sized window across it and + // test each position: reassemble the window's raw bytes, match its FP + // value against a stored operand, and snap it to satisfy the predicate + // against the other operand. try_new_i2s_fp_value VERIFIES every + // guess (fp_get_distance == 0), so windows that don't line up with a + // real operand are simply rejected. This lets jigsaw hit exact FP + // equalities (e.g. x == y with two symbolic operands, or x == C) that + // gradient descent alone cannot. + for (uint32_t fpsize : {(uint32_t)8, (uint32_t)4}) { + if (c_size < fpsize) continue; + const bool is_float = (fpsize == 4); + for (size_t offset = c_off; offset + fpsize <= c_off + c_size; offset++) { + // reassemble the raw input bytes of this window + uint64_t input = 0; + int i = 0; + for (size_t off = offset; off < offset + fpsize; off++) { + const uint32_t lidx = c->local_map.at(off); + uint64_t v = input_min.get(cm->input_args[lidx].second); + input |= (v << i); + i += 8; + } + // interpret the chunk as an FP number, promoted to double so it can + // be matched against the (always double) stored operands + double cur; + if (is_float) { float f; memcpy(&f, &input, sizeof(f)); cur = (double)f; } + else { memcpy(&cur, &input, sizeof(cur)); } + uint64_t cur_bits; + memcpy(&cur_bits, &cur, sizeof(cur_bits)); + + double target; + if (cur_bits == cm->op1) { + target = get_i2s_fp_value(cm->comparison, op2d, true, is_float); + } else if (cur_bits == cm->op2) { + target = get_i2s_fp_value(cm->comparison, op1d, false, is_float); + } else { + continue; + } + + // encode the target in the input's native FP width + uint64_t value = 0; + if (is_float) { float tf = (float)target; memcpy(&value, &tf, sizeof(tf)); } + else { memcpy(&value, &target, sizeof(target)); } + + // test the new value (verifies via fp_get_distance == 0) + uint64_t dis = try_new_i2s_fp_value(c, cm, input_min, cm->comparison, + offset, fpsize, value, task); + if (dis == 0) { + i = 0; + for (size_t off = offset; off < offset + fpsize; off++) { + const uint32_t lidx = c->local_map.at(off); + uint8_t v = ((value >> i) & 0xff); + temp_input.set(cm->input_args[lidx].second, v); + i += 8; + } + updated = true; + fp_done = true; + break; // one match per comparison + } + } // end foreach window position + if (fp_done) break; + } // end foreach fp width + if (fp_done) break; + } // end foreach candidate } else if (cm->comparison == rgd::Memcmp) { size_t const_index = 0; for (auto const& arg : c->input_args) { @@ -646,24 +1021,124 @@ static uint64_t try_i2s(MutInput &input_min, MutInput &temp_input, uint64_t f0, } } } - if (updated) { - uint64_t f_new = distance(temp_input, task->distances, task); - if (f_new < f0) { - // std::cout << "i2s succeeded: " << f0 << " -> " << f_new << std::endl; - input_min = temp_input; - task->min_distances = task->distances; - return f_new; - } + if (!updated) break; // no snap applied this round -> fixpoint + uint64_t f_new = distance(temp_input, task->distances, task); + if (f_new < f0) { + // std::cout << "i2s succeeded: " << f0 << " -> " << f_new << std::endl; + input_min = temp_input; + task->min_distances = task->distances; + f0 = f_new; + if (f0 == 0) break; // solved + continue; // strict progress; look for more snaps + } + if (f_new == f0) { + // Lateral move: total distance unchanged. Keep it only if it actually shifted + // which constraints are satisfied (min_distances != distances), so that the next + // round has a newly-unsatisfied constraint to snap. Otherwise stop -- committing + // a no-op would just spin until the round cap. + if (task->min_distances == task->distances) break; + input_min = temp_input; + task->min_distances = task->distances; + continue; } + break; // f_new > f0: the snap worsened the global distance, discard it + } // end round loop return f0; } static uint64_t repick_start_point(MutInput &input_min, std::shared_ptr task) { + // Full random restart. A "targeted" variant that only re-rolls bytes feeding a + // currently-unsatisfied constraint (keeping the rest) was tried and z3-validated + // A/B'd over 6,636 files: it was a large net loss (union ceiling 5219->4713) -- + // its conservatism starves the exploration a full reroll provides. Reverted. input_min.randomize(); uint64_t ret = distance(input_min, task->min_distances, task); return ret; } +// #2 near-miss jitter: GD stalled (flat gradient) but the total distance is +// small. Run a bounded local random search -- small +/- deltas and bit flips on +// the bytes of unsatisfied constraints, keeping any non-worsening move -- to hop +// the tiny barriers that flat/misleading gradients leave GD stuck at. Unlike a +// random restart this preserves the near-solution instead of re-rolling it away. +static uint64_t near_miss_jitter(MutInput &input, std::shared_ptr task, + uint64_t f0) { + std::vector rel; + for (uint32_t i = 0, n = (uint32_t)input.len(); i < n; i++) { + for (size_t cid : task->cmap(i)) { + if (task->min_distances[cid]) { rel.push_back(i); break; } + } + } + if (rel.empty()) return f0; + if (g_trace) { + fprintf(stderr, "[trace] jitter-begin f0=%lu rel=%zu/%lu bytes {", + (unsigned long)f0, rel.size(), (unsigned long)input.len()); + for (size_t k = 0; k < rel.size(); k++) + fprintf(stderr, "%s%u", k ? "," : "", (unsigned)task->inputs()[rel[k]].first); + fprintf(stderr, "}\n"); + } + uint64_t best = f0; + for (int r = 0; r < NEAR_MISS_ROUNDS && !task->stopped; r++) { + uint32_t idx = rel[input.get_rand() % rel.size()]; + uint64_t save = input.value[idx]; + uint8_t rnd = input.get_rand(); + char mv[24] = ""; + if (rnd & 1) { + uint32_t bit = (rnd >> 1) & 7; + input.flip(idx, bit); // single-bit flip + if (g_trace) snprintf(mv, sizeof(mv), "flip b%u", bit); + } else { + uint64_t d = ((uint64_t)(rnd >> 2) & 7) + 1; // small delta 1..8 + input.update(idx, (rnd & 2) != 0, d); + if (g_trace) snprintf(mv, sizeof(mv), "%c%lu", (rnd & 2) ? '+' : '-', (unsigned long)d); + } + uint64_t f_new = distance(input, task->distances, task); + if (task->solved) { + if (g_trace) + fprintf(stderr, "[trace] r%-3d off@%u %-8s -> SOLVED\n", + r, (unsigned)task->inputs()[idx].first, mv); + return 0; + } + bool accept = (f_new <= best); + if (g_trace) { + // only log accepted moves that change f (real progress) and periodic beats + // -- reverts and no-op laterals are the common case and would drown the log + if (accept && f_new < best) + fprintf(stderr, "[trace] r%-3d off@%u %-8s f=%lu->%lu ACCEPT\n", + r, (unsigned)task->inputs()[idx].first, mv, + (unsigned long)best, (unsigned long)f_new); + } + if (accept) { // keep improving AND lateral moves (escape plateaus) + best = f_new; + task->min_distances = task->distances; + } else { + input.value[idx] = save; // revert a worsening move + } + } + if (g_trace) fprintf(stderr, "[trace] jitter-end f0=%lu\n", (unsigned long)best); + return best; +} + +// Unified local-optimum escape: near-miss jitter when the total distance is +// already small (preserve the near-solution), otherwise a full random restart. +// Used both when the gradient is flat AND when descend stagnates with a +// non-flat-but-misleading gradient. +// +// jitter_calls is a per-task budget guard: each jitter runs NEAR_MISS_ROUNDS +// evals, and the flat-gradient loop can fire many times, so uncapped jitter +// starves the multi-epoch descent that solves near-miss cases (observed as an +// unstable-set regression). After NEAR_MISS_MAX_CALLS jitters we fall back to +// the cheap restart, preserving budget for descent. +static uint64_t do_escape(MutInput &input, std::shared_ptr task, + uint64_t f0, int &jitter_calls, bool allow_jitter) { + if (allow_jitter && f0 <= NEAR_MISS_F0 && jitter_calls < NEAR_MISS_MAX_CALLS) { + jitter_calls++; + return near_miss_jitter(input, task, f0); + } + uint64_t r = repick_start_point(input, task); + trace_step("restart", input, r, task); + return r; +} static uint64_t reload_input(MutInput &input_min, std::shared_ptr task) { input_min.assign(task->inputs()); @@ -678,22 +1153,55 @@ static uint64_t reload_input(MutInput &input_min, std::shared_ptr ta } bool rgd::gd_entry(std::shared_ptr task) { +#if JIGSAW_SEARCH_DEBUG + // JIGSAW_DEBUG=1 traces which phase produced the solution (i2s vs gradient + // descent) and the attempt count -- useful for telling apart constraints that + // are actually *searched* from those the i2s heuristic snaps for free. + static const bool dbg = (getenv("JIGSAW_DEBUG") != nullptr); + // JIGSAW_REPORT_ITERS=1 emits a parseable "[jigsaw] iters" line on every solve + // (attempts-to-solve for this task). Used to profile the attempts distribution + // and pick a sensible default budget (MAX_EXEC_TIMES / --budget). + static const bool report_iters = (getenv("JIGSAW_REPORT_ITERS") != nullptr); +#endif + trace_init(); MutInput input(task->inputs_size()); MutInput scratch_input(task->inputs_size()); task->attempts = 0; uint64_t f0 = reload_input(input, task); + trace_step("seed", input, f0, task); f0 = try_i2s(input, scratch_input, f0, task); - if (task->stopped) + if (task->stopped) { +#if JIGSAW_SEARCH_DEBUG + if (dbg) + fprintf(stderr, "[jigsaw] solved=%d by i2s (initial), attempts=%lu\n", + task->solved, (unsigned long)task->attempts); + if (report_iters && task->solved) + fprintf(stderr, "[jigsaw] iters=%lu phase=i2s-initial\n", + (unsigned long)task->attempts); +#endif return task->solved; + } if (f0 == UINTMAX_MAX) return false; - int ep_i = 0; + [[maybe_unused]] int ep_i = 0; // epoch counter (read by diagnostics / #if DEBUG) + // counters read only by the JIGSAW_SEARCH_DEBUG diagnostics below + [[maybe_unused]] int dbg_restarts = 0; // flat-gradient escape loop iterations + int jitter_calls = 0; // per-task jitter budget (see do_escape) Grad grad(input.len()); + // DIAGNOSTIC toggle (A/B only): JIGSAW_NO_JITTER disables flat-loop jitter so a + // single build can reproduce the pre-jitter baseline for comparison. Compiled + // out in production (a constant false), where flat-loop jitter is always on. +#if JIGSAW_SEARCH_DEBUG + static const bool no_jitter = (getenv("JIGSAW_NO_JITTER") != nullptr); +#else + static constexpr bool no_jitter = false; +#endif + while (true) { if (task->stopped) { break; @@ -714,12 +1222,19 @@ bool rgd::gd_entry(std::shared_ptr task) { if (task->stopped) break; g_i++; + dbg_restarts++; //f0 = repick_start_point(input, f0, rng); //f0 = reload_input(input); - f0 = repick_start_point(input, task); + f0 = do_escape(input, task, f0, jitter_calls, !no_jitter); // flat gradient: jitter OK f0 = try_i2s(input, scratch_input, f0, task); - if (task->stopped) + if (task->stopped) { +#if JIGSAW_SEARCH_DEBUG + if (dbg) + fprintf(stderr, "[jigsaw] solved=%d by i2s (restart), attempts=%lu\n", + task->solved, (unsigned long)task->attempts); +#endif break; + } grad.clear(); cal_gradient(input, f0, grad, task); } @@ -729,10 +1244,58 @@ bool rgd::gd_entry(std::shared_ptr task) { } //TODO grad.normalize(); + uint64_t before = f0; f0 = descend(input, scratch_input, f0, grad, task); +#if JIGSAW_SEARCH_DEBUG + if (dbg && task->solved) + fprintf(stderr, "[jigsaw] solved=1 by gradient descent, epoch=%d attempts=%lu\n", + ep_i, (unsigned long)task->attempts); + if (g_trace) { + char lbl[24]; snprintf(lbl, sizeof(lbl), "descend ep%d", ep_i); + trace_step(lbl, input, f0, task); + } +#endif ep_i += 1; + // Descend-stagnation escape: the gradient wasn't flat, but this epoch's line + // search failed to improve the global distance (misleading gradient / local + // optimum). The flat-gradient loop above only fires when grad==0, which on + // sage-style tasks happens 1-3x while descend can stall for dozens of epochs. + // Fire the same escape here so a near-solution isn't abandoned to luck. + // DIAGNOSTIC toggles (A/B only): the gradient here is non-flat, so jitter + // (gradient-blind) may displace the descent that solves smooth near-misses. + // JIGSAW_NO_STAGNATION -> skip this hook entirely (baseline-like) + // JIGSAW_STAG_RESTART -> escape via restart only, never jitter here + // Compiled out in production (both constant false), where the hook fires with + // jitter -- the winning configuration. +#if JIGSAW_SEARCH_DEBUG + static const bool no_stag = (getenv("JIGSAW_NO_STAGNATION") != nullptr); + static const bool stag_restart = (getenv("JIGSAW_STAG_RESTART") != nullptr); +#else + static constexpr bool no_stag = false; + static constexpr bool stag_restart = false; +#endif + if (!no_stag && !task->stopped && f0 >= before) { + f0 = do_escape(input, task, f0, jitter_calls, !stag_restart); + f0 = try_i2s(input, scratch_input, f0, task); + } //if (ep_i == 2) break; } +#if JIGSAW_SEARCH_DEBUG + if (dbg && !task->solved) { + uint64_t fmin = 0; uint64_t nzero = 0; + for (int k = 0, n = task->size(); k < n; k++) { + fmin = sat_inc(fmin, task->min_distances[k]); + if (task->min_distances[k]) nzero++; + } + fprintf(stderr, + "[jigsaw] gave up (unsolved), epochs=%d attempts=%lu bytes=%zu cons=%d unsat_cons=%lu final_f0=%lu flat_restarts=%d\n", + ep_i, (unsigned long)task->attempts, (size_t)input.len(), + (int)task->size(), (unsigned long)nzero, (unsigned long)fmin, dbg_restarts); + } + if (report_iters && task->solved) + fprintf(stderr, "[jigsaw] iters=%lu phase=search\n", + (unsigned long)task->attempts); +#endif return task->solved; } diff --git a/solvers/jigsaw/grad.cc b/solvers/jigsaw/grad.cc index 20a1485c..c7204c00 100644 --- a/solvers/jigsaw/grad.cc +++ b/solvers/jigsaw/grad.cc @@ -14,7 +14,7 @@ std::vector& Grad::get_value() { uint64_t Grad::max_val() { uint64_t ret = 0; - for (auto gradu : grads) { + for (auto &gradu : grads) { // by reference: avoid copying each GradUnit in this hot loop //std::cout << "graud value is " << gradu.val < ret) ret = gradu.val; @@ -32,7 +32,7 @@ void Grad::normalize() { } void Grad::clear() { - for (auto gradu : grads) { + for (auto &gradu : grads) { // by reference: iterating by value zeroed only copies gradu.val = 0; gradu.pct = 0.0; } @@ -45,9 +45,11 @@ size_t Grad::len() { uint64_t Grad::val_sum() { uint64_t ret = 0; - for (auto gradu : grads) { + for (auto &gradu : grads) { //FIXME: saturating_add - ret += gradu.val; + // done: saturate on overflow so descend's guess_step (f0 / val_sum) stays sane + uint64_t next = ret + gradu.val; + ret = (next < ret) ? (uint64_t)-1 : next; } return ret; } diff --git a/solvers/jigsaw/input.cc b/solvers/jigsaw/input.cc index 830c9347..1faf06c8 100644 --- a/solvers/jigsaw/input.cc +++ b/solvers/jigsaw/input.cc @@ -1,5 +1,6 @@ #include "input.h" #include +#include #include #include #include @@ -77,13 +78,37 @@ uint8_t MutInput::get(const size_t i) { return value[i]; } +// When JIGSAW_SEED is set (e.g. via smttest's --seed), the PRNG is seeded +// deterministically so search strategies can be compared without run-to-run +// basin-shift noise. Each MutInput instance still gets a distinct stream +// (base + monotonic counter) so the two instances per solve don't collide, +// while the whole run stays reproducible across invocations. +static unsigned g_mutinput_seed_counter = 0; + MutInput::MutInput(size_t size) { r_idx = 0; value = (uint64_t*)malloc(size * sizeof(uint64_t)); size_ = size; unsigned int seed; //_rdseed32_step(&seed); - seed = (unsigned)time(NULL); + const char *fixed = getenv("JIGSAW_SEED"); + if (fixed) + seed = (unsigned)strtoul(fixed, nullptr, 0) + g_mutinput_seed_counter++; + else { + // Production path: decorrelate the restart PRNG across parallel workers and + // successive instances. time(NULL) has only 1s granularity, so fuzzing + // workers (and the two MutInputs per solve) constructed in the same second + // used to share an identical restart stream -- killing the exploration + // diversity random restarts exist to provide. Mix a high-resolution + // monotonic clock with a monotonic counter and the instance address so no + // two constructions collide. + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + seed = (unsigned)ts.tv_nsec + ^ ((unsigned)ts.tv_sec << 16) + ^ (g_mutinput_seed_counter++ * 0x9E3779B9u) + ^ (unsigned)((uintptr_t)this >> 4); + } memset(r_s, 0, 256); memset(&r_d, 0, sizeof(struct random_data)); initstate_r(seed, r_s, 256, &r_d); diff --git a/solvers/jigsaw/jit.cc b/solvers/jigsaw/jit.cc index 106e7f9b..c1f23b9e 100644 --- a/solvers/jigsaw/jit.cc +++ b/solvers/jigsaw/jit.cc @@ -6,6 +6,8 @@ #include "llvm/IR/Function.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Intrinsics.h" +#include "llvm/IR/FPEnv.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/IR/Module.h" #include "llvm/IR/Type.h" @@ -17,6 +19,9 @@ #include "llvm/Transforms/Scalar/GVN.h" #include +#include +#include +#include #include #include @@ -29,6 +34,172 @@ using namespace rgd; std::unique_ptr JIT; +// --- floating-point codegen helpers -------------------------------------- +// Node values in this JIT are always integers of node->bits() width; for an FP +// node that integer holds the raw IEEE-754 encoding. as_fp() reinterprets such +// an integer as the matching float/double so we can emit native FP ops, and +// as_bits() reinterprets an FP result back to the integer bit-pattern that the +// rest of codegen (and the value cache) expects. +static inline llvm::Type* fp_type(llvm::IRBuilder<> &Builder, unsigned bits) { + return bits == 32 ? Builder.getFloatTy() : Builder.getDoubleTy(); +} +static inline llvm::Value* as_fp(llvm::IRBuilder<> &Builder, llvm::Value* v, + unsigned bits) { + return Builder.CreateBitCast(v, fp_type(Builder, bits)); +} +static inline llvm::Value* as_bits(llvm::IRBuilder<> &Builder, llvm::Value* v, + unsigned bits) { + return Builder.CreateBitCast(v, + llvm::Type::getIntNTy(Builder.getContext(), bits)); +} + +// --- over-width shift / divide-by-zero semantics ------------------------- +// Over-width shifts (amount >= bit-width) and integer div/rem by zero are +// UNDEFINED in C/C++ (x86 masks the shift count; integer /0 traps), but are +// fully DEFINED in SMT-LIB2: +// bvshl / bvlshr by >= width -> 0 ; bvashr by >= width -> sign fill +// bvudiv x 0 -> ~0 (all ones) ; bvurem x 0 -> x (the dividend) +// bvsdiv x 0 -> (x s>= 0 ? ~0 : 1); bvsrem x 0 -> x (the dividend) +// SymSan's z3 backends (z3-solver.cpp / z3-ts.cpp) model these with SMT-LIB +// semantics, so jigsaw must match to keep the i2s->jigsaw->z3 chain sound -- +// otherwise jigsaw can report a SAT that its own z3 oracle rejects. Define +// JIGSAW_HW_SEMANTICS to instead use the legacy hardware behavior (raw LLVM +// shift, relying on x86 count masking; the divisor=1 div-by-zero hack), e.g. +// to match a C/C++ program's concrete execution rather than the SMT-LIB model. +#ifndef JIGSAW_HW_SEMANTICS +#define JIGSAW_SMTLIB_SEMANTICS 1 +#endif + +static inline llvm::Value* int_const(llvm::IRBuilder<> &B, unsigned bits, + uint64_t v) { + return llvm::ConstantInt::get( + llvm::Type::getIntNTy(B.getContext(), bits), v); +} + +static llvm::Value* build_shl(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { +#ifdef JIGSAW_SMTLIB_SEMANTICS + // (amount < width) ? (a << b) : 0 + llvm::Value* in_range = B.CreateICmpULT(b, int_const(B, bits, bits)); + return B.CreateSelect(in_range, B.CreateShl(a, b), int_const(B, bits, 0)); +#else + return B.CreateShl(a, b); // legacy: relies on x86 shift-count masking +#endif +} + +static llvm::Value* build_lshr(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { +#ifdef JIGSAW_SMTLIB_SEMANTICS + // (amount < width) ? (a >>u b) : 0 + llvm::Value* in_range = B.CreateICmpULT(b, int_const(B, bits, bits)); + return B.CreateSelect(in_range, B.CreateLShr(a, b), int_const(B, bits, 0)); +#else + return B.CreateLShr(a, b); // legacy: relies on x86 shift-count masking +#endif +} + +static llvm::Value* build_ashr(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { +#ifdef JIGSAW_SMTLIB_SEMANTICS + // shift by >= width fills with the sign bit == shifting by (width-1) + llvm::Value* in_range = B.CreateICmpULT(b, int_const(B, bits, bits)); + llvm::Value* amt = B.CreateSelect(in_range, b, int_const(B, bits, bits - 1)); + return B.CreateAShr(a, amt); +#else + return B.CreateAShr(a, b); // legacy: relies on x86 shift-count masking +#endif +} + +// a divisor that is never zero, so the hardware div/rem cannot trap/poison. +static inline llvm::Value* nonzero_divisor(llvm::IRBuilder<> &B, llvm::Value* d, + unsigned bits) { + // FIXME: this is a hack to avoid division by zero, but should use a better way + // FIXME: should record the divisor to avoid gradient vanish + llvm::Value* is_zero = B.CreateICmpEQ(d, int_const(B, bits, 0)); + return B.CreateSelect(is_zero, int_const(B, bits, 1), d); +} + +static llvm::Value* build_udiv(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { + llvm::Value* q = B.CreateUDiv(a, nonzero_divisor(B, b, bits)); +#ifdef JIGSAW_SMTLIB_SEMANTICS + llvm::Value* is_zero = B.CreateICmpEQ(b, int_const(B, bits, 0)); + return B.CreateSelect(is_zero, llvm::ConstantInt::getAllOnesValue(a->getType()), q); +#else + return q; // legacy: divisor forced to 1 +#endif +} + +static llvm::Value* build_sdiv(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { + llvm::Value* q = B.CreateSDiv(a, nonzero_divisor(B, b, bits)); +#ifdef JIGSAW_SMTLIB_SEMANTICS + // bvsdiv x 0 = (x s>= 0) ? ~0 : 1 + llvm::Value* is_zero = B.CreateICmpEQ(b, int_const(B, bits, 0)); + llvm::Value* nonneg = B.CreateICmpSGE(a, int_const(B, bits, 0)); + llvm::Value* zval = B.CreateSelect(nonneg, + llvm::ConstantInt::getAllOnesValue(a->getType()), int_const(B, bits, 1)); + return B.CreateSelect(is_zero, zval, q); +#else + return q; // legacy: divisor forced to 1 +#endif +} + +static llvm::Value* build_urem(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { + llvm::Value* r = B.CreateURem(a, nonzero_divisor(B, b, bits)); +#ifdef JIGSAW_SMTLIB_SEMANTICS + llvm::Value* is_zero = B.CreateICmpEQ(b, int_const(B, bits, 0)); + return B.CreateSelect(is_zero, a, r); // bvurem x 0 = x +#else + return r; // legacy: divisor forced to 1 -> x % 1 == 0 +#endif +} + +static llvm::Value* build_srem(llvm::IRBuilder<> &B, llvm::Value* a, + llvm::Value* b, unsigned bits) { + llvm::Value* r = B.CreateSRem(a, nonzero_divisor(B, b, bits)); +#ifdef JIGSAW_SMTLIB_SEMANTICS + llvm::Value* is_zero = B.CreateICmpEQ(b, int_const(B, bits, 0)); + return B.CreateSelect(is_zero, a, r); // bvsrem x 0 = x +#else + return r; // legacy: divisor forced to 1 -> x % 1 == 0 +#endif +} + +// Walk the AST collecting the rounding mode used by rm-carrying FP arithmetic +// (FAdd/FSub/FMul/FDiv and FpSqrt). fp.rem has no rounding mode, and FpRound +// carries its own selector but lowers to a mode-independent intrinsic +// (floor/ceil/trunc/...), so both are skipped here. The rm selector convention +// is 0=rna,1=rne,2=rtp,3=rtn,4=rtz; the real runtime leaves index()==0 on +// FP-arith nodes (RNE default) and the smttest parser rejects rna on arith, so +// selectors 0 and 1 are both treated as RNE. Accumulates into `acc`: +// -2 = none seen yet, -1 = bail, 0 = RNE, 2/3/4 = single directed mode. +// Sets acc to -1 as soon as two distinct modes are seen (RNE mixed with a +// directed mode counts as mixed): the single-mode-per-formula JIT can honor +// only one MXCSR rounding mode, so mixed formulas fall back to z3. +static void collect_fp_mode(const AstNode* node, int& acc) { + if (acc == -1) return; // already decided to bail + uint32_t k = node->kind(); + if (k == rgd::FAdd || k == rgd::FSub || k == rgd::FMul || + k == rgd::FDiv || k == rgd::FpSqrt) { + uint32_t sel = node->index(); + int m = (sel >= 2 && sel <= 4) ? (int)sel : 0; // 0/1 -> RNE + if (acc == -2) acc = m; + else if (acc != m) { acc = -1; return; } + } + for (int i = 0; i < node->children_size(); ++i) + collect_fp_mode(&node->children(i), acc); +} + +// Returns the formula's single FP rounding mode: 0 (RNE / no directed FP arith), +// 2/3/4 (a single directed mode used throughout), or -1 (mixed -> caller bails). +static int detect_fp_mode(const AstNode* node) { + int acc = -2; + collect_fp_mode(node, acc); + return acc == -2 ? 0 : acc; +} + static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* node, std::map const& local_map, llvm::Value* arg, @@ -164,13 +335,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - llvm::Value* VA0 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 0); - llvm::Value* VA1 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 1); - // FIXME: this is a hack to avoid division by zero, but should use a better way - // FIXME: should record the divisor to avoid gradient vanish - llvm::Value* cond = Builder.CreateICmpEQ(c2, VA0); - llvm::Value* divisor = Builder.CreateSelect(cond, VA1, c2); - ret = Builder.CreateUDiv(c1, divisor); + ret = build_udiv(Builder, c1, c2, node->bits()); break; } case rgd::SDiv: { @@ -178,13 +343,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - llvm::Value* VA0 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 0); - llvm::Value* VA1 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 1); - // FIXME: this is a hack to avoid division by zero, but should use a better way - // FIXME: should record the divisor to avoid gradient vanish - llvm::Value* cond = Builder.CreateICmpEQ(c2, VA0); - llvm::Value* divisor = Builder.CreateSelect(cond, VA1, c2); - ret = Builder.CreateSDiv(c1, divisor); + ret = build_sdiv(Builder, c1, c2, node->bits()); break; } case rgd::URem: { @@ -192,13 +351,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - llvm::Value* VA0 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 0); - llvm::Value* VA1 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 1); - // FIXME: this is a hack to avoid division by zero, but should use a better way - // FIXME: should record the divisor to avoid gradient vanish - llvm::Value* cond = Builder.CreateICmpEQ(c2, VA0); - llvm::Value* divisor = Builder.CreateSelect(cond, VA1, c2); - ret = Builder.CreateURem(c1, divisor); + ret = build_urem(Builder, c1, c2, node->bits()); break; } case rgd::SRem: { @@ -206,13 +359,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - llvm::Value* VA0 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 0); - llvm::Value* VA1 = llvm::ConstantInt::get(llvm::Type::getIntNTy(Builder.getContext(), node->bits()), 1); - // FIXME: this is a hack to avoid division by zero, but should use a better way - // FIXME: should record the divisor to avoid gradient vanish - llvm::Value* cond = Builder.CreateICmpEQ(c2, VA0); - llvm::Value* divisor = Builder.CreateSelect(cond, VA1, c2); - ret = Builder.CreateSRem(c1, divisor); + ret = build_srem(Builder, c1, c2, node->bits()); break; } case rgd::Neg: { @@ -256,7 +403,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - ret = Builder.CreateShl(c1, c2); + ret = build_shl(Builder, c1, c2, node->bits()); break; } case rgd::LShr: { @@ -264,7 +411,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - ret = Builder.CreateLShr(c1, c2); + ret = build_lshr(Builder, c1, c2, node->bits()); break; } case rgd::AShr: { @@ -272,7 +419,7 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - ret = Builder.CreateAShr(c1, c2); + ret = build_ashr(Builder, c1, c2, node->bits()); break; } // all the following ICmp expressions should be top level @@ -291,9 +438,17 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, const AstNode* rc2 = &node->children(1); llvm::Value* c1 = codegen(Builder, rc1, local_map, arg, value_cache); llvm::Value* c2 = codegen(Builder, rc2, local_map, arg, value_cache); - // extend to 64-bit to avoid overflow - llvm::Value* c1e = Builder.CreateZExt(c1, Builder.getInt64Ty()); - llvm::Value* c2e = Builder.CreateZExt(c2, Builder.getInt64Ty()); + // extend to 64-bit to avoid overflow. For SIGNED comparisons the operands + // must be sign-extended, otherwise a negative sub-64-bit value (e.g. the + // i32 0xFE000000) becomes a large positive i64 and get_distance's + // (int64_t)a kind() == rgd::Slt || node->kind() == rgd::Sle || + node->kind() == rgd::Sgt || node->kind() == rgd::Sge); + llvm::Value* c1e = is_signed ? Builder.CreateSExtOrTrunc(c1, Builder.getInt64Ty()) + : Builder.CreateZExtOrTrunc(c1, Builder.getInt64Ty()); + llvm::Value* c2e = is_signed ? Builder.CreateSExtOrTrunc(c2, Builder.getInt64Ty()) + : Builder.CreateZExtOrTrunc(c2, Builder.getInt64Ty()); // save the comparison operands to the output args // so it's easier to negate the condition @@ -357,6 +512,240 @@ static llvm::Value* codegen(llvm::IRBuilder<> &Builder, #endif break; } + // floating-point arithmetic: reinterpret the integer children as fp, emit + // the native FP op, then reinterpret the result back to its bit-pattern. + case rgd::FAdd: + case rgd::FSub: + case rgd::FMul: + case rgd::FDiv: + case rgd::FRem: { + const AstNode* rc1 = &node->children(0); + const AstNode* rc2 = &node->children(1); + llvm::Value* c1 = as_fp(Builder, + codegen(Builder, rc1, local_map, arg, value_cache), rc1->bits()); + llvm::Value* c2 = as_fp(Builder, + codegen(Builder, rc2, local_map, arg, value_cache), rc2->bits()); + llvm::Value* r; + // Under a directed rounding mode (addFunction put the Builder in + // constrained-FP mode and set MXCSR) emit constrained intrinsics so the + // opt passes constant-fold in the chosen mode instead of RNE. fp.rem has + // no rounding mode, so it always uses the plain (exact) frem. + if (Builder.getIsFPConstrained() && node->kind() != rgd::FRem) { + llvm::Intrinsic::ID cid; + switch (node->kind()) { + case rgd::FAdd: cid = llvm::Intrinsic::experimental_constrained_fadd; break; + case rgd::FSub: cid = llvm::Intrinsic::experimental_constrained_fsub; break; + case rgd::FMul: cid = llvm::Intrinsic::experimental_constrained_fmul; break; + default: cid = llvm::Intrinsic::experimental_constrained_fdiv; break; + } + r = Builder.CreateConstrainedFPBinOp(cid, c1, c2); + } else { + switch (node->kind()) { + case rgd::FAdd: r = Builder.CreateFAdd(c1, c2); break; + case rgd::FSub: r = Builder.CreateFSub(c1, c2); break; + case rgd::FMul: r = Builder.CreateFMul(c1, c2); break; + case rgd::FDiv: r = Builder.CreateFDiv(c1, c2); break; + default: r = Builder.CreateFRem(c1, c2); break; + } + } + ret = as_bits(Builder, r, node->bits()); + break; + } + case rgd::FNeg: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + ret = as_bits(Builder, Builder.CreateFNeg(c), node->bits()); + break; + } + // FP -> integer casts (result is a plain integer of node->bits()). + case rgd::FpToUi: + case rgd::FpToSi: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + llvm::Type* iTy = llvm::Type::getIntNTy(Builder.getContext(), node->bits()); + ret = (node->kind() == rgd::FpToUi) ? Builder.CreateFPToUI(c, iTy) + : Builder.CreateFPToSI(c, iTy); + break; + } + // integer -> FP casts (child is a plain integer, result is fp bits). + case rgd::UiToFp: + case rgd::SiToFp: { + const AstNode* rc = &node->children(0); + llvm::Value* c = codegen(Builder, rc, local_map, arg, value_cache); + llvm::Type* fTy = fp_type(Builder, node->bits()); + llvm::Value* r = (node->kind() == rgd::UiToFp) ? Builder.CreateUIToFP(c, fTy) + : Builder.CreateSIToFP(c, fTy); + ret = as_bits(Builder, r, node->bits()); + break; + } + // FP -> FP width changes. + case rgd::FpTrunc: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + ret = as_bits(Builder, + Builder.CreateFPTrunc(c, fp_type(Builder, node->bits())), node->bits()); + break; + } + case rgd::FpExt: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + ret = as_bits(Builder, + Builder.CreateFPExt(c, fp_type(Builder, node->bits())), node->bits()); + break; + } + // unary FP intrinsics (fabs/sqrt) and transcendentals lowered to libm calls + // (exp/exp2/log/log2/log10). The JIT resolves the libm symbols from the + // solver process (see rgdJit.h). + case rgd::FpFabs: + case rgd::FpSqrt: + case rgd::FpExp: + case rgd::FpExp2: + case rgd::FpLog: + case rgd::FpLog2: + case rgd::FpLog10: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + // fp.sqrt is the only rm-carrying op here: under a directed rounding mode + // emit the constrained sqrt so folding/rounding follow the chosen mode. + if (node->kind() == rgd::FpSqrt && Builder.getIsFPConstrained()) { + llvm::Function* decl = llvm::Intrinsic::getDeclaration( + Builder.GetInsertBlock()->getModule(), + llvm::Intrinsic::experimental_constrained_sqrt, {c->getType()}); + ret = as_bits(Builder, Builder.CreateConstrainedFPCall(decl, {c}), + node->bits()); + break; + } + llvm::Intrinsic::ID id; + switch (node->kind()) { + case rgd::FpFabs: id = llvm::Intrinsic::fabs; break; + case rgd::FpSqrt: id = llvm::Intrinsic::sqrt; break; + case rgd::FpExp: id = llvm::Intrinsic::exp; break; + case rgd::FpExp2: id = llvm::Intrinsic::exp2; break; + case rgd::FpLog: id = llvm::Intrinsic::log; break; + case rgd::FpLog2: id = llvm::Intrinsic::log2; break; + default: id = llvm::Intrinsic::log10; break; + } + ret = as_bits(Builder, Builder.CreateUnaryIntrinsic(id, c), node->bits()); + break; + } + // round-to-integral; rounding-mode selector (fp_rounding_mode) in index(). + case rgd::FpRound: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + llvm::Intrinsic::ID id; + switch (node->index()) { + case 0: id = llvm::Intrinsic::round; break; // rna: ties away + case 1: id = llvm::Intrinsic::roundeven; break; // rne: ties to even + case 2: id = llvm::Intrinsic::ceil; break; // rtp: toward +inf + case 3: id = llvm::Intrinsic::floor; break; // rtn: toward -inf + default: id = llvm::Intrinsic::trunc; break; // rtz: toward zero + } + ret = as_bits(Builder, Builder.CreateUnaryIntrinsic(id, c), node->bits()); + break; + } + // binary FP intrinsics (min/max/copysign) and pow (lowered to a libm call). + case rgd::FpMin: + case rgd::FpMax: + case rgd::FpCopysign: + case rgd::FpPow: { + const AstNode* rc1 = &node->children(0); + const AstNode* rc2 = &node->children(1); + llvm::Value* c1 = as_fp(Builder, + codegen(Builder, rc1, local_map, arg, value_cache), rc1->bits()); + llvm::Value* c2 = as_fp(Builder, + codegen(Builder, rc2, local_map, arg, value_cache), rc2->bits()); + llvm::Intrinsic::ID id; + switch (node->kind()) { + case rgd::FpMin: id = llvm::Intrinsic::minnum; break; + case rgd::FpMax: id = llvm::Intrinsic::maxnum; break; + case rgd::FpCopysign: id = llvm::Intrinsic::copysign; break; + default: id = llvm::Intrinsic::pow; break; + } + ret = as_bits(Builder, Builder.CreateBinaryIntrinsic(id, c1, c2), node->bits()); + break; + } + // lrint: round-to-nearest-integer returning an integer (node->bits()). + case rgd::FpLrint: { + const AstNode* rc = &node->children(0); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + llvm::Type* iTy = llvm::Type::getIntNTy(Builder.getContext(), node->bits()); + ret = Builder.CreateIntrinsic(llvm::Intrinsic::lrint, + {iTy, c->getType()}, {c}); + break; + } + // log1p has no LLVM intrinsic; call the libm function directly (resolved + // from the solver process by the JIT's dynamic-library symbol generator). + case rgd::FpLog1p: { + const AstNode* rc = &node->children(0); + unsigned bits = node->bits(); + llvm::Value* c = as_fp(Builder, + codegen(Builder, rc, local_map, arg, value_cache), rc->bits()); + llvm::Type* fTy = fp_type(Builder, bits); + llvm::Module* M = Builder.GetInsertBlock()->getModule(); + llvm::FunctionCallee fn = M->getOrInsertFunction( + bits == 32 ? "log1pf" : "log1p", fTy, fTy); + ret = as_bits(Builder, Builder.CreateCall(fn, {c}), bits); + break; + } + // FP comparisons (top level, like the integer compare case): we don't apply + // the predicate here, we just save the two operands so gd.cc get_distance() + // can compute the per-predicate distance. Promote both operands to double + // and store their IEEE bits, so get_distance reinterprets arg[0]/arg[1] + // uniformly as doubles regardless of the original float/double width. + case rgd::FOeq: + case rgd::FOgt: + case rgd::FOge: + case rgd::FOlt: + case rgd::FOle: + case rgd::FOne: + case rgd::FOrd: + case rgd::FUno: + case rgd::FUeq: + case rgd::FUgt: + case rgd::FUge: + case rgd::FUlt: + case rgd::FUle: + case rgd::FUne: { + const AstNode* rc1 = &node->children(0); + const AstNode* rc2 = &node->children(1); + llvm::Value* c1 = as_fp(Builder, + codegen(Builder, rc1, local_map, arg, value_cache), rc1->bits()); + llvm::Value* c2 = as_fp(Builder, + codegen(Builder, rc2, local_map, arg, value_cache), rc2->bits()); + if (rc1->bits() == 32) c1 = Builder.CreateFPExt(c1, Builder.getDoubleTy()); + if (rc2->bits() == 32) c2 = Builder.CreateFPExt(c2, Builder.getDoubleTy()); + llvm::Value* c1e = Builder.CreateBitCast(c1, Builder.getInt64Ty()); + llvm::Value* c2e = Builder.CreateBitCast(c2, Builder.getInt64Ty()); + + // save the (double-promoted) comparison operands to the output args + llvm::Value* idx[1]; + idx[0] = llvm::ConstantInt::get(Builder.getInt32Ty(), 0); + Builder.CreateStore(c1e, + Builder.CreateGEP(Builder.getInt64Ty(), arg, idx)); + idx[0] = llvm::ConstantInt::get(Builder.getInt32Ty(), 1); + Builder.CreateStore(c2e, + Builder.CreateGEP(Builder.getInt64Ty(), arg, idx)); + + ret = nullptr; + break; + } + // The four FP boolean predicates produce a bit, not a measurable magnitude, + // so gradient descent has nothing to follow. Reject them explicitly; the + // out-of-process chain falls back to the FP-aware z3 solver for these. + case rgd::FpIsNan: + case rgd::FpIsInf: + case rgd::FpIsFinite: + case rgd::FpSignbit: { + throw std::invalid_argument("floating-point predicate not supported in jigsaw"); + break; + } default: throw std::invalid_argument("unhandled expression"); //printExpression(node); @@ -376,6 +765,7 @@ int rgd::addFunction(const AstNode* node, uint64_t id) { if ((!isRelationalKind(node->kind()) && + !isFPRelationalKind(node->kind()) && node->kind() != rgd::Memcmp && node->kind() != rgd::MemcmpN)) { std::cerr << "non-relational expr\n"; @@ -401,6 +791,33 @@ int rgd::addFunction(const AstNode* node, Builder.SetInsertPoint(po); uint32_t idx = 0; + // Determine the formula's FP rounding mode. 0 (RNE / no directed FP arith) + // uses the plain native path unchanged; a single directed mode (2/3/4) needs + // constrained intrinsics + MXCSR; mixed modes (-1) bail so the driver falls + // back to z3 (which handles per-op rounding). See detect_fp_mode above. + int fpmode = detect_fp_mode(node); + if (fpmode < 0) return -1; // mixed rounding modes: single-mode JIT can't honor + bool directed = (fpmode >= 2); + if (directed) { + // x86 has no per-instruction rounding: the constrained intrinsics' constant + // mode is only an assumption the FP env is set that way, so we must ALSO set + // MXCSR at entry (and restore RNE before returning, so the solver process's + // own FP is not left in a directed mode). See the Phase-0 spike below. + llvm::RoundingMode rmode; + int flt_rounds; // llvm.set.rounding arg (FLT_ROUNDS): rtz=0,rne=1,rtp=2,rtn=3 + switch (fpmode) { + case 2: rmode = llvm::RoundingMode::TowardPositive; flt_rounds = 2; break; // rtp + case 3: rmode = llvm::RoundingMode::TowardNegative; flt_rounds = 3; break; // rtn + default: rmode = llvm::RoundingMode::TowardZero; flt_rounds = 0; break; // rtz + } + fooFunc->addFnAttr(llvm::Attribute::StrictFP); + Builder.setIsFPConstrained(true); + Builder.setDefaultConstrainedRounding(rmode); + Builder.setDefaultConstrainedExcept(llvm::fp::ebIgnore); + Builder.CreateIntrinsic(llvm::Intrinsic::set_rounding, {}, + {Builder.getInt32(flt_rounds)}); + } + auto args = fooFunc->arg_begin(); llvm::Value* var = &(*args); std::unordered_map value_cache; @@ -415,6 +832,11 @@ int rgd::addFunction(const AstNode* node, std::cerr << "non-comparison expr\n"; return -1; } + if (directed) { + // restore round-to-nearest so subsequent FP in the solver process is RNE + Builder.CreateIntrinsic(llvm::Intrinsic::set_rounding, {}, + {Builder.getInt32(1)}); + } Builder.CreateRet(body); llvm::raw_ostream *stream = &llvm::outs(); @@ -428,6 +850,116 @@ int rgd::addFunction(const AstNode* node, return 0; } +// --- Phase-0 spike: does the JIT honor directed FP rounding? -------------- +// Load-bearing feasibility check before plumbing SMT-LIB rounding modes through +// the AST. x86 has no per-instruction rounding (mode lives in MXCSR), and a +// constrained intrinsic's *constant* rounding mode is only an ASSUMPTION the FP +// environment is set that way -- so directed rounding at JIT runtime needs BOTH +// a constrained intrinsic (so the 4 opt passes constant-fold in the right mode +// instead of RNE) AND llvm.set.rounding to actually set MXCSR. This builds a +// function computing 0.1+0.2 two ways under roundTowardNegative (a case where +// RNE gives 0x3FD3333333333334 but RTN gives 0x3FD3333333333333 -- 1 ULP apart): +// out[0] = constrained.fadd(0.1, 0.2) -- CONSTANT: tests compile-time folding +// out[1] = constrained.fadd(x, 0.2) -- SYMBOLIC x=out[0]: tests runtime MXCSR +// runs the same optimizeModule passes, JITs it, and checks both equal RTN(0.1+0.2). +// Returns 0 on success, non-zero if the mechanism does not produce directed rounding. +int rgd::spike_fp_rounding() { + auto TheCtx = std::make_unique(); + auto TheModule = std::make_unique("spike_m", *TheCtx); + TheModule->setDataLayout(JIT->getDataLayout()); + llvm::IRBuilder<> Builder(*TheCtx); + + auto *I64 = Builder.getInt64Ty(); + auto *Dbl = Builder.getDoubleTy(); + std::vector input_type(1, llvm::PointerType::getUnqual(I64)); + auto *funcType = llvm::FunctionType::get(Builder.getVoidTy(), input_type, false); + auto *fooFunc = llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, + "spikefn", TheModule.get()); + fooFunc->addFnAttr(llvm::Attribute::StrictFP); + auto *po = llvm::BasicBlock::Create(Builder.getContext(), "entry", fooFunc); + Builder.SetInsertPoint(po); + + // constrained-FP mode: emit strictfp calls with our chosen rounding/exception. + Builder.setIsFPConstrained(true); + Builder.setDefaultConstrainedRounding(llvm::RoundingMode::TowardNegative); + Builder.setDefaultConstrainedExcept(llvm::fp::ebIgnore); + + auto args = fooFunc->arg_begin(); + llvm::Value* arg = &(*args); + + // set MXCSR to round-toward-negative (FLT_ROUNDS: -inf == 3) + Builder.CreateIntrinsic(llvm::Intrinsic::set_rounding, {}, + {Builder.getInt32(3)}); + + auto *P1 = llvm::ConstantFP::get(Dbl, 0.1); + auto *P2 = llvm::ConstantFP::get(Dbl, 0.2); + + // out[0] = 0.1 + 0.2 (both constant -> exercises compile-time folding) + llvm::Value* cst = Builder.CreateConstrainedFPBinOp( + llvm::Intrinsic::experimental_constrained_fadd, P1, P2, nullptr, "", + nullptr, llvm::RoundingMode::TowardNegative, llvm::fp::ebIgnore); + + // x = out[0] (runtime value = 0.1), out[1] = x + 0.2 (symbolic -> exercises MXCSR) + llvm::Value* p0 = Builder.CreateGEP(I64, arg, Builder.getInt64(0)); + llvm::Value* xb = Builder.CreateLoad(I64, p0); + llvm::Value* x = Builder.CreateBitCast(xb, Dbl); + llvm::Value* dyn = Builder.CreateConstrainedFPBinOp( + llvm::Intrinsic::experimental_constrained_fadd, x, P2, nullptr, "", + nullptr, llvm::RoundingMode::TowardNegative, llvm::fp::ebIgnore); + + Builder.CreateStore(Builder.CreateBitCast(cst, I64), p0); + llvm::Value* p1 = Builder.CreateGEP(I64, arg, Builder.getInt64(1)); + Builder.CreateStore(Builder.CreateBitCast(dyn, I64), p1); + + // restore round-to-nearest before returning + Builder.CreateIntrinsic(llvm::Intrinsic::set_rounding, {}, + {Builder.getInt32(1)}); + Builder.CreateRetVoid(); + + if (llvm::verifyFunction(*fooFunc, &llvm::errs())) { + std::cerr << "[spike] verifyFunction FAILED\n"; + return 2; + } + if (getenv("SPIKE_DUMP_IR")) TheModule->print(llvm::errs(), nullptr); + JIT->addModule(std::move(TheModule), std::move(TheCtx)); + + auto sym = JIT->lookup("spikefn").get(); +#if LLVM_VERSION_MAJOR >= 17 + auto fn = (void(*)(uint64_t*))sym.getAddress().getValue(); +#else + auto fn = (void(*)(uint64_t*))sym.getAddress(); +#endif + + // reference values computed with the C FP environment. volatile a/b defeat + // compile-time folding so the division happens under the set rounding mode. + volatile double a = 0.1, b = 0.2; + std::fesetround(FE_TONEAREST); volatile double rne = a + b; + std::fesetround(FE_DOWNWARD); volatile double rtn = a + b; + std::fesetround(FE_TONEAREST); + uint64_t rne_b, rtn_b; + { double d = rne; memcpy(&rne_b, &d, 8); } + { double d = rtn; memcpy(&rtn_b, &d, 8); } + + uint64_t out[2]; double init = 0.1; memcpy(&out[0], &init, 8); out[1] = 0; + fn(out); + + auto show = [](const char* tag, uint64_t got, uint64_t rtn, uint64_t rne) { + double g; memcpy(&g, &got, 8); + bool ok = (got == rtn); + fprintf(stderr, "[spike] %-18s got=%.17g (0x%016lx) RTN=0x%016lx RNE=0x%016lx %s\n", + tag, g, (unsigned long)got, (unsigned long)rtn, (unsigned long)rne, + ok ? "OK (directed)" : (got == rne ? "FAIL (RNE!)" : "FAIL (other)")); + return ok; + }; + fprintf(stderr, "[spike] RTN(1/3) and RNE(1/3) differ: %s\n", + rtn_b != rne_b ? "yes" : "NO -- test is degenerate!"); + bool ok0 = show("const-folded", out[0], rtn_b, rne_b); + bool ok1 = show("runtime-symbolic", out[1], rtn_b, rne_b); + fprintf(stderr, "[spike] RESULT: %s\n", + (ok0 && ok1) ? "PASS -- JIT honors directed rounding" : "FAIL"); + return (ok0 && ok1) ? 0 : 1; +} + test_fn_type rgd::performJit(uint64_t id) { std::string funcName = "rgdjit_f" + std::to_string(id); auto ExprSymbol = JIT->lookup(funcName).get(); diff --git a/solvers/jigsaw/jit.h b/solvers/jigsaw/jit.h index cdad27cd..18e831cd 100644 --- a/solvers/jigsaw/jit.h +++ b/solvers/jigsaw/jit.h @@ -14,6 +14,10 @@ int addFunction(const AstNode* node, test_fn_type performJit(uint64_t id); +// Phase-0 spike: verify the JIT honors directed FP rounding (constrained +// intrinsics + llvm.set.rounding). Returns 0 on success. +int spike_fp_rounding(); + bool gd_entry(std::shared_ptr task); } diff --git a/solvers/jit-solver.cpp b/solvers/jit-solver.cpp index 54b7195a..257d2ed7 100644 --- a/solvers/jit-solver.cpp +++ b/solvers/jit-solver.cpp @@ -103,8 +103,12 @@ JITSolver::solve(std::shared_ptr task, uint64_t id = ++uuid; start = getTimeStamp(); if (addFunction(c->get_root(), c->local_map, id) != 0) { + // jigsaw is integer-only and rejects unsupported (e.g. floating-point) + // roots by returning non-zero here. Return TIMEOUT rather than ERROR + // so the driver advances to the next (FP-aware z3) solver -- ERROR sets + // out_buf=NULL and stops the solver chain. WARNF("failed to add function\n"); - return SOLVER_ERROR; + return SOLVER_TIMEOUT; } process_time += (getTimeStamp() - start); start = getTimeStamp(); diff --git a/solvers/z3-solver.cpp b/solvers/z3-solver.cpp index 9470ce37..da81339d 100644 --- a/solvers/z3-solver.cpp +++ b/solvers/z3-solver.cpp @@ -23,7 +23,13 @@ z3::context g_z3_context; const unsigned kSolverTimeout = 10000; // 10 seconds Z3Solver::Z3Solver() - : context_(g_z3_context), solver_(z3::solver(context_, "QF_BV")) + // QF_BVFP (bit-vectors + floating point) rather than QF_BV: FP constraints + // built for FCmp/FP-arith need the floating-point theory. Under plain + // QF_BV z3 treats the FloatingPoint sort as uninterpreted (finite-universe + // model) and returns spurious SAT with a bogus all-zero solution. BVFP is + // still a quantifier-free, bit-blasted logic, so the common BV-only tasks + // are unaffected. + : context_(g_z3_context), solver_(z3::solver(context_, "QF_BVFP")) { // Set timeout for solver z3::params p(context_); @@ -32,13 +38,99 @@ Z3Solver::Z3Solver() } static inline z3::expr -cache_expr(uint32_t label, z3::expr const &e, - std::unordered_map &expr_cache) { +cache_expr(uint32_t label, z3::expr const &e, + std::unordered_map &expr_cache) { if (label != 0) expr_cache.insert({label, e}); return e; } +//===----------------------------------------------------------------------===// +// Floating-point helpers (mirror solvers/z3-ts.cpp) +// +// FP-typed labels are represented as bit-vectors holding the IEEE-754 encoding +// (matching how the runtime stores operands and how inputs are byte-BVs). The +// RGD AstNode tree keeps FP operands/results as BV children; these helpers lift +// a BV to the fpa theory, lower a fpa result back to a BV, and build/evaluate +// FCmp. Rounding follows the z3 context default (RNE), matching LLVM's default +// FP environment. Rounding-mode selectors match __dfsan::fp_rounding_mode +// (rna=0, rne=1, rtp=2, rtn=3, rtz=4); FpRound carries one in AstNode::index(). +//===----------------------------------------------------------------------===// + +static z3::sort fpa_sort_for(z3::context &ctx, unsigned bits) { + switch (bits) { + case 16: return ctx.fpa_sort<16>(); + case 32: return ctx.fpa_sort<32>(); + case 64: return ctx.fpa_sort<64>(); + default: throw z3::exception("unsupported floating-point width"); + } +} + +// Reinterpret an IEEE-754 bit-vector as a floating-point value. +static z3::expr bv_to_fp(z3::context &ctx, const z3::expr &bv, unsigned bits) { + return bv.mk_from_ieee_bv(fpa_sort_for(ctx, bits)); +} + +// Lower a floating-point value back to its IEEE-754 bit-vector encoding. +static z3::expr fp_to_bv(const z3::expr &fp) { + return fp.mk_to_ieee_bv(); +} + +// Build a z3 rounding-mode expression from an fp_rounding_mode selector. +static z3::expr get_rm(z3::context &ctx, uint32_t sel) { + switch (sel) { + case 0: return z3::expr(ctx, Z3_mk_fpa_rna(ctx)); // rna + case 1: return z3::expr(ctx, Z3_mk_fpa_rne(ctx)); // rne + case 2: return z3::expr(ctx, Z3_mk_fpa_rtp(ctx)); // rtp + case 3: return z3::expr(ctx, Z3_mk_fpa_rtn(ctx)); // rtn + case 4: return z3::expr(ctx, Z3_mk_fpa_rtz(ctx)); // rtz + default: return z3::expr(ctx, Z3_mk_fpa_rne(ctx)); + } +} + +// Rounding mode for FP arithmetic (FAdd/FSub/FMul/FDiv/FpSqrt). Unlike FpRound, +// these carry no rm in the real runtime -- index()==0 means "RNE default" -- and +// the smttest parser rejects rna on arithmetic, so selectors 0 and 1 both map to +// RNE here (keeping legacy RNE formulas unchanged) while 2/3/4 pick the directed +// mode the SMT-LIB benchmark specified. +static z3::expr get_arith_rm(z3::context &ctx, uint32_t sel) { + return get_rm(ctx, sel < 2 ? 1 : sel); +} + +// Build a boolean expression for an FCmp with the given LLVM predicate (0..15). +// lhs/rhs must be fpa-sorted. Ordered (O*) predicates are false when either +// operand is NaN; unordered (U*) predicates are true when either is NaN. +static z3::expr get_fcmp(z3::expr const &lhs, z3::expr const &rhs, uint32_t predicate) { + z3::context &ctx = lhs.ctx(); + z3::expr nan_a(ctx, Z3_mk_fpa_is_nan(ctx, lhs)); + z3::expr nan_b(ctx, Z3_mk_fpa_is_nan(ctx, rhs)); + z3::expr unordered = (nan_a || nan_b); + z3::expr eq(ctx, Z3_mk_fpa_eq(ctx, lhs, rhs)); // ordered equality (false on NaN) + z3::expr lt(ctx, Z3_mk_fpa_lt(ctx, lhs, rhs)); + z3::expr gt(ctx, Z3_mk_fpa_gt(ctx, lhs, rhs)); + z3::expr le(ctx, Z3_mk_fpa_leq(ctx, lhs, rhs)); + z3::expr ge(ctx, Z3_mk_fpa_geq(ctx, lhs, rhs)); + switch (predicate) { + case 0: return ctx.bool_val(false); // FCMP_FALSE + case 1: return eq; // FCMP_OEQ + case 2: return gt; // FCMP_OGT + case 3: return ge; // FCMP_OGE + case 4: return lt; // FCMP_OLT + case 5: return le; // FCMP_OLE + case 6: return (lt || gt); // FCMP_ONE (ordered and not equal) + case 7: return (!unordered); // FCMP_ORD (no NaN) + case 8: return unordered; // FCMP_UNO (either NaN) + case 9: return (unordered || eq); // FCMP_UEQ + case 10: return (unordered || gt); // FCMP_UGT + case 11: return (unordered || ge); // FCMP_UGE + case 12: return (unordered || lt); // FCMP_ULT + case 13: return (unordered || le); // FCMP_ULE + case 14: return (!eq); // FCMP_UNE (unordered or not equal) + case 15: return ctx.bool_val(true); // FCMP_TRUE + default: throw z3::exception("unsupported fcmp predicate"); + } +} + z3::expr Z3Solver::serialize(const AstNode* node, const std::vector> &input_args, std::unordered_map &expr_cache) { @@ -195,6 +287,190 @@ z3::expr Z3Solver::serialize(const AstNode* node, // z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); // return cache_expr(node->label(), !c1, expr_cache); // } + // floating-point arithmetic. FP operands/results are IEEE-754 bit-vectors of + // the same width; lift both children to fpa, compute with the node's rounding + // mode (index(); 0/1 == RNE default), lower back to BV. + case rgd::FAdd: + case rgd::FSub: + case rgd::FMul: + case rgd::FDiv: + case rgd::FRem: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + z3::expr c2 = serialize(&node->children(1), input_args, expr_cache); + unsigned bits = node->children(0).bits(); + z3::expr f1 = bv_to_fp(context_, c1, bits); + z3::expr f2 = bv_to_fp(context_, c2, bits); + z3::expr rm = get_arith_rm(context_, node->index()); + z3::expr fr(context_); + switch (node->kind()) { + case rgd::FAdd: fr = z3::expr(context_, Z3_mk_fpa_add(context_, rm, f1, f2)); break; + case rgd::FSub: fr = z3::expr(context_, Z3_mk_fpa_sub(context_, rm, f1, f2)); break; + case rgd::FMul: fr = z3::expr(context_, Z3_mk_fpa_mul(context_, rm, f1, f2)); break; + case rgd::FDiv: fr = z3::expr(context_, Z3_mk_fpa_div(context_, rm, f1, f2)); break; + // NOTE: z3 fpa_rem is the IEEE-754 remainder, which differs from + // LLVM frem / C fmod for some inputs (sign/magnitude of result). + // fp.rem carries no rounding mode (the result is exact). + case rgd::FRem: fr = z3::rem(f1, f2); break; + } + return cache_expr(node->label(), fp_to_bv(fr), expr_cache); + } + case rgd::FNeg: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + z3::expr fp = bv_to_fp(context_, c1, node->children(0).bits()); + z3::expr r(context_, Z3_mk_fpa_neg(context_, fp)); + return cache_expr(node->label(), fp_to_bv(r), expr_cache); + } + // floating-point casts. FP-typed operands carry the IEEE-754 encoding as a + // bit-vector, so we lift the source to fpa, convert, then (for FP results) + // lower back to a BV. Int results (FpToSi/FpToUi/FpLrint) stay as BV. + case rgd::FpToSi: + case rgd::FpToUi: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + unsigned src_bits = node->children(0).bits(); + z3::expr fp = bv_to_fp(context_, c1, src_bits); + z3::expr r = (node->kind() == rgd::FpToSi) ? + z3::expr(context_, Z3_mk_fpa_to_sbv(context_, get_rm(context_, 4 /*rtz*/), fp, node->bits())) : + z3::expr(context_, Z3_mk_fpa_to_ubv(context_, get_rm(context_, 4 /*rtz*/), fp, node->bits())); + // z3's fpa.to_sbv/to_ubv is a *partial* function: for NaN/inf and values + // outside the target integer range the result is unconstrained, letting the + // solver pick an out-of-range operand and assign the result freely (a bogus + // solution that doesn't match C truncation). Constrain the operand to the + // representable range so the conversion is well-defined. See z3-ts.cpp for + // the rationale on the 2^63-1024 / 2^64-2048 upper bounds (INT64_MAX and + // UINT64_MAX are not representable as doubles and round *up* out of range). + { z3::sort ssort = fpa_sort_for(context_, src_bits); + double lo, hi; + if (node->kind() == rgd::FpToSi) { + if (node->bits() >= 64) { lo = -9223372036854775808.0; hi = 9223372036854774784.0; } + else { lo = -(double)(1ULL << (node->bits() - 1)); hi = (double)((1ULL << (node->bits() - 1)) - 1); } + } else { + lo = 0.0; + hi = (node->bits() >= 64) ? 18446744073709549568.0 : (double)((1ULL << node->bits()) - 1); + } + z3::expr flo(context_, Z3_mk_fpa_numeral_double(context_, lo, ssort)); + z3::expr fhi(context_, Z3_mk_fpa_numeral_double(context_, hi, ssort)); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_geq(context_, fp, flo))); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_leq(context_, fp, fhi))); + } + return cache_expr(node->label(), r, expr_cache); + } + case rgd::SiToFp: + case rgd::UiToFp: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + z3::sort fs = fpa_sort_for(context_, node->bits()); + z3::expr fp = (node->kind() == rgd::SiToFp) ? + z3::expr(context_, Z3_mk_fpa_to_fp_signed(context_, get_rm(context_, 1 /*rne*/), c1, fs)) : + z3::expr(context_, Z3_mk_fpa_to_fp_unsigned(context_, get_rm(context_, 1 /*rne*/), c1, fs)); + return cache_expr(node->label(), fp_to_bv(fp), expr_cache); + } + case rgd::FpTrunc: + case rgd::FpExt: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + z3::expr fp = bv_to_fp(context_, c1, node->children(0).bits()); + z3::expr fp2(context_, Z3_mk_fpa_to_fp_float(context_, get_rm(context_, 1 /*rne*/), fp, + fpa_sort_for(context_, node->bits()))); + return cache_expr(node->label(), fp_to_bv(fp2), expr_cache); + } + // floating-point unary intrinsics. FpRound carries the rounding-mode + // selector (fp_rounding_mode) in AstNode::index(). + case rgd::FpFabs: + case rgd::FpSqrt: + case rgd::FpRound: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + z3::expr fp = bv_to_fp(context_, c1, node->children(0).bits()); + z3::expr r(context_); + switch (node->kind()) { + case rgd::FpFabs: + r = z3::expr(context_, Z3_mk_fpa_abs(context_, fp)); break; + case rgd::FpSqrt: + r = z3::expr(context_, Z3_mk_fpa_sqrt(context_, get_arith_rm(context_, node->index()), fp)); break; + case rgd::FpRound: + r = z3::expr(context_, Z3_mk_fpa_round_to_integral(context_, get_rm(context_, node->index()), fp)); break; + } + return cache_expr(node->label(), fp_to_bv(r), expr_cache); + } + // floating-point binary intrinsics (minnum/maxnum/copysign). + case rgd::FpMin: + case rgd::FpMax: + case rgd::FpCopysign: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + z3::expr c2 = serialize(&node->children(1), input_args, expr_cache); + unsigned bits = node->children(0).bits(); + if (node->kind() == rgd::FpCopysign) { + // copysign is pure bit manipulation: magnitude of x, sign bit of y. + z3::expr signmask = context_.bv_val((uint64_t)1 << (bits - 1), bits); + return cache_expr(node->label(), (c1 & ~signmask) | (c2 & signmask), expr_cache); + } + z3::expr f1 = bv_to_fp(context_, c1, bits); + z3::expr f2 = bv_to_fp(context_, c2, bits); + z3::expr r = (node->kind() == rgd::FpMin) ? + z3::expr(context_, Z3_mk_fpa_min(context_, f1, f2)) : + z3::expr(context_, Z3_mk_fpa_max(context_, f1, f2)); + return cache_expr(node->label(), fp_to_bv(r), expr_cache); + } + // floating-point predicates (isnan/isinf/finite/signbit). Unary FP operand; + // integer result of width node->bits() feeding a normal ICmp. + case rgd::FpIsNan: + case rgd::FpIsInf: + case rgd::FpIsFinite: + case rgd::FpSignbit: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + unsigned src_bits = node->children(0).bits(); + if (node->kind() == rgd::FpSignbit) { + // signbit is a pure bit read: the IEEE sign bit is the MSB of the BV. + // Zero-extend that 1-bit value to the int result width (correct for -0). + z3::expr sign = c1.extract(src_bits - 1, src_bits - 1); + return cache_expr(node->label(), z3::zext(sign, node->bits() - 1), expr_cache); + } + z3::expr fp = bv_to_fp(context_, c1, src_bits); + z3::expr cond(context_); + switch (node->kind()) { + case rgd::FpIsNan: + cond = z3::expr(context_, Z3_mk_fpa_is_nan(context_, fp)); break; + case rgd::FpIsInf: + cond = z3::expr(context_, Z3_mk_fpa_is_infinite(context_, fp)); break; + case rgd::FpIsFinite: { + z3::expr nan(context_, Z3_mk_fpa_is_nan(context_, fp)); + z3::expr inf(context_, Z3_mk_fpa_is_infinite(context_, fp)); + cond = !nan && !inf; break; + } + } + return cache_expr(node->label(), + z3::ite(cond, context_.bv_val(1, node->bits()), + context_.bv_val(0, node->bits())), + expr_cache); + } + // round-to-nearest-integer libcalls (lrint/llrint). Round with the default + // mode (RNE) then convert to a signed integer of width node->bits(). Like + // FpToSi, z3's fpa.to_sbv is a partial function, so constrain the operand. + case rgd::FpLrint: { + z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); + unsigned src_bits = node->children(0).bits(); + z3::expr fp = bv_to_fp(context_, c1, src_bits); + z3::expr r(context_, Z3_mk_fpa_to_sbv(context_, get_rm(context_, 1 /*rne*/), fp, node->bits())); + { z3::sort ssort = fpa_sort_for(context_, src_bits); + double lo, hi; + if (node->bits() >= 64) { lo = -9223372036854775808.0; hi = 9223372036854774784.0; } + else { lo = -(double)(1ULL << (node->bits() - 1)); hi = (double)((1ULL << (node->bits() - 1)) - 1); } + z3::expr flo(context_, Z3_mk_fpa_numeral_double(context_, lo, ssort)); + z3::expr fhi(context_, Z3_mk_fpa_numeral_double(context_, hi, ssort)); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_geq(context_, fp, flo))); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_leq(context_, fp, fhi))); + } + return cache_expr(node->label(), r, expr_cache); + } + // FP transcendentals (exp/exp2/log/log2/log10/log1p/pow) are i2s-only: z3's + // fpa theory has no operation to invert them, so reject explicitly and let + // the chain fall back (the i2s solver, tried first, handles these). + case rgd::FpExp: + case rgd::FpExp2: + case rgd::FpLog: + case rgd::FpLog2: + case rgd::FpLog10: + case rgd::FpLog1p: + case rgd::FpPow: + throw z3::exception("unsupported FP transcendental (i2s-only)"); + break; default: WARNF("unhandler expr: "); throw z3::exception("unsupported operator"); @@ -213,6 +489,16 @@ z3::expr Z3Solver::serialize_rel(uint32_t comparison, z3::expr c1 = serialize(&node->children(0), input_args, expr_cache); z3::expr c2 = serialize(&node->children(1), input_args, expr_cache); + // floating-point comparison: operands are IEEE-754 bit-vectors; lift both to + // fpa and build a NaN-aware boolean. The FP relational kinds map directly to + // LLVM FCmp predicates 1..14 (FOeq..FUne), i.e. (kind - FOeq + 1). + if (rgd::isFPRelationalKind(comparison)) { + unsigned bits = node->children(0).bits(); + z3::expr f1 = bv_to_fp(context_, c1, bits); + z3::expr f2 = bv_to_fp(context_, c2, bits); + return get_fcmp(f1, f2, comparison - rgd::FOeq + 1); + } + switch(comparison) { case rgd::Equal: case rgd::Memcmp: @@ -271,6 +557,7 @@ Z3Solver::solve(std::shared_ptr task, try { solver_.reset(); // reset solver + aux_constraints_.clear(); // drop any FP range constraints from a prior solve auto base_task = task->base_task; std::vector assumptions; while (base_task != nullptr) { @@ -306,6 +593,12 @@ Z3Solver::solve(std::shared_ptr task, DEBUGF("adding expr %s\n", z3expr.to_string().c_str()); solver_.add(z3expr); } + // add any auxiliary FP range constraints gathered during serialization so + // partial fpa.to_sbv/to_ubv conversions stay well-defined. + for (auto const &aux : aux_constraints_) { + DEBUGF("adding aux constraint %s\n", aux.to_string().c_str()); + solver_.add(aux); + } auto ret = solver_.check(); if (ret == z3::sat) { memcpy(out_buf, in_buf, in_size); diff --git a/solvers/z3-ts.cpp b/solvers/z3-ts.cpp index 50862b98..ed99a513 100644 --- a/solvers/z3-ts.cpp +++ b/solvers/z3-ts.cpp @@ -11,6 +11,8 @@ #include #include +#include +#include using namespace symsan; @@ -61,6 +63,48 @@ static const std::unordered_map OP_MAP { {__dfsan::fprefixof, "prefixof"}, {__dfsan::fsuffixof, "suffixof"}, {__dfsan::flength, "length"}, + // floating-point + {__dfsan::FAdd, "FAdd"}, + {__dfsan::FSub, "FSub"}, + {__dfsan::FMul, "FMul"}, + {__dfsan::FDiv, "FDiv"}, + {__dfsan::FRem, "FRem"}, + {__dfsan::FPToUI, "FPToUI"}, + {__dfsan::FPToSI, "FPToSI"}, + {__dfsan::UIToFP, "UIToFP"}, + {__dfsan::SIToFP, "SIToFP"}, + {__dfsan::FPTrunc, "FPTrunc"}, + {__dfsan::FPExt, "FPExt"}, + {__dfsan::fp_neg, "FNeg"}, + {__dfsan::fp_fabs, "fabs"}, + {__dfsan::fp_sqrt, "sqrt"}, + {__dfsan::fp_round, "fround"}, + {__dfsan::fp_min, "fmin"}, + {__dfsan::fp_max, "fmax"}, + {__dfsan::fp_copysign, "copysign"}, + {__dfsan::fp_is_nan, "isnan"}, + {__dfsan::fp_is_inf, "isinf"}, + {__dfsan::fp_is_finite, "isfinite"}, + {__dfsan::fp_signbit, "signbit"}, + {__dfsan::fp_lrint, "lrint"}, +#define RELATIONAL_FCMP(cmp) (__dfsan::FCmp | (cmp << 8)) + {RELATIONAL_FCMP(0), "FcmpFalse"}, + {RELATIONAL_FCMP(1), "FcmpOeq"}, + {RELATIONAL_FCMP(2), "FcmpOgt"}, + {RELATIONAL_FCMP(3), "FcmpOge"}, + {RELATIONAL_FCMP(4), "FcmpOlt"}, + {RELATIONAL_FCMP(5), "FcmpOle"}, + {RELATIONAL_FCMP(6), "FcmpOne"}, + {RELATIONAL_FCMP(7), "FcmpOrd"}, + {RELATIONAL_FCMP(8), "FcmpUno"}, + {RELATIONAL_FCMP(9), "FcmpUeq"}, + {RELATIONAL_FCMP(10), "FcmpUgt"}, + {RELATIONAL_FCMP(11), "FcmpUge"}, + {RELATIONAL_FCMP(12), "FcmpUlt"}, + {RELATIONAL_FCMP(13), "FcmpUle"}, + {RELATIONAL_FCMP(14), "FcmpUne"}, + {RELATIONAL_FCMP(15), "FcmpTrue"}, +#undef RELATIONAL_FCMP }; static std::string get_op_name(uint32_t op) { @@ -312,6 +356,151 @@ static bool eval_icmp(uint16_t predicate, uint64_t val1, uint64_t val2, uint8_t // std::unreachable(); } +//===----------------------------------------------------------------------===// +// Floating-point helpers +// +// FP-typed labels are represented as bit-vectors holding the IEEE-754 encoding +// (matching how the runtime stores op1/op2 and how inputs are byte-BVs). These +// helpers lift a BV to the fpa theory, lower a fpa result back to a BV, and +// build/evaluate FCmp. Rounding follows the z3 context default (RNE), which +// matches LLVM's default FP environment. +//===----------------------------------------------------------------------===// + +static z3::sort fpa_sort_for(z3::context &ctx, unsigned bits) { + switch (bits) { + case 16: return ctx.fpa_sort<16>(); + case 32: return ctx.fpa_sort<32>(); + case 64: return ctx.fpa_sort<64>(); + default: throw z3::exception("unsupported floating-point width"); + } +} + +// Reinterpret an IEEE-754 bit-vector as a floating-point value. +static z3::expr bv_to_fp(z3::context &ctx, const z3::expr &bv, unsigned bits) { + return bv.mk_from_ieee_bv(fpa_sort_for(ctx, bits)); +} + +// Lower a floating-point value back to its IEEE-754 bit-vector encoding. +static z3::expr fp_to_bv(const z3::expr &fp) { + return fp.mk_to_ieee_bv(); +} + +// Build a z3 rounding-mode expression from an fp_rounding_mode selector. +static z3::expr get_rm(z3::context &ctx, uint32_t sel) { + switch (sel) { + case __dfsan::fp_rm_rna: return z3::expr(ctx, Z3_mk_fpa_rna(ctx)); + case __dfsan::fp_rm_rne: return z3::expr(ctx, Z3_mk_fpa_rne(ctx)); + case __dfsan::fp_rm_rtp: return z3::expr(ctx, Z3_mk_fpa_rtp(ctx)); + case __dfsan::fp_rm_rtn: return z3::expr(ctx, Z3_mk_fpa_rtn(ctx)); + case __dfsan::fp_rm_rtz: return z3::expr(ctx, Z3_mk_fpa_rtz(ctx)); + default: return z3::expr(ctx, Z3_mk_fpa_rne(ctx)); + } +} + +// Rounding mode for FP arithmetic/sqrt carried in the high byte of `op`. +// Selectors 0 and 1 both mean RNE for arithmetic: plain LLVM fadd/fmul/... (and +// non-strict compilation) leave the high byte 0, and constrained round.tonearest +// maps to 1; rna (0) has no MXCSR representation and collides with the default, +// so it too is treated as RNE here (matches z3-solver.cpp get_arith_rm and the +// smttest parser, which rejects rna on arithmetic). 2/3/4 are directed. +static z3::expr get_arith_rm(z3::context &ctx, uint32_t sel) { + return get_rm(ctx, sel < 2 ? __dfsan::fp_rm_rne : sel); +} + +// Build a boolean expression for an FCmp with the given LLVM predicate (0..15). +// lhs/rhs must be fpa-sorted. Ordered (O*) predicates are false when either +// operand is NaN; unordered (U*) predicates are true when either is NaN. +static z3::expr get_fcmp(z3::expr const &lhs, z3::expr const &rhs, uint32_t predicate) { + z3::context &ctx = lhs.ctx(); + z3::expr nan_a(ctx, Z3_mk_fpa_is_nan(ctx, lhs)); + z3::expr nan_b(ctx, Z3_mk_fpa_is_nan(ctx, rhs)); + z3::expr unordered = (nan_a || nan_b); + z3::expr eq(ctx, Z3_mk_fpa_eq(ctx, lhs, rhs)); // ordered equality (false on NaN) + z3::expr lt(ctx, Z3_mk_fpa_lt(ctx, lhs, rhs)); + z3::expr gt(ctx, Z3_mk_fpa_gt(ctx, lhs, rhs)); + z3::expr le(ctx, Z3_mk_fpa_leq(ctx, lhs, rhs)); + z3::expr ge(ctx, Z3_mk_fpa_geq(ctx, lhs, rhs)); + switch (predicate) { + case 0: return ctx.bool_val(false); // FCMP_FALSE + case 1: return eq; // FCMP_OEQ + case 2: return gt; // FCMP_OGT + case 3: return ge; // FCMP_OGE + case 4: return lt; // FCMP_OLT + case 5: return le; // FCMP_OLE + case 6: return (lt || gt); // FCMP_ONE (ordered and not equal) + case 7: return (!unordered); // FCMP_ORD (no NaN) + case 8: return unordered; // FCMP_UNO (either NaN) + case 9: return (unordered || eq); // FCMP_UEQ + case 10: return (unordered || gt); // FCMP_UGT + case 11: return (unordered || ge); // FCMP_UGE + case 12: return (unordered || lt); // FCMP_ULT + case 13: return (unordered || le); // FCMP_ULE + case 14: return (!eq); // FCMP_UNE (unordered or not equal) + case 15: return ctx.bool_val(true); // FCMP_TRUE + default: throw z3::exception("unsupported fcmp predicate"); + } +} + +// Decode an IEEE-754 bit pattern into a C double (widening 32-bit floats). +static inline double fp_decode(uint64_t bits_val, uint8_t bits) { + if (bits == 64) { + double d; memcpy(&d, &bits_val, sizeof(d)); return d; + } else if (bits == 32) { + uint32_t u = (uint32_t)bits_val; float f; memcpy(&f, &u, sizeof(f)); return (double)f; + } + // half and other widths: not decoded for concrete evaluation + return 0.0; +} + +// Encode a C double back into an IEEE-754 bit pattern of the given width. +static inline uint64_t fp_encode(double v, uint8_t bits) { + if (bits == 64) { + uint64_t u; memcpy(&u, &v, sizeof(u)); return u; + } else if (bits == 32) { + float f = (float)v; uint32_t u; memcpy(&u, &f, sizeof(u)); return (uint64_t)u; + } + return 0; +} + +// Concrete evaluation of an FCmp for FILTER_WRONG_AST value tracking. +static bool eval_fcmp(uint16_t predicate, uint64_t val1, uint64_t val2, uint8_t bits) { + double a = fp_decode(val1, bits), b = fp_decode(val2, bits); + bool ord = !(std::isnan(a) || std::isnan(b)); + switch (predicate) { + case 0: return false; + case 1: return ord && a == b; + case 2: return ord && a > b; + case 3: return ord && a >= b; + case 4: return ord && a < b; + case 5: return ord && a <= b; + case 6: return ord && a != b; + case 7: return ord; + case 8: return !ord; + case 9: return !ord || a == b; + case 10: return !ord || a > b; + case 11: return !ord || a >= b; + case 12: return !ord || a < b; + case 13: return !ord || a <= b; + case 14: return !ord || a != b; + case 15: return true; + default: return false; + } +} + +// Concrete evaluation of an FP binary op, returning the IEEE bit pattern. +static uint64_t eval_fp_binop(uint16_t op, uint64_t v1, uint64_t v2, uint8_t bits) { + double a = fp_decode(v1, bits), b = fp_decode(v2, bits), r = 0.0; + switch (op) { + case __dfsan::FAdd: r = a + b; break; + case __dfsan::FSub: r = a - b; break; + case __dfsan::FMul: r = a * b; break; + case __dfsan::FDiv: r = a / b; break; + case __dfsan::FRem: r = std::fmod(a, b); break; // C frem semantics + default: return 0; + } + return fp_encode(r, bits); +} + uint64_t Z3AstParser::serialize_input(dfsan_label label, uint32_t input, uint32_t offset, uint32_t bytes, input_dep_set_t &input_deps) { char name[256]; @@ -512,6 +701,249 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { RECORD_VALUE(value_cache_[info->l1]); continue; } //FIXME: other casting ops (BitCast)? + // floating-point casts. FP-typed labels carry the IEEE-754 encoding as a + // bit-vector, so we lift the operand to fpa, convert, then (for FP results) + // lower back to a BV. Int results (FPToSI/FPToUI) stay as BV directly. + else if (info->op == __dfsan::FPToSI || info->op == __dfsan::FPToUI) { + z3::expr base = get_cached_expr(info->l1, input_deps); + unsigned src_bits = get_label_info(info->l1)->size; + z3::expr fp = bv_to_fp(context_, base, src_bits); + z3::expr r = (info->op == __dfsan::FPToSI) ? + z3::expr(context_, Z3_mk_fpa_to_sbv(context_, get_rm(context_, __dfsan::fp_rm_rtz), fp, info->size)) : + z3::expr(context_, Z3_mk_fpa_to_ubv(context_, get_rm(context_, __dfsan::fp_rm_rtz), fp, info->size)); + // z3's fpa.to_sbv/to_ubv is a *partial* function: for NaN/inf and values + // outside the target integer range the result is unconstrained, letting the + // solver pick an out-of-range operand and assign the result freely (a bogus + // solution that doesn't match C truncation). Constrain the operand to the + // representable range so the conversion is well-defined. Bounds are closed + // (slightly conservative at the fractional edges, but never spurious). + // NOTE: INT64_MAX (2^63-1) and UINT64_MAX (2^64-1) are not representable as + // doubles and round *up* to 2^63 / 2^64 -- both outside the target range, + // where to_sbv/to_ubv is undefined. Use the largest double strictly below + // the overflow point instead (2^63-1024 signed, 2^64-2048 unsigned). + { z3::sort ssort = fpa_sort_for(context_, src_bits); + double lo, hi; + if (info->op == __dfsan::FPToSI) { + if (info->size >= 64) { lo = -9223372036854775808.0; hi = 9223372036854774784.0; } + else { lo = -(double)(1ULL << (info->size - 1)); hi = (double)((1ULL << (info->size - 1)) - 1); } + } else { + lo = 0.0; + hi = (info->size >= 64) ? 18446744073709549568.0 : (double)((1ULL << info->size) - 1); + } + z3::expr flo(context_, Z3_mk_fpa_numeral_double(context_, lo, ssort)); + z3::expr fhi(context_, Z3_mk_fpa_numeral_double(context_, hi, ssort)); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_geq(context_, fp, flo))); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_leq(context_, fp, fhi))); + } + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, r); + TRACK_LABEL_BV_ONLY(); + { double d = fp_decode(value_cache_[info->l1], src_bits); + RECORD_VALUE((info->op == __dfsan::FPToSI) ? + (uint64_t)(int64_t)d : (uint64_t)d); } + continue; + } else if (info->op == __dfsan::SIToFP || info->op == __dfsan::UIToFP) { + z3::expr base = get_cached_expr(info->l1, input_deps); + unsigned src_bits = get_label_info(info->l1)->size; + z3::sort fs = fpa_sort_for(context_, info->size); + z3::expr fp = (info->op == __dfsan::SIToFP) ? + z3::expr(context_, Z3_mk_fpa_to_fp_signed(context_, get_rm(context_, __dfsan::fp_rm_rne), base, fs)) : + z3::expr(context_, Z3_mk_fpa_to_fp_unsigned(context_, get_rm(context_, __dfsan::fp_rm_rne), base, fs)); + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, fp_to_bv(fp)); + TRACK_LABEL_BV_ONLY(); + { uint64_t iv = value_cache_[info->l1] & (src_bits >= 64 ? ~0UL : ((1UL << src_bits) - 1)); + double d; + if (info->op == __dfsan::SIToFP) { + int64_t s = (int64_t)(iv << (64 - src_bits)) >> (64 - src_bits); + d = (double)s; + } else { + d = (double)iv; + } + RECORD_VALUE(fp_encode(d, info->size)); } + continue; + } else if (info->op == __dfsan::FPTrunc || info->op == __dfsan::FPExt) { + z3::expr base = get_cached_expr(info->l1, input_deps); + unsigned src_bits = get_label_info(info->l1)->size; + z3::expr fp = bv_to_fp(context_, base, src_bits); + z3::expr fp2(context_, Z3_mk_fpa_to_fp_float(context_, get_rm(context_, __dfsan::fp_rm_rne), fp, + fpa_sort_for(context_, info->size))); + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, fp_to_bv(fp2)); + TRACK_LABEL_BV_ONLY(); + { double d = fp_decode(value_cache_[info->l1], src_bits); + RECORD_VALUE(fp_encode(d, info->size)); } + continue; + } + // floating-point negate + intrinsics (unary). Operand in l1; fp_round + // carries the rounding-mode selector (fp_rounding_mode) in op1. + else if (info->op == __dfsan::fp_neg || info->op == __dfsan::fp_fabs || + (info->op & 0xff) == __dfsan::fp_sqrt || info->op == __dfsan::fp_round) { + z3::expr base = get_cached_expr(info->l1, input_deps); + unsigned bits = info->size; + z3::expr fp = bv_to_fp(context_, base, bits); + double a = fp_decode(value_cache_[info->l1], bits), rv = 0.0; + z3::expr r(context_); + // fp_sqrt may carry a rounding selector in the high byte (constrained + // llvm.experimental.constrained.sqrt), so dispatch on the base opcode. + switch (info->op & 0xff) { + case __dfsan::fp_neg: + r = z3::expr(context_, Z3_mk_fpa_neg(context_, fp)); rv = -a; break; + case __dfsan::fp_fabs: + r = z3::expr(context_, Z3_mk_fpa_abs(context_, fp)); rv = std::fabs(a); break; + case __dfsan::fp_sqrt: + // Directed rounding (from constrained sqrt) is carried in op's high + // byte; plain llvm.sqrt leaves it 0 (RNE). Note: the seed value `rv` + // uses libm sqrt (RNE); the symbolic constraint uses the true mode. + r = z3::expr(context_, Z3_mk_fpa_sqrt(context_, get_arith_rm(context_, info->op >> 8), fp)); + rv = std::sqrt(a); break; + case __dfsan::fp_round: { + uint32_t sel = (uint32_t)info->op1.i; + r = z3::expr(context_, Z3_mk_fpa_round_to_integral(context_, get_rm(context_, sel), fp)); + switch (sel) { + case __dfsan::fp_rm_rna: rv = std::round(a); break; + case __dfsan::fp_rm_rne: rv = std::nearbyint(a); break; + case __dfsan::fp_rm_rtp: rv = std::ceil(a); break; + case __dfsan::fp_rm_rtn: rv = std::floor(a); break; + case __dfsan::fp_rm_rtz: rv = std::trunc(a); break; + default: rv = std::nearbyint(a); break; + } + break; + } + } + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, fp_to_bv(r)); + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(fp_encode(rv, bits)); + continue; + } + // floating-point predicates (isnan/isinf/finite/signbit; unary, FP operand + // in l1, integer result of width info->size = sizeof(int)*8). These come + // from custom libc wrappers; SymSan has no working "functional" ABI, so the + // wrapper records the predicate here and the result feeds a normal ICmp. + else if (info->op == __dfsan::fp_is_nan || info->op == __dfsan::fp_is_inf || + info->op == __dfsan::fp_is_finite || info->op == __dfsan::fp_signbit) { + z3::expr base = get_cached_expr(info->l1, input_deps); + unsigned src_bits = get_label_info(info->l1)->size; + double a = fp_decode(value_cache_[info->l1], src_bits); + z3::expr r(context_); + uint64_t rv = 0; + if (info->op == __dfsan::fp_signbit) { + // signbit is a pure bit read: the IEEE sign bit is the MSB of the BV. + // Zero-extend that 1-bit value to the int result width (correct for -0). + z3::expr sign = base.extract(src_bits - 1, src_bits - 1); + r = z3::zext(sign, info->size - 1); + rv = std::signbit(a) ? 1 : 0; + } else { + z3::expr fp = bv_to_fp(context_, base, src_bits); + z3::expr cond(context_); + switch (info->op) { + case __dfsan::fp_is_nan: + cond = z3::expr(context_, Z3_mk_fpa_is_nan(context_, fp)); + rv = std::isnan(a) ? 1 : 0; break; + case __dfsan::fp_is_inf: + cond = z3::expr(context_, Z3_mk_fpa_is_infinite(context_, fp)); + rv = std::isinf(a) ? 1 : 0; break; + case __dfsan::fp_is_finite: { + z3::expr nan(context_, Z3_mk_fpa_is_nan(context_, fp)); + z3::expr inf(context_, Z3_mk_fpa_is_infinite(context_, fp)); + cond = !nan && !inf; + rv = std::isfinite(a) ? 1 : 0; break; + } + } + r = z3::ite(cond, context_.bv_val(1, info->size), + context_.bv_val(0, info->size)); + } + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, r); + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(rv); + continue; + } + // round-to-nearest-integer libcalls (lrint/llrint). Round with the default + // mode (RNE) then convert to a signed integer of width info->size. Like + // FPToSI, z3's fpa.to_sbv is a partial function, so constrain the operand to + // the representable range. + else if (info->op == __dfsan::fp_lrint) { + z3::expr base = get_cached_expr(info->l1, input_deps); + unsigned src_bits = get_label_info(info->l1)->size; + z3::expr fp = bv_to_fp(context_, base, src_bits); + z3::expr r(context_, Z3_mk_fpa_to_sbv(context_, + get_rm(context_, __dfsan::fp_rm_rne), fp, info->size)); + { z3::sort ssort = fpa_sort_for(context_, src_bits); + double lo, hi; + // INT64_MAX (2^63-1) is not representable as a double and rounds *up* to + // 2^63, which is outside the signed range -- fpa.to_sbv would be + // undefined there and the solver could pick that point and assign the + // result freely. Use the largest double strictly below 2^63 (2^63-1024) + // as the closed upper bound. -2^63 is exactly representable. + if (info->size >= 64) { lo = -9223372036854775808.0; hi = 9223372036854774784.0; } + else { lo = -(double)(1ULL << (info->size - 1)); hi = (double)((1ULL << (info->size - 1)) - 1); } + z3::expr flo(context_, Z3_mk_fpa_numeral_double(context_, lo, ssort)); + z3::expr fhi(context_, Z3_mk_fpa_numeral_double(context_, hi, ssort)); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_geq(context_, fp, flo))); + aux_constraints_.push_back(z3::expr(context_, Z3_mk_fpa_leq(context_, fp, fhi))); + } + tsize_cache_.emplace_back(tsize_cache_[info->l1]); + cache_expr(l, r); + TRACK_LABEL_BV_ONLY(); + { double d = fp_decode(value_cache_[info->l1], src_bits); + RECORD_VALUE((uint64_t)std::llrint(d)); } + continue; + } + // floating-point binary intrinsics (minnum/maxnum/copysign). Either operand + // may be a concrete constant (label 0, value in op1/op2). + else if (info->op == __dfsan::fp_min || info->op == __dfsan::fp_max || + info->op == __dfsan::fp_copysign) { + unsigned bits = info->size; + z3::expr b1 = (info->l1 >= CONST_OFFSET) ? + get_cached_expr(info->l1, input_deps) : + context_.bv_val((uint64_t)info->op1.i, bits); + z3::expr b2 = (info->l2 >= CONST_OFFSET) ? + get_cached_expr(info->l2, input_deps) : + context_.bv_val((uint64_t)info->op2.i, bits); + uint64_t v1 = (info->l1 >= CONST_OFFSET) ? value_cache_[info->l1] : info->op1.i; + uint64_t v2 = (info->l2 >= CONST_OFFSET) ? value_cache_[info->l2] : info->op2.i; + double a = fp_decode(v1, bits), b = fp_decode(v2, bits), rv = 0.0; + z3::expr r(context_); + if (info->op == __dfsan::fp_copysign) { + // copysign is pure bit manipulation: magnitude of x, sign bit of y. + z3::expr signmask = context_.bv_val((uint64_t)1 << (bits - 1), bits); + r = (b1 & ~signmask) | (b2 & signmask); + rv = std::copysign(a, b); + tsize_cache_.emplace_back((info->l1 >= CONST_OFFSET) ? + tsize_cache_[info->l1] : tsize_cache_[info->l2]); + cache_expr(l, r); + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(fp_encode(rv, bits)); + continue; + } + z3::expr fp1 = bv_to_fp(context_, b1, bits); + z3::expr fp2 = bv_to_fp(context_, b2, bits); + if (info->op == __dfsan::fp_min) { + r = z3::expr(context_, Z3_mk_fpa_min(context_, fp1, fp2)); rv = std::fmin(a, b); + } else { + r = z3::expr(context_, Z3_mk_fpa_max(context_, fp1, fp2)); rv = std::fmax(a, b); + } + tsize_cache_.emplace_back((info->l1 >= CONST_OFFSET) ? + tsize_cache_[info->l1] : tsize_cache_[info->l2]); + cache_expr(l, fp_to_bv(r)); + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(fp_encode(rv, bits)); + continue; + } + // floating-point transcendentals (exp/exp2/log/log2/log10/log1p/pow). z3's + // fpa theory has no operation to invert them, so reject explicitly. In the + // out-of-process RGD chain the i2s solver (tried first) flips these guards + // numerically; the in-process path here simply cannot solve them. + else if (info->op == __dfsan::fp_exp || info->op == __dfsan::fp_exp2 || + info->op == __dfsan::fp_log || info->op == __dfsan::fp_log2 || + info->op == __dfsan::fp_log10 || info->op == __dfsan::fp_log1p || + info->op == __dfsan::fp_pow) { + fprintf(stderr, "WARNING: unsupported FP transcendental op %u " + "(i2s-only) for label %u\n", info->op & 0xff, l); + throw z3::exception("unsupported FP transcendental (i2s-only)"); + } // symsan-defined else if (info->op == __dfsan::Extract) { z3::expr base = get_cached_expr(info->l1, input_deps); @@ -1711,6 +2143,42 @@ z3::expr Z3AstParser::serialize(dfsan_label label, input_dep_set_t &deps) { RECORD_VALUE((int64_t)val1 % (int64_t)val2); break; } + // floating-point arithmetic. op1/op2 are IEEE-754 bit-vectors of `size`; + // lift to fpa, compute, lower back to BV. The rounding mode is carried in + // the high byte of `op`: plain LLVM `fadd`/etc. leave it 0 (RNE), but + // targets built with strict FP / FENV_ACCESS emit constrained intrinsics + // whose rounding TaintPass.cpp packs there (dynamic modes resolved at + // runtime via llvm.get.rounding). FRem has no rounding. (SMT-LIB directed + // modes flow through the RGD path -- z3-solver.cpp / jit.cc -- via index().) + case __dfsan::FAdd: case __dfsan::FSub: + case __dfsan::FMul: case __dfsan::FDiv: case __dfsan::FRem: { + z3::expr f1 = bv_to_fp(context_, op1, size); + z3::expr f2 = bv_to_fp(context_, op2, size); + z3::expr rm = get_arith_rm(context_, info->op >> 8); + z3::expr fr(context_); + switch (info->op & 0xff) { + case __dfsan::FAdd: fr = z3::expr(context_, Z3_mk_fpa_add(context_, rm, f1, f2)); break; + case __dfsan::FSub: fr = z3::expr(context_, Z3_mk_fpa_sub(context_, rm, f1, f2)); break; + case __dfsan::FMul: fr = z3::expr(context_, Z3_mk_fpa_mul(context_, rm, f1, f2)); break; + case __dfsan::FDiv: fr = z3::expr(context_, Z3_mk_fpa_div(context_, rm, f1, f2)); break; + // NOTE: z3 fpa_rem is the IEEE-754 remainder, which differs from + // LLVM frem / C fmod for some inputs (sign/magnitude of result). + case __dfsan::FRem: fr = z3::rem(f1, f2); break; + } + cache_expr(l, fp_to_bv(fr)); + TRACK_LABEL_BV_ONLY(); + RECORD_VALUE(eval_fp_binop(info->op & 0xff, val1, val2, size)); + break; + } + // floating-point comparison: lift operands to fpa, build a bool. + case __dfsan::FCmp: { + z3::expr f1 = bv_to_fp(context_, op1, size); + z3::expr f2 = bv_to_fp(context_, op2, size); + cache_expr(l, get_fcmp(f1, f2, info->op >> 8)); + TRACK_LABEL_PROPAGATE_BOTH(); + RECORD_VALUE(eval_fcmp(info->op >> 8, val1, val2, size) ? 1 : 0); + break; + } // relational case __dfsan::ICmp: { // Note: string function ICmps are handled early before BV creation diff --git a/tests/fcmp.c b/tests/fcmp.c new file mode 100644 index 00000000..d17096d8 --- /dev/null +++ b/tests/fcmp.c @@ -0,0 +1,59 @@ +// FP equality solving (in-process z3 solver only; the RGD/fastgen path does not +// model FP, so there are no KO_USE_FASTGEN RUN lines here). An else-if chain +// selects on an exact double match (x == 1.2) or an exact float match +// (y == 2.1f). The seed ("A"*20) misses both, so a single concolic run flips +// both comparisons and emits one input per branch. Note: the solver emits the +// nested (else-if) float solution first, so id-0-0-0 hits Good2 and id-0-0-1 +// hits Good1. +// +// 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-FLT %s +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN-DBL %s +// RUN: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN-FLT %s +// RUN: %t.uninstrumented %t.out/id-0-0-1 | FileCheck --check-prefix=CHECK-GEN-DBL %s + +#include +#include +#include +#include +#include "lib.h" + +int main (int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + char buf[20]; + size_t ret; + + FILE* fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x = 0; + float y = 0; + memcpy(&x, buf, sizeof x); + memcpy(&y, buf + 10, sizeof y); + + if (x == 1.2) { + // CHECK-GEN-DBL: Good1 + printf("Good1\n"); + } else if (y == 2.1f) { + // CHECK-GEN-FLT: Good2 + printf("Good2\n"); + } else { + // CHECK-ORIG: Bad + printf("Bad\n"); + } + + return 0; +} diff --git a/tests/fp_arith.c b/tests/fp_arith.c new file mode 100644 index 00000000..3137f3a2 --- /dev/null +++ b/tests/fp_arith.c @@ -0,0 +1,72 @@ +// Floating-point solving smoke test (in-process z3 solver only; the RGD/fastgen +// path does not model FP). Reads 8 tainted bytes into a double and exercises, +// in four independent branches, FP arithmetic (FMul/FSub), an FP->int cast +// (FPToSI), the sqrt libcall (custom wrapper), and the fabs intrinsic, each +// guarding a hard-to-reach branch. The seed (x = 1.0) misses all four, so a +// single concolic run flips each and emits one input per branch -- see +// tests/switch.c for the same multi-output pattern. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c"import struct,sys; sys.stdout.buffer.write(struct.pack(' %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// 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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[8] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x; + memcpy(&x, buf, sizeof x); + + // arithmetic + FP compare guarding a hard-to-reach branch + if (x * 2.0 - 1.5 > 3.0) { + // CHECK-GEN1: BRANCH_ARITH + printf("BRANCH_ARITH\n"); + } + // FP -> int cast + if ((int)x == 42) { + // CHECK-GEN2: BRANCH_CAST + printf("BRANCH_CAST\n"); + } + // intrinsic + if (sqrt(x) > 5.0) { + // CHECK-GEN3: BRANCH_SQRT + printf("BRANCH_SQRT\n"); + } + // fabs intrinsic + compare + if (fabs(x) < 0.25) { + // CHECK-GEN4: BRANCH_FABS + printf("BRANCH_FABS\n"); + } + + // CHECK-ORIG: done + printf("done\n"); + return 0; +} diff --git a/tests/fp_arith_i2s.c b/tests/fp_arith_i2s.c new file mode 100644 index 00000000..73c0b389 --- /dev/null +++ b/tests/fp_arith_i2s.c @@ -0,0 +1,97 @@ +// Floating-point input-to-state solving THROUGH a single arithmetic op. +// +// Each guard compares an input-derived value that has passed through one FP +// arithmetic op against a constant (`x op C K`). The i2s solver inverts +// the arithmetic to recover the input (write `K-C`, `K/C`, ... into the bytes), +// the same RedQueen trick it uses for integer `x op C == K` -- no z3 required. +// Constants are chosen so every inversion is exact (no rounding), and every +// guess is verified before i2s commits it. +// +// Covers FAdd/FMul (float) and FSub/FDiv (double), including a constant on the +// left of a non-commutative op (`10.0 - x`). The checks are independent (not +// nested behind an early bail), so one concolic run emits one input per check +// (see tests/switch.c). +// +// The %afltest lines exercise the out-of-process RGD path with i2s only (no +// SYMSAN_USE_Z3), demonstrating that i2s alone solves each branch; the %fgtest / +// KO_USE_Z3 lines additionally confirm the in-process z3 solver. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*40)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %afltest %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: %t.uninstrumented %t.out/id-0-0-4 | FileCheck --check-prefix=CHECK-GEN5 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: %t.uninstrumented %t.out/id-0-0-4 | FileCheck --check-prefix=CHECK-GEN5 %s +// RUN: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: %t.uninstrumented %t.out/id-0-0-4 | FileCheck --check-prefix=CHECK-GEN5 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[40] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + float x0, x1; + double x2, x3, x4; + memcpy(&x0, buf + 0, sizeof x0); + memcpy(&x1, buf + 4, sizeof x1); + memcpy(&x2, buf + 8, sizeof x2); + memcpy(&x3, buf + 16, sizeof x3); + memcpy(&x4, buf + 24, sizeof x4); + + // Seed is all zeros, so every guard fails. i2s inverts the one arithmetic op + // in each guard to recover the required input. + if (x0 + 3.5f == 10.0f) { // FAdd (float): x0 = 6.5 + // CHECK-GEN1: Good1 + printf("Good1\n"); + } + if (x1 * 2.0f == 9.0f) { // FMul (float): x1 = 4.5 + // CHECK-GEN2: Good2 + printf("Good2\n"); + } + if (x2 - 1.25 == 100.0) { // FSub (double, const rhs): x2 = 101.25 + // CHECK-GEN3: Good3 + printf("Good3\n"); + } + if (x3 / 4.0 == 2.5) { // FDiv (double, const rhs): x3 = 10.0 + // CHECK-GEN4: Good4 + printf("Good4\n"); + } + if (10.0 - x4 == 3.0) { // FSub (double, const lhs): x4 = 7.0 + // CHECK-GEN5: Good5 + printf("Good5\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/fp_cast64.c b/tests/fp_cast64.c new file mode 100644 index 00000000..0ad8b849 --- /dev/null +++ b/tests/fp_cast64.c @@ -0,0 +1,60 @@ +// 64-bit FP->int cast solving (in-process z3 solver only). Regression test for +// the fpa.to_sbv/to_ubv partial-function boundary bug: INT64_MAX / UINT64_MAX +// are not representable as doubles and round *up* to 2^63 / 2^64 (outside the +// target range, where the conversion is undefined), so an over-loose upper +// range bound let the solver pick that out-of-range point and assign the result +// freely -- producing an input that does not actually satisfy the branch. Each +// branch below casts a tainted double to a 64-bit integer and compares to a +// concrete value; the all-zero seed misses both, so a single concolic run flips +// each and emits one input per branch (see tests/switch.c for the pattern). +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*16)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// 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: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %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 +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[16] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x0, x1; + memcpy(&x0, buf + 0, sizeof x0); + memcpy(&x1, buf + 8, sizeof x1); + + // FPToSI to a 64-bit signed integer + if ((int64_t)x0 == 1000000) { + // CHECK-GEN1: BRANCH_S64 + printf("BRANCH_S64\n"); + } + // FPToUI to a 64-bit unsigned integer + if ((uint64_t)x1 == 2000000) { + // CHECK-GEN2: BRANCH_U64 + printf("BRANCH_U64\n"); + } + + // CHECK-ORIG: done + printf("done\n"); + return 0; +} diff --git a/tests/fp_challenge_double.c b/tests/fp_challenge_double.c new file mode 100644 index 00000000..ab26a640 --- /dev/null +++ b/tests/fp_challenge_double.c @@ -0,0 +1,89 @@ +// Floating-point solving, float64 challenge. Adapted from the AFL +// "test-double.c" challenge: land four float64 values (read from tainted bytes) +// inside very tight ranges / on an exact constant that random mutation can't hit. +// +// Like the float32 challenge (tests/fp_challenge_float.c), these guards are +// DIRECT comparisons of an input-derived double against a constant, so the +// *input-to-state* (i2s) solver flips them by copying the constant's IEEE-754 +// bytes into the input -- no FP arithmetic reasoning / z3 required. The last +// check is an EXACT double equality (`x3 == pi`), which i2s solves by writing +// pi's exact bit pattern; the AFL original notes "no fuzzer can solve double +// arithmetic", so the exact-equality form is the hardest case still i2s-solvable. +// The four checks are independent (not nested behind an early bail), so one +// concolic run emits one input per check (see tests/switch.c). +// +// The %afltest lines exercise the out-of-process RGD path with i2s only (no +// SYMSAN_USE_Z3), demonstrating that i2s alone solves the challenge; the +// %fgtest / KO_USE_Z3 lines additionally confirm the in-process z3 solver. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*32)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %afltest %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[32] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x0, x1, x2, x3; + memcpy(&x0, buf + 0, sizeof x0); + memcpy(&x1, buf + 8, sizeof x1); + memcpy(&x2, buf + 16, sizeof x2); + memcpy(&x3, buf + 24, sizeof x3); + + // Seed is all zeros, so every value misses its target. Each guard is a direct + // float64 comparison against a constant, so i2s copies the boundary (or, for + // the exact case, the constant itself) into the input. + if (x0 >= 0.01 && x0 <= 0.99) { + // CHECK-GEN1: Good1 + printf("Good1\n"); + } + if (x1 >= 101.9 && x1 <= 109.0) { + // CHECK-GEN2: Good2 + printf("Good2\n"); + } + if (x2 >= 22222221.9 && x2 <= 22222225.1) { + // CHECK-GEN3: Good3 + printf("Good3\n"); + } + if (x3 == 3.141592653589793116) { // exact match + // CHECK-GEN4: Good4 + printf("Good4\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/fp_challenge_float.c b/tests/fp_challenge_float.c new file mode 100644 index 00000000..5c66ef42 --- /dev/null +++ b/tests/fp_challenge_float.c @@ -0,0 +1,81 @@ +// Floating-point solving, float32 challenge. Adapted from the AFL +// "test-float.c" challenge, which requires landing three float32 values (read +// from tainted bytes) inside very tight ranges that random mutation can't hit. +// +// These challenges are meant for the *input-to-state* (i2s) solver: each guard +// is a DIRECT comparison of an input-derived float against a constant (no FP +// arithmetic in between), so i2s can flip it by copying the constant's IEEE-754 +// bytes into the input. A two-sided range `lo <= x && x <= hi` short-circuits +// after its first condition, but flipping that first bound to the boundary +// (x = lo) already lands inside the range (lo <= hi), so a single concolic run +// solves each range -- no arithmetic reasoning / z3 required. The three checks +// are independent (not nested behind an early bail), so one run emits one input +// per check (see tests/switch.c for the multi-output pattern). +// +// The %afltest lines exercise the out-of-process RGD path with i2s only (no +// SYMSAN_USE_Z3), demonstrating that i2s alone solves the challenge; the +// %fgtest / KO_USE_Z3 lines additionally confirm the in-process z3 solver. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*12)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %afltest %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-2 | FileCheck --check-prefix=CHECK-GEN3 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[12] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + float x0, x1, x2; + memcpy(&x0, buf + 0, sizeof x0); + memcpy(&x1, buf + 4, sizeof x1); + memcpy(&x2, buf + 8, sizeof x2); + + // Seed is all zeros, so every value misses its range. Each guard is a direct + // float32 comparison against a constant, so i2s copies the boundary into the + // input; flipping the first bound to the boundary lands inside the range. + if (x0 >= 1000000.01f && x0 <= 1000010.99f) { + // CHECK-GEN1: Good1 + printf("Good1\n"); + } + if (x1 >= 101.9f && x1 <= 109.0f) { + // CHECK-GEN2: Good2 + printf("Good2\n"); + } + if (x2 >= 22222221.9f && x2 <= 22222225.1f) { + // CHECK-GEN3: Good3 + printf("Good3\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/fp_i2s_offset.c b/tests/fp_i2s_offset.c new file mode 100644 index 00000000..57b6a9af --- /dev/null +++ b/tests/fp_i2s_offset.c @@ -0,0 +1,70 @@ +// Regression test: standalone i2s (I2SSolver) must write the inverted FP value +// to the offset the arith operand ACTUALLY reads, not to an unrelated input that +// merely holds a coincidentally-equal value. +// +// Each guard is `X == Y op C` where Y (not X) is the symbolic operand of a single +// FP arith op against a constant C. On the all-zero seed X == Y == 0, so feeding +// X through `op C` yields the same value as `Y op C` -- the old value-only +// structural check matched the X candidate (which sorts first) and wrote the +// inverted result to X, corrupting an unrelated input and leaving the guard +// UNsatisfied. solve_fcmp now anchors each candidate to the arith operand's +// Read offset, so it writes to Y instead and the guard flips. +// +// The two operands are NON-adjacent (a gap of unused bytes sits between them) so +// each is its own 4/8-byte i2s candidate -- the case where the wrong-offset write +// was observable. Solved by standalone i2s alone, so only %afltest RUN lines +// (no SYMSAN_USE_JIGSAW / SYMSAN_USE_Z3, no in-process z3 %fgtest lines). +// +// The checks are independent (not nested behind an early bail), so one concolic +// run emits one input per check (see tests/switch.c). +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*48)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %afltest %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 +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[48] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double a, b; + float fa, fb; + memcpy(&a, buf + 0, sizeof a); // direct operand of guard 1 + memcpy(&b, buf + 16, sizeof b); // arith operand of guard 1 (gap 8..15) + memcpy(&fa, buf + 32, sizeof fa); // direct operand of guard 2 + memcpy(&fb, buf + 44, sizeof fb); // arith operand of guard 2 (gap 36..43) + + // The arith operand (b / fb) is on the RIGHT; the plain input (a / fa) is on + // the LEFT and sorts first among candidates. The fix must write to b / fb. + if (a == b + 1.0) { // b = -1.0 (so a == 0 == b + 1.0) + // CHECK-GEN1: GoodDbl + printf("GoodDbl\n"); + } + if (fa == fb + 1.0f) { // fb = -1.0f (so fa == 0 == fb + 1.0f) + // CHECK-GEN2: GoodFloat + printf("GoodFloat\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/fp_libcall.c b/tests/fp_libcall.c new file mode 100644 index 00000000..ba40f738 --- /dev/null +++ b/tests/fp_libcall.c @@ -0,0 +1,69 @@ +// FP predicate / rounding libcall solving (in-process z3 solver only; the +// RGD/fastgen path does not model FP). Exercises the custom wrappers that +// replace the old (broken) `functional` ABI for math libcalls: lrint / llrint +// (round-to-nearest then convert to integer) and __signbit (IEEE sign bit). +// clang keeps these as real calls (isnan/isinf/finite are lowered inline and +// are already covered by the FCmp path). Reads tainted bytes into three +// doubles; the all-zero seed misses every branch, so a single concolic run +// flips each and emits one input per branch -- see tests/switch.c for the same +// multi-output pattern. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*24)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// 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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[24] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x0, x1, x2; + memcpy(&x0, buf + 0, sizeof x0); + memcpy(&x1, buf + 8, sizeof x1); + memcpy(&x2, buf + 16, sizeof x2); + + // round-to-nearest-integer libcall (custom fp_lrint wrapper) + if (lrint(x0) == 42) { + // CHECK-GEN1: BRANCH_LRINT + printf("BRANCH_LRINT\n"); + } + // 64-bit round-to-integer libcall + if (llrint(x1) == 1000) { + // CHECK-GEN2: BRANCH_LLRINT + printf("BRANCH_LLRINT\n"); + } + // IEEE sign-bit predicate libcall (custom fp_signbit wrapper) + if (__signbit(x2)) { + // CHECK-GEN3: BRANCH_SIGNBIT + printf("BRANCH_SIGNBIT\n"); + } + + // CHECK-ORIG: done + printf("done\n"); + return 0; +} diff --git a/tests/fp_rounding.c b/tests/fp_rounding.c new file mode 100644 index 00000000..9f80e844 --- /dev/null +++ b/tests/fp_rounding.c @@ -0,0 +1,94 @@ +// Directed floating-point rounding captured in the real-symex instrumentation +// path AND honored by the solvers' arithmetic model. +// +// Built with -frounding-math and FENV_ACCESS ON, so clang lowers the FP square +// and comparison to @llvm.experimental.constrained.* intrinsics; the +// fesetround(FE_DOWNWARD) makes the multiply's rounding mode `round.dynamic` +// (resolved at run time from MXCSR). TaintPass captures these intrinsics +// (before the blanket llvm.experimental filter) and packs the rounding-mode +// selector into the high byte of the union op; the solvers read it back +// (parsers/rgd-parser.cpp for the RGD/jigsaw chain, solvers/z3-ts.cpp for the +// in-process z3 path) and model round-toward-negative multiplication. +// +// The guard is `x*x == 0x40488000000000ab` (== RTN(x*x) for the solution, one +// ULP below the round-to-nearest square). This is chosen to actually TEST the +// rounding model, not just capture: +// * two symbolic operands (x*x) => i2s cannot invert it (i2s would otherwise +// solve any branch via a concrete candidate + re-execution, which is +// rounding-correct regardless of the model and would mask a regression); +// * an exact FP equality => jigsaw gradient descent cannot land it either; +// * so z3 must solve it, and `x*x == target` is SAT under round-toward- +// negative but UNSAT under round-to-nearest (verified with z3). +// A solver that fails to honor the captured rounding mode therefore finds the +// formula UNSAT, produces no input, and the CHECK-GEN re-execution below fails. +// The generated input is validated against the -frounding-math oracle (which +// rounds downward for real): it must print HIT. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*8)' > %t.bin +// RUN: clang -O1 -frounding-math -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// +// in-process z3 solver via fgtest (TaintPass capture -> z3-ts.cpp): +// RUN: env KO_USE_FASTGEN=1 %ko-clang -frounding-math -o %t.fg %s -lm +// 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 +// +// out-of-process RGD chain via afltest (TaintPass capture -> rgd-parser.cpp; +// i2s and jigsaw GD both reject exact x*x equality, so z3 in the chain solves): +// RUN: rm -rf %t.out && mkdir -p %t.out +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" SYMSAN_USE_JIGSAW=1 SYMSAN_USE_Z3=1 %afltest %t.fg %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s +// +// in-process z3 solver in the instrumented runtime (KO_USE_Z3): +// RUN: rm -rf %t.out && mkdir -p %t.out +// RUN: env KO_USE_Z3=1 %ko-clang -frounding-math -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include +#include +#include "lib.h" +#pragma STDC FENV_ACCESS ON + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[8] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x; + memcpy(&x, buf, sizeof x); + + // Directed-rounded square of a symbolic operand. FE_DOWNWARD => the + // constrained.fmul carries round.dynamic and rounds toward -inf at run time. + // x*x (two symbolic refs) is invertible by neither i2s nor gradient descent, + // so the rounding-aware z3 model must solve the equality below. + fesetround(FE_DOWNWARD); + double y = x * x; + fesetround(FE_TONEAREST); + + // target == RTN(x*x) for the solution, 1 ULP below the RNE square. x*x==target + // is SAT under round-toward-negative but UNSAT under round-to-nearest. + uint64_t tb = 0x40488000000000abULL; + double target; + memcpy(&target, &tb, sizeof target); + + if (y == target) { + // CHECK-GEN: HIT + printf("HIT\n"); + return 0; + } + // CHECK-ORIG: MISS + printf("MISS\n"); + return 0; +} diff --git a/tests/fp_solving.c b/tests/fp_solving.c new file mode 100644 index 00000000..732908be --- /dev/null +++ b/tests/fp_solving.c @@ -0,0 +1,52 @@ +// Floating-point solving (in-process z3 solver only; jigsaw/RGD does not model FP). +// A single hard-to-reach branch whose guard combines the fabs intrinsic, the +// sqrt libcall (modeled via the dfsan_custom.cpp wrapper), and FP arithmetic +// that clang contracts into @llvm.fmuladd (a*b+c, -ffp-contract=on) followed by +// an FP comparison (FCmp). The seed misses the branch; the z3 solver must +// reconstruct the FP expression and produce an input that flips it. +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c"import struct,sys; sys.stdout.buffer.write(struct.pack(' %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// 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: env KO_USE_Z3=1 %ko-clang -o %t.z3 %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %t.z3 %t.bin +// RUN: %t.uninstrumented %t.out/id-0-0-0 | FileCheck --check-prefix=CHECK-GEN %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[8] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x; + memcpy(&x, buf, sizeof x); + + // fabs (intrinsic) + sqrt (custom libcall wrapper) + `a*2.0+1.0` (contracted + // to @llvm.fmuladd) + FCmp. Seed x=1.0 => sqrt(1)*2+1 = 3.0, fails the guard. + if (sqrt(fabs(x)) * 2.0 + 1.0 > 20.0) { + // CHECK-GEN: GOOD + printf("GOOD\n"); + } else { + // CHECK-ORIG: Bad + printf("Bad\n"); + } + + return 0; +} diff --git a/tests/i2s_offset.c b/tests/i2s_offset.c new file mode 100644 index 00000000..476348dd --- /dev/null +++ b/tests/i2s_offset.c @@ -0,0 +1,79 @@ +// Regression test: standalone i2s (I2SSolver) must write the inverted integer +// value to the offset the operand ACTUALLY reads, not to an unrelated input that +// merely holds a coincidentally-equal value. +// +// This is the integer analogue of tests/fp_i2s_offset.c. Each guard pairs a +// single-arith-op operand (`X op C`) with a plain-Read operand. On the all-zero +// seed every symbolic byte is 0, so the arith operand's raw input bytes (0) equal +// the OTHER (plain-Read) operand's value (0). solve_icmp's direct-match branch +// compares candidates by value only, so it matched the arith operand's candidate +// against the plain-Read operand and wrote that operand's inverted value to the +// arith input -- the WRONG offset, leaving the guard UNsatisfied. solve_icmp now +// anchors each direct match to the compared side's Read offset, so the coincidental +// candidate is rejected and the arith side is solved through the binop branch. +// +// The two operands of each guard are NON-adjacent (a gap of unused bytes sits +// between them) so each is its own 8-byte i2s candidate -- adjacent operands merge +// into one >8-byte run that solve_icmp skips, hiding the bug. The arith operand +// sits at the LOWER offset so its candidate is visited first (the premature match). +// +// Guard A exercises the coincidental match against op2 (right side is the plain +// Read); guard B against op1 (left side is the plain Read). +// +// Solved by standalone i2s alone, so only %afltest RUN lines (no +// SYMSAN_USE_JIGSAW / SYMSAN_USE_Z3, no in-process z3 %fgtest lines). +// +// The checks are independent (not nested behind an early bail), so one concolic +// run emits one input per check (see tests/switch.c). +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*64)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %afltest %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 +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[64] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + int64_t bb, aa, cc, dd; + memcpy(&bb, buf + 0, sizeof bb); // guard A arith input (lower offset) + memcpy(&aa, buf + 16, sizeof aa); // guard A plain-Read operand (gap 8..15) + memcpy(&cc, buf + 32, sizeof cc); // guard B arith input (lower offset, gap 24..31) + memcpy(&dd, buf + 48, sizeof dd); // guard B plain-Read operand (gap 40..47) + + // Guard A: the arith side (bb + 1) is on the LEFT (op1); the plain Read (aa) is + // on the RIGHT (op2). On the zero seed bb's bytes (0) equal aa's value (0), so + // the old value-only check matched bb's candidate against op2 and wrote to bb. + if (bb + 1 == aa) { // solved by bb = -1 (so bb + 1 == 0 == aa) + // CHECK-GEN1: GoodA + printf("GoodA\n"); + } + // Guard B: the plain Read (dd) is on the LEFT (op1); the arith side (cc + 1) is + // on the RIGHT (op2). cc's bytes (0) equal dd's value (0), matching op1. + if (dd == cc + 1) { // solved by cc = -1 (so cc + 1 == 0 == dd) + // CHECK-GEN2: GoodB + printf("GoodB\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/i2s_transcendental.c b/tests/i2s_transcendental.c new file mode 100644 index 00000000..91e2faf9 --- /dev/null +++ b/tests/i2s_transcendental.c @@ -0,0 +1,111 @@ +// Floating-point input-to-state solving THROUGH a transcendental libcall. +// +// Each guard compares an input-derived value that has passed through one FP +// transcendental (exp/exp2/log/log2/log10/log1p/pow) against a constant. z3's +// fpa theory has NO way to invert these, and jigsaw is integer-only -- both +// reject them (see z3-solver.cpp / z3-ts.cpp / jit.cc). The i2s solver instead +// computes the closed-form libm inverse (log for exp, exp for log, pow(10,.) +// for log10, s^(1/c) for pow, log(K)/log(c) for a constant-base pow, ...) writes +// those IEEE-754 bytes into the input, and VERIFIES the guess end-to-end before +// committing. So these branches are solvable by i2s ALONE. +// +// Because only i2s (in the out-of-process RGD chain) can invert transcendentals, +// this test uses ONLY the %afltest RUN lines. It deliberately omits the +// %fgtest / KO_USE_Z3 lines that tests/fp_arith_i2s.c keeps: those exercise the +// in-process z3 path, which has no i2s and therefore cannot solve these guards. +// +// NOTE on pow exponents/bases: LLVM's simplify-libcalls (optimizePow) rewrites +// the degenerate forms pow(x,2.0) -> x*x and pow(2.0,x) -> exp2(x) even without +// -ffast-math, which would bypass the __dfsw_pow wrapper entirely. We therefore +// use a cube (exponent 3.0) for the exponent-constant case and base 3.0 for the +// base-constant case, both of which keep the pow libcall (and thus the fp_pow op). +// +// The checks are independent (not nested behind an early bail), so one concolic +// run emits one input per check (see tests/switch.c). +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*56)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" %afltest %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: %t.uninstrumented %t.out/id-0-0-4 | FileCheck --check-prefix=CHECK-GEN5 %s +// RUN: %t.uninstrumented %t.out/id-0-0-5 | FileCheck --check-prefix=CHECK-GEN6 %s +// RUN: %t.uninstrumented %t.out/id-0-0-6 | FileCheck --check-prefix=CHECK-GEN7 %s +// RUN: %t.uninstrumented %t.out/id-0-0-7 | FileCheck --check-prefix=CHECK-GEN8 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[56] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x0, x1, x3, x4, x7; + float x2, x5, x6; + memcpy(&x0, buf + 0, sizeof x0); // exp2 (double) + memcpy(&x1, buf + 8, sizeof x1); // log (double) + memcpy(&x2, buf + 16, sizeof x2); // log2f (float) + memcpy(&x3, buf + 20, sizeof x3); // log10 (double) + memcpy(&x4, buf + 28, sizeof x4); // log1p (double) + memcpy(&x5, buf + 36, sizeof x5); // powf (float, exponent const) + memcpy(&x6, buf + 40, sizeof x6); // expf (float) + memcpy(&x7, buf + 44, sizeof x7); // pow (double, base const) + + // Seed is all zeros, so every guard fails. i2s inverts the one transcendental + // in each guard to recover the required input. Constants are chosen so the + // exact-inverse cases round-trip precisely (powers), and the inequality cases + // (log1p/expf) leave margin for FP rounding. + if (exp2(x0) == 8.0) { // x0 = log2(8) = 3.0 + // CHECK-GEN1: Good1 + printf("Good1\n"); + } + if (log(x1) == 0.0) { // x1 = exp(0) = 1.0 + // CHECK-GEN2: Good2 + printf("Good2\n"); + } + if (log2f(x2) == 3.0f) { // x2 = exp2(3) = 8.0 + // CHECK-GEN3: Good3 + printf("Good3\n"); + } + if (log10(x3) == 2.0) { // x3 = pow(10,2) = 100.0 + // CHECK-GEN4: Good4 + printf("Good4\n"); + } + if (log1p(x4) > 1.0) { // x4 > expm1(1) ~= 1.718 + // CHECK-GEN5: Good5 + printf("Good5\n"); + } + if (powf(x5, 3.0f) == 27.0f) { // x5 = 27^(1/3) = 3.0 (exponent-const) + // CHECK-GEN6: Good6 + printf("Good6\n"); + } + if (expf(x6) > 5.0f) { // x6 > log(5) ~= 1.609 + // CHECK-GEN7: Good7 + printf("Good7\n"); + } + if (pow(3.0, x7) == 81.0) { // x7 = log(81)/log(3) = 4.0 (base-const) + // CHECK-GEN8: Good8 + printf("Good8\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/jigsaw_fp.c b/tests/jigsaw_fp.c new file mode 100644 index 00000000..18765ed3 --- /dev/null +++ b/tests/jigsaw_fp.c @@ -0,0 +1,97 @@ +// Floating-point solving in the JIGSAW solver (JIT + numeric gradient descent). +// +// The out-of-process RGD chain runs i2s -> jigsaw -> z3 (optimistic; first SAT +// wins). Each guard below compares a value that has passed through an FP op +// with TWO symbolic operands (or an FP intrinsic) against a constant. i2s can +// only invert a single FP op with a *constant* operand, so it rejects all of +// these; jigsaw JIT-compiles the constraint, computes a measurable FP distance +// (0 == satisfied), and follows the byte gradient to a solution. +// +// These are all INEQUALITIES on purpose: gradient descent excels at driving a +// measurable distance to zero, but cannot reliably land on an exact FP equality +// (e.g. x*x == 25.0), which is z3's job. So this test isolates jigsaw's +// strength. +// +// RUN lines use %afltest with SYMSAN_USE_JIGSAW=1 and WITHOUT SYMSAN_USE_Z3: +// enabling z3 would let z3 mask a jigsaw regression, and i2s alone solves none +// of these (verified). There are no %fgtest / KO_USE_Z3 lines because those +// exercise the in-process z3 path, which has no jigsaw/i2s chain. +// +// The checks are independent (not nested behind an early bail), so one concolic +// run emits one input per check (see tests/switch.c). +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*60)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" SYMSAN_USE_JIGSAW=1 %afltest %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s +// RUN: %t.uninstrumented %t.out/id-0-0-3 | FileCheck --check-prefix=CHECK-GEN4 %s +// RUN: %t.uninstrumented %t.out/id-0-0-4 | FileCheck --check-prefix=CHECK-GEN5 %s +// RUN: %t.uninstrumented %t.out/id-0-0-5 | FileCheck --check-prefix=CHECK-GEN6 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[60] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double x0, x1, x2, x3, x4, x5, x6; + float fx; + memcpy(&x0, buf + 0, sizeof x0); // FMul (x0*x0) + memcpy(&x1, buf + 8, sizeof x1); // FAdd operand + memcpy(&x2, buf + 16, sizeof x2); // FAdd operand + memcpy(&x3, buf + 24, sizeof x3); // FpSqrt + memcpy(&fx, buf + 32, sizeof fx); // float FMul + memcpy(&x4, buf + 36, sizeof x4); // FNeg + memcpy(&x5, buf + 44, sizeof x5); // FpMin operand + memcpy(&x6, buf + 52, sizeof x6); // FpMin operand + + // Seed is all zeros, so every guard fails. Each branch has an FP op that i2s + // cannot invert (two symbolic operands, or an intrinsic), so jigsaw's + // gradient descent must find the solution. + if (x0 * x0 > 25.0) { // two symbolic operands (same var) + // CHECK-GEN1: GoodMul + printf("GoodMul\n"); + } + if (x1 + x2 > 100.0) { // two symbolic vars + // CHECK-GEN2: GoodAdd + printf("GoodAdd\n"); + } + if (sqrt(x3) > 3.0) { // FP intrinsic (i2s rejects sqrt) + // CHECK-GEN3: GoodSqrt + printf("GoodSqrt\n"); + } + if (fx * fx > 9.0f) { // float path (compare promoted to double) + // CHECK-GEN4: GoodFMul + printf("GoodFMul\n"); + } + if (-x4 > 10.0) { // FNeg + // CHECK-GEN5: GoodNeg + printf("GoodNeg\n"); + } + if (fmin(x5, x6) > 7.0) { // FpMin, two symbolic vars + // CHECK-GEN6: GoodMin + printf("GoodMin\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/jigsaw_fp_i2s.c b/tests/jigsaw_fp_i2s.c new file mode 100644 index 00000000..fb0e2d33 --- /dev/null +++ b/tests/jigsaw_fp_i2s.c @@ -0,0 +1,88 @@ +// Floating-point input-to-state (RedQueen) heuristic in the JIGSAW solver. +// +// Jigsaw has a built-in in-solver i2s fast path (`try_i2s` in solvers/jigsaw/ +// gd.cc) that snaps a raw input chunk onto a comparison operand before running +// gradient descent. It now handles FP comparisons: it slides an FP-sized window +// across each consecutive-input-byte candidate, matches the window's float/double +// value against a stored operand, and snaps it to satisfy the predicate against +// the other operand (verifying every guess with fp_get_distance == 0). +// +// Each guard below is an exact FP EQUALITY with TWO symbolic operands, and the +// two operands are ADJACENT in the file, so they merge into ONE oversized (16 +// byte) i2s candidate run. That is exactly the case the sliding FP window was +// added to handle -- and it also isolates jigsaw, because: +// - standalone i2s (I2SSolver, first in the chain) only decodes candidate runs +// that are exactly 4 or 8 bytes, so it skips the merged 16-byte run entirely; +// and +// - gradient descent cannot reliably land on an exact FP equality. +// So only jigsaw's i2s heuristic can flip these. (The float path -- 4-byte +// window, is_float -- is identical code and is exercised by the out-of-tree +// probe; a committed float guard is omitted because a standalone-i2s quirk grabs +// float arith comparisons first.) +// +// RUN lines use %afltest with SYMSAN_USE_JIGSAW=1 and WITHOUT SYMSAN_USE_Z3, to +// isolate jigsaw (enabling z3 would let it mask a jigsaw regression). No %fgtest +// / KO_USE_Z3 lines: those exercise the in-process z3 path, which has no +// jigsaw/i2s chain. +// +// The checks are independent (not nested behind an early bail), so one concolic +// run emits one input per check (see tests/switch.c). +// +// RUN: rm -rf %t.out +// RUN: mkdir -p %t.out +// RUN: python -c'import sys; sys.stdout.buffer.write(b"\x00"*48)' > %t.bin +// RUN: clang -O0 -o %t.uninstrumented %s -lm +// RUN: %t.uninstrumented %t.bin | FileCheck --check-prefix=CHECK-ORIG %s +// RUN: env KO_USE_FASTGEN=1 %ko-clang -o %t.fg %s -lm +// RUN: env TAINT_OPTIONS="taint_file=%t.bin output_dir=%t.out" SYMSAN_USE_JIGSAW=1 %afltest %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-2 | FileCheck --check-prefix=CHECK-GEN3 %s + +#include +#include +#include +#include +#include +#include "lib.h" + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [file]\n", argv[0]); + return -1; + } + + unsigned char buf[48] = {0}; + FILE *fp = chk_fopen(argv[1], "rb"); + chk_fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + double a, b, c, d, e, f; + memcpy(&a, buf + 0, sizeof a); // adjacent to b -> merged 16B run + memcpy(&b, buf + 8, sizeof b); + memcpy(&c, buf + 16, sizeof c); // adjacent to d -> merged 16B run + memcpy(&d, buf + 24, sizeof d); + memcpy(&e, buf + 32, sizeof e); // adjacent to f -> merged 16B run + memcpy(&f, buf + 40, sizeof f); + + // Seed is all zeros, so every equality is initially false. Two symbolic + // operands each, so jigsaw's i2s heuristic must recover the solution. The + // three guards also cover both operand-match directions: the input chunk can + // match the LEFT stored operand (op1) or the RIGHT one (op2). + if (a == b + 1.0) { // a = b + 1.0 = 1.0 (input matches op2 side) + // CHECK-GEN1: GoodAdd + printf("GoodAdd\n"); + } + if (c == d - 5.0) { // c = d - 5.0 = -5.0 (input matches op2 side) + // CHECK-GEN2: GoodSub + printf("GoodSub\n"); + } + if (f == e + 2.0) { // f = e + 2.0 = 2.0 (input matches op1 side) + // CHECK-GEN3: GoodRev + printf("GoodRev\n"); + } + + // CHECK-ORIG: Bad + printf("Bad\n"); + return 0; +} diff --git a/tests/lit.cfg b/tests/lit.cfg index 00699366..37979a50 100644 --- a/tests/lit.cfg +++ b/tests/lit.cfg @@ -29,9 +29,8 @@ path = os.path.pathsep.join([ ]) config.environment['PATH'] = path -# config.environment['KO_CC'] = 'clang-14' -# config.environment['KO_CXX'] = 'clang++-14' config.substitutions.append(('%ko-clang', os.path.join(bin_dir, "ko-clang"))) config.substitutions.append(('%ko-clangxx', os.path.join(bin_dir, "ko-clang++"))) config.substitutions.append(('%fgtest', os.path.join(bin_dir, "fgtest"))) +config.substitutions.append(('%afltest', os.path.join(bin_dir, "afltest")))