// End-to-end: the hook template in every mode over the self-test hook, the tracer writing a // real JSONL file, and tracecmp.py judging it (exit 0 clean / 1 divergence / 2 invalid). #include #include #include #include #include #include #include #include "check.h" #include "harness.h" #include "shim/trace/hook.h" #include "shim/trace/selftest.h" #include "shim/trace/tracer.h" using namespace shim::trace; using namespace shim::selftest; static std::string slurp(const std::string& p) { std::ifstream f(p, std::ios::binary); std::stringstream ss; ss << f.rdbuf(); return ss.str(); } static std::vector lines(const std::string& s) { std::vector out; std::size_t start = 0; while (start < s.size()) { std::size_t nl = s.find('\n', start); if (nl == std::string::npos) nl = s.size(); out.push_back(s.substr(start, nl - start)); start = nl + 1; } return out; } static bool has(const std::string& hay, const char* needle) { return hay.find(needle) != std::string::npos; } using HFill = Hook; using HWrong = Hook; using HThrows = Hook; static void wire() { HFill::original = &Fill; HWrong::original = &Fill; HThrows::original = &Fill; } static std::uint32_t reference(std::uint32_t n, std::uint32_t seed) { std::vector b(n); return Fill(b.data(), n, seed); } // ---- without a tracer: modes still route correctly, nothing is written ---------------------------- static void modes_without_tracer() { wire(); std::uint8_t buf[16]; const std::uint32_t want = reference(16, 1); HFill::mode = Mode::Off; CHECK_EQ(HFill::detour()(buf, 16, 1), want); HFill::mode = Mode::Replace; CHECK_EQ(HFill::detour()(buf, 16, 1), want); // ours agrees with the original HWrong::mode = Mode::Replace; std::uint8_t wb[16]; CHECK_EQ(HWrong::detour()(wb, 16, 1), want); CHECK_EQ(static_cast(wb[15]), (static_cast(buf[15]) + 1) & 0xff); // replace ran ours (the wrong one) HFill::mode = Mode::Trace; CHECK_EQ(HFill::detour()(buf, 16, 1), want); // tracer closed: record dropped, result intact HFill::mode = Mode::Compare; CHECK_EQ(HFill::detour()(buf, 16, 1), want); CHECK_EQ(Tracer::instance().records_written(), 0u); } // ---- clean log: trace + compare (identical ours) + replace ------------------------------------------ static void clean_log(const std::string& path) { Config cfg; cfg.path = path; cfg.inline_max = 32; cfg.apply("hook.Shim::SelfTest::Fill", "trace"); Tracer& tr = Tracer::instance(); tr.configure(cfg); HFill::register_policy(tr); HWrong::register_policy(tr); CHECK(tr.open("shim_trace_test", std::string(64, 'a').c_str())); HFill::configure(tr); CHECK(HFill::mode == Mode::Trace); std::uint8_t buf[64]; CHECK_EQ(HFill::detour()(buf, 16, 7), reference(16, 7)); // trace, inline region CHECK_EQ(HFill::detour()(buf, 64, 8), reference(64, 8)); // trace, hashed region (64 > inline_max 32) HFill::mode = Mode::Compare; CHECK_EQ(HFill::detour()(buf, 16, 9), reference(16, 9)); // compare, clean CHECK_EQ(HFill::detour()(buf, 64, 10), reference(64, 10)); CHECK_EQ(HFill::detour()(buf, 0, 11), reference(0, 11)); // empty region HFill::mode = Mode::Replace; CHECK_EQ(HFill::detour()(buf, 16, 12), reference(16, 12)); // replace: nothing emitted // call ids are process-global and increase across threads; depth stays 0 per thread std::thread t([&] { std::uint8_t tb[8]; HFill::mode = Mode::Trace; HFill::detour()(tb, 8, 13); }); t.join(); tr.close(); CHECK_EQ(tr.records_written(), 6u); const std::string text = slurp(path); const auto ls = lines(text); CHECK_EQ(ls.size(), static_cast(7)); CHECK(has(ls[0], "{\"meta\":{\"format\":1,\"build\":\"shim_trace_test\",\"exe_sha256\":\"aaaa")); CHECK(has(ls[0], "\"inline_max\":32,\"hooks\":{\"Shim::SelfTest::Fill\":{\"ftol\":0,\"ftol_kind\":\"abs\",\"ptr\":\"ignore\"},\"Shim::SelfTest::FillWrong\":{")); CHECK(has(ls[1], "\"hook\":\"Shim::SelfTest::Fill\",\"mode\":\"trace\",\"call_id\":")); CHECK(has(ls[1], "\"depth\":0,\"args\":[{\"t\":\"ptr\",\"v\":\"0x")); CHECK(has(ls[1], "\"n\":\"buf\"},{\"t\":\"u32\",\"v\":16,\"n\":\"n\"},{\"t\":\"u32\",\"v\":7,\"n\":\"seed\"}],\"ret\":{\"t\":\"u32\",\"v\":")); CHECK(has(ls[1], "\"side\":{\"buf\":{\"before\":{\"t\":\"bytes\",\"n\":16,\"sha256\":\"")); CHECK(has(ls[1], "\"hex\":\"")); CHECK(has(ls[2], "\"n\":64,\"sha256\":\"")); CHECK(has(ls[2], "\"head\":\"")); CHECK(!has(ls[2], "\"hex\":\"")); CHECK(has(ls[3], "\"mode\":\"compare\"")); CHECK(has(ls[3], "\"ours\":{\"ret\":{\"t\":\"u32\",\"v\":")); CHECK(has(ls[3], "\"diverged\":false,\"diff\":[]}")); CHECK(has(ls[5], "\"n\":0,\"sha256\":\"e3b0c442")); CHECK(!has(text, "\"mode\":\"replace\"")); // the thread's record: different thread id, depth 0, a later call_id than the main thread's CHECK(has(ls[6], "\"mode\":\"trace\"")); CHECK(has(ls[6], "\"v\":13,\"n\":\"seed\"}")); // ASCII only, LF only for (char c : text) CHECK(static_cast(c) < 0x80 && c != '\r'); const int rc = tracetest::run_tracecmp(path); if (rc != tracetest::kSkipped) CHECK_EQ(rc, 0); } // ---- divergent log: wrong ours (region differs), throwing ours (err) -------------------------------- static void bad_log(const std::string& path) { Config cfg; cfg.path = path; cfg.default_mode = Mode::Compare; Tracer& tr = Tracer::instance(); tr.configure(cfg); CHECK(tr.open("shim_trace_test", "")); HWrong::configure(tr); HThrows::configure(tr); HFill::configure(tr); CHECK(HWrong::mode == Mode::Compare); std::uint8_t buf[16]; CHECK_EQ(HWrong::detour()(buf, 16, 21), reference(16, 21)); // caller still gets the original's result CHECK_EQ(static_cast(buf[15]), static_cast(buf[15])); // and the original's memory (ours wrote scratch) std::uint8_t ref[16]; Fill(ref, 16, 21); CHECK(std::memcmp(buf, ref, 16) == 0); CHECK_EQ(HThrows::detour()(buf, 16, 22), reference(16, 22)); // a throwing ours is contained CHECK_EQ(HFill::detour()(buf, 16, 23), reference(16, 23)); // and a clean call after it is still clean tr.close(); const auto ls = lines(slurp(path)); CHECK_EQ(ls.size(), static_cast(4)); CHECK(has(ls[1], "\"hook\":\"Shim::SelfTest::FillWrong\",\"mode\":\"compare\"")); CHECK(has(ls[1], "\"diverged\":true,\"diff\":[{\"path\":\"side.buf.after\",\"why\":\"hash\",\"orig\":{\"t\":\"bytes\",\"n\":16")); CHECK(has(ls[1], "\"first_diff_offset\":15}]}")); CHECK(has(ls[2], "\"hook\":\"Shim::SelfTest::FillThrows\",\"mode\":\"compare\"")); CHECK(!has(ls[2], ",\"ours\":{\"ret\"")); // err record: no ours block (the diff's "ours" key is the err text) CHECK(has(ls[2], "\"diverged\":true,\"diff\":[{\"path\":\"call\",\"why\":\"err\",\"orig\":null,\"ours\":\"ours: selftest: deliberate throw\"}],\"err\":\"ours: selftest: deliberate throw\"}")); CHECK(has(ls[3], "\"diverged\":false,\"diff\":[]}")); const int rc = tracetest::run_tracecmp(path); if (rc != tracetest::kSkipped) CHECK_EQ(rc, 1); // --hook filter on the clean hook only -> clean const int rc2 = tracetest::run_tracecmp(path, "--hook Shim::SelfTest::Fill"); if (rc2 != tracetest::kSkipped) CHECK_EQ(rc2, 0); } // ---- an unusable log (truncated last line) is exit 2 -------------------------------------------------- static void truncated_log(const std::string& src, const std::string& dst) { std::string text = slurp(src); text.resize(text.size() - 10); std::FILE* f = std::fopen(dst.c_str(), "wb"); if (f) { std::fwrite(text.data(), 1, text.size(), f); std::fclose(f); } const int rc = tracetest::run_tracecmp(dst); if (rc != tracetest::kSkipped) CHECK_EQ(rc, 2); const int rc2 = tracetest::run_tracecmp(dst, "--skip-invalid"); if (rc2 != tracetest::kSkipped) CHECK_EQ(rc2, 0); } int main(int argc, char** argv) { const std::string dir = argc > 1 ? argv[1] : "."; modes_without_tracer(); clean_log(dir + "/hook_clean.jsonl"); bad_log(dir + "/hook_bad.jsonl"); truncated_log(dir + "/hook_clean.jsonl", dir + "/hook_truncated.jsonl"); // the packaged self-test entry point used by the shim at startup Config cfg; cfg.path = dir + "/selftest.jsonl"; Tracer::instance().configure(cfg); CHECK(Tracer::instance().open("x", "")); CHECK_EQ(run_once(Mode::Compare), reference(64, 0x5eed)); Tracer::instance().close(); const int rc = tracetest::run_tracecmp(dir + "/selftest.jsonl"); if (rc != tracetest::kSkipped) CHECK_EQ(rc, 0); return tracetest::finish("shim_trace_hook"); }