sots-engine/tests/shim_trace/test_hook.cpp

213 lines
8.8 KiB
C++

// 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 <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#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<std::string> lines(const std::string& s) {
std::vector<std::string> 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<FillHook>;
using HWrong = Hook<FillWrongHook>;
using HThrows = Hook<FillThrowsHook>;
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<std::uint8_t> 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<int>(wb[15]), (static_cast<int>(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<std::size_t>(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<unsigned char>(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<int>(buf[15]), static_cast<int>(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<std::size_t>(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");
}