222 lines
9.4 KiB
C++
222 lines
9.4 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: ours' own record
|
|
|
|
// 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(), 7u);
|
|
|
|
const std::string text = slurp(path);
|
|
const auto ls = lines(text);
|
|
CHECK_EQ(ls.size(), static_cast<std::size_t>(8));
|
|
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\","
|
|
"\"coverage\":{\"state\":\"complete\",\"why\":\"Fill writes buf[0..n) and nothing else; "
|
|
"the whole range is a declared region\",\"unmodelled\":[]}},\"Shim::SelfTest::FillWrong\":{"));
|
|
// every registered hook states its coverage, and none is left unstated
|
|
CHECK(tr.unstated_hooks().empty());
|
|
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"));
|
|
// replace now records what ours wrote: side present, no ours/diverged/diff to compare against
|
|
CHECK(has(ls[6], "\"mode\":\"replace\""));
|
|
CHECK(has(ls[6], "\"v\":12,\"n\":\"seed\"}"));
|
|
CHECK(has(ls[6], "\"side\":{\"buf\":{\"before\":{\"t\":\"bytes\""));
|
|
CHECK(!has(ls[6], "\"ours\":"));
|
|
CHECK(!has(ls[6], "\"diverged\":"));
|
|
// the thread's record: different thread id, depth 0, a later call_id than the main thread's
|
|
CHECK(has(ls[7], "\"mode\":\"trace\""));
|
|
CHECK(has(ls[7], "\"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");
|
|
}
|