fuzzer and performance optimisations
This commit is contained in:
parent
d5ea2e2974
commit
a4650fd589
11 changed files with 897 additions and 146 deletions
39
tests/fuzz_config.cpp
Normal file
39
tests/fuzz_config.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// fuzz_config.cpp - libFuzzer harness for the .rc parser.
|
||||
//
|
||||
// Built and driven by tools/fuzz.py with clang++ -fsanitize=fuzzer. The
|
||||
// parser is exercised on raw bytes through the same `parse_rc_stream` path
|
||||
// used by the real init. `@import` is rejected inside the harness (the
|
||||
// opener throws) so fuzz input can never open real files such as /dev/zero.
|
||||
#include "../src/config.cpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct NoImports {
|
||||
std::unique_ptr<std::istream> operator()(const std::string&) const {
|
||||
throw std::runtime_error("fuzz: @import disabled");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
const std::string text(reinterpret_cast<const char*>(data), size);
|
||||
bajia::Config cfg;
|
||||
std::vector<std::string> chain;
|
||||
std::vector<std::string> seen;
|
||||
try {
|
||||
std::istringstream in(text);
|
||||
bajia::parse_rc_stream(cfg, in, "/fuzz/main.rc", chain, seen,
|
||||
NoImports{});
|
||||
} catch (const std::exception&) {
|
||||
// every parse error is a valid outcome; crashes are the bugs.
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
277
tests/unit_tests.cpp
Normal file
277
tests/unit_tests.cpp
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
// unit_tests.cpp - host-side unit tests for the bajia parser and pure helpers.
|
||||
#include "../src/config.cpp"
|
||||
#include "../src/logger.cpp"
|
||||
#include "../src/supervisor.cpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace bajia;
|
||||
|
||||
namespace {
|
||||
|
||||
int g_checks = 0;
|
||||
int g_failures = 0;
|
||||
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
++g_checks; \
|
||||
if (!(cond)) { \
|
||||
++g_failures; \
|
||||
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_EQ(a, b) \
|
||||
do { \
|
||||
++g_checks; \
|
||||
const auto va = (a); \
|
||||
const auto vb = (b); \
|
||||
if (!(va == vb)) { \
|
||||
++g_failures; \
|
||||
std::printf("FAIL %s:%d: %s != %s\n", __FILE__, __LINE__, #a, \
|
||||
#b); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_THROWS(expr, substr) \
|
||||
do { \
|
||||
++g_checks; \
|
||||
bool caught = false; \
|
||||
try { \
|
||||
expr; \
|
||||
} catch (const std::exception& e) { \
|
||||
caught = true; \
|
||||
if (std::string(e.what()).find(substr) == std::string::npos) { \
|
||||
++g_failures; \
|
||||
std::printf("FAIL %s:%d: threw wrong error: %s\n", __FILE__, \
|
||||
__LINE__, e.what()); \
|
||||
} \
|
||||
} \
|
||||
if (!caught) { \
|
||||
++g_failures; \
|
||||
std::printf("FAIL %s:%d: did not throw: %s\n", __FILE__, __LINE__, \
|
||||
#expr); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// In-memory replacement for the production opener: imports resolve against a
|
||||
// logical file tree instead of the real filesystem.
|
||||
struct VirtualFS {
|
||||
std::map<std::string, std::string> files;
|
||||
|
||||
std::unique_ptr<std::istream> operator()(const std::string& path) const {
|
||||
auto it = files.find(path);
|
||||
if (it == files.end())
|
||||
return nullptr;
|
||||
return std::make_unique<std::istringstream>(it->second);
|
||||
}
|
||||
};
|
||||
|
||||
Config parse_string(const std::string& text, const VirtualFS& fs,
|
||||
const std::string& name = "/t/main.rc") {
|
||||
Config cfg;
|
||||
std::vector<std::string> chain, seen;
|
||||
std::istringstream in(text);
|
||||
parse_rc_stream(cfg, in, name, chain, seen, fs);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void test_tokenize() {
|
||||
CHECK(tokenize("").empty());
|
||||
CHECK(tokenize(" \t ").empty());
|
||||
CHECK(tokenize("# only a comment").empty());
|
||||
CHECK(tokenize("; semicolon comment").empty());
|
||||
CHECK_EQ(tokenize("a b c").size(), 3u);
|
||||
CHECK_EQ(tokenize("a \t b").size(), 2u);
|
||||
CHECK_EQ(tokenize("service foo /bin/true ").size(), 3u);
|
||||
CHECK_EQ(tokenize("a # rest is a comment").size(), 1u);
|
||||
CHECK_EQ(tokenize("a ; rest is a comment").size(), 1u);
|
||||
// quoted strings survive as one token
|
||||
CHECK_EQ(tokenize("write /x \"hello world\"").size(), 3u);
|
||||
// escaped characters inside quotes
|
||||
const auto t = tokenize("\"a\\\"b\" c");
|
||||
CHECK_EQ(t.size(), 2u);
|
||||
CHECK_EQ(t[0], std::string("a\"b"));
|
||||
// unterminated quote: best-effort keeps what we have
|
||||
CHECK_EQ(tokenize("\"unterminated").size(), 1u);
|
||||
}
|
||||
|
||||
void test_parse_respawn() {
|
||||
CHECK(parse_respawn("always") == RespawnPolicy::Always);
|
||||
CHECK(parse_respawn("on-failure") == RespawnPolicy::OnFailure);
|
||||
CHECK(parse_respawn("never") == RespawnPolicy::Never);
|
||||
CHECK(parse_respawn("bogus") == RespawnPolicy::Never);
|
||||
}
|
||||
|
||||
void test_resolve_import_path() {
|
||||
CHECK_EQ(resolve_import_path("/a/b/main.rc", "sub/x.rc"),
|
||||
std::string("/a/b/sub/x.rc"));
|
||||
CHECK_EQ(resolve_import_path("/a/b/main.rc", "/abs/y.rc"),
|
||||
std::string("/abs/y.rc"));
|
||||
CHECK_EQ(resolve_import_path("main.rc", "x.rc"), std::string("x.rc"));
|
||||
CHECK_EQ(resolve_import_path("/a/main.rc", ""), std::string(""));
|
||||
}
|
||||
|
||||
void test_parse_basic() {
|
||||
VirtualFS fs;
|
||||
auto cfg = parse_string(
|
||||
"service sshd /usr/sbin/sshd\n"
|
||||
" user = root\n"
|
||||
" respawn = on-failure\n"
|
||||
" class = main\n"
|
||||
" setenv = FOO=bar\n"
|
||||
" oneshot\n"
|
||||
" disabled\n"
|
||||
" console\n"
|
||||
"on early-init\n"
|
||||
" mkdir /dev/pts 0755\n"
|
||||
" log hello\n",
|
||||
fs);
|
||||
CHECK_EQ(cfg.services.size(), 1u);
|
||||
CHECK_EQ(cfg.actions.size(), 1u);
|
||||
const auto& svc = cfg.services[0];
|
||||
CHECK_EQ(svc.name, std::string("sshd"));
|
||||
CHECK_EQ(svc.args.size(), 1u);
|
||||
CHECK_EQ(svc.args[0], std::string("/usr/sbin/sshd"));
|
||||
CHECK_EQ(svc.uid, std::string("root"));
|
||||
CHECK_EQ(svc.service_class, std::string("main"));
|
||||
CHECK(svc.respawn == RespawnPolicy::OnFailure);
|
||||
CHECK(svc.oneshot);
|
||||
CHECK(svc.disabled);
|
||||
CHECK(svc.console);
|
||||
CHECK_EQ(svc.env.size(), 1u);
|
||||
const auto& act = cfg.actions[0];
|
||||
CHECK_EQ(act.trigger, std::string("early-init"));
|
||||
CHECK_EQ(act.commands.size(), 2u);
|
||||
CHECK(act.commands[0].kind == Command::Kind::Mkdir);
|
||||
CHECK(act.commands[1].kind == Command::Kind::Log);
|
||||
|
||||
// option values require `= value`
|
||||
CHECK_THROWS(parse_string("service s /bin/true\n respawn always\n", fs),
|
||||
"requires '= value'");
|
||||
// unknown option keys are silently ignored (by design)
|
||||
CHECK_EQ(parse_string("service s /bin/true\n color = red\n", fs).services.size(),
|
||||
1u);
|
||||
// service needs name + executable
|
||||
CHECK_THROWS(parse_string("service lonely\n", fs), "'service' requires");
|
||||
// action needs a trigger
|
||||
CHECK_THROWS(parse_string("on\n", fs), "'on' requires a trigger");
|
||||
// unknown command inside an action
|
||||
CHECK_THROWS(parse_string("on boot\n frobnicate /x\n", fs),
|
||||
"unknown action command");
|
||||
// unknown directive at column 0
|
||||
CHECK_THROWS(parse_string("BROKEN = yes\n", fs), "unexpected directive");
|
||||
}
|
||||
|
||||
void test_imports() {
|
||||
VirtualFS fs;
|
||||
fs.files["/t/lib/base.rc"] = "service base /bin/true\non boot\n log base\n";
|
||||
fs.files["/t/lib/net.rc"] = "service net /bin/true\n";
|
||||
|
||||
// relative + transitive imports resolve against the importing file's dir
|
||||
auto cfg = parse_string("@import lib/base.rc\n@import lib/net.rc\n", fs);
|
||||
CHECK_EQ(cfg.services.size(), 2u);
|
||||
CHECK_EQ(cfg.actions.size(), 1u);
|
||||
|
||||
// absolute import
|
||||
fs.files["/t/abs.rc"] = "service abs /bin/true\n";
|
||||
cfg = parse_string("@import /t/abs.rc\n", fs);
|
||||
CHECK_EQ(cfg.services.size(), 1u);
|
||||
|
||||
// diamond imports dedupe: the shared file is not replayed
|
||||
fs.files["/t/a.rc"] = "@import lib/base.rc\n";
|
||||
fs.files["/t/b.rc"] = "@import lib/base.rc\n";
|
||||
cfg = parse_string("@import a.rc\n@import b.rc\n", fs);
|
||||
CHECK_EQ(cfg.services.size(), 1u); // base defined once, not twice
|
||||
CHECK_EQ(cfg.actions.size(), 1u);
|
||||
|
||||
// missing import carries the full ancestry
|
||||
fs.files["/t/mid.rc"] = "@import /t/nope.rc\n";
|
||||
CHECK_THROWS(parse_string("@import mid.rc\n", fs),
|
||||
"cannot open config file: /t/nope.rc (imported from "
|
||||
"/t/main.rc -> /t/mid.rc)");
|
||||
|
||||
// nested errors get "(at file:line)" context chained upward
|
||||
fs.files["/t/bad.rc"] = "BROKEN = yes\n";
|
||||
CHECK_THROWS(parse_string("@import bad.rc\n", fs),
|
||||
"/t/bad.rc:1: unexpected directive 'BROKEN' (at /t/main.rc:1)");
|
||||
|
||||
// circular imports
|
||||
fs.files["/t/x.rc"] = "@import /t/y.rc\n";
|
||||
fs.files["/t/y.rc"] = "@import /t/x.rc\n";
|
||||
CHECK_THROWS(parse_string("@import /t/x.rc\n", fs), "circular import");
|
||||
fs.files["/t/self.rc"] = "@import self.rc\n";
|
||||
CHECK_THROWS(parse_string("@import self.rc\n", fs), "circular import");
|
||||
|
||||
// placement rules
|
||||
CHECK_THROWS(parse_string("on boot\n log x\n@import lib/base.rc\n", fs),
|
||||
"only valid at column 0 outside a section");
|
||||
CHECK_THROWS(parse_string("@import a b\n", fs), "exactly one path");
|
||||
CHECK_THROWS(parse_string(" @import lib/base.rc\n", fs),
|
||||
"only valid at column 0");
|
||||
}
|
||||
|
||||
void test_supervisor_helpers() {
|
||||
// numeric ids pass through; unknown names resolve to -1
|
||||
CHECK(resolve_user("0") == 0);
|
||||
CHECK(resolve_user("65534") == 65534u);
|
||||
CHECK(resolve_user("definitely-not-a-user") == static_cast<uid_t>(-1));
|
||||
CHECK(resolve_group("1") == 1);
|
||||
CHECK(resolve_group("garbage-group-name") == static_cast<gid_t>(-1));
|
||||
|
||||
// service_changed: equal copies are unchanged, each field flips it
|
||||
Service a;
|
||||
a.name = "x";
|
||||
a.args = {"/bin/true"};
|
||||
CHECK(!service_changed(a, a));
|
||||
Service b = a;
|
||||
b.args = {"/bin/false"};
|
||||
CHECK(service_changed(a, b));
|
||||
b = a;
|
||||
b.cwd = "/tmp";
|
||||
CHECK(service_changed(a, b));
|
||||
b = a;
|
||||
b.uid = "nobody";
|
||||
CHECK(service_changed(a, b));
|
||||
b = a;
|
||||
b.respawn = RespawnPolicy::Never;
|
||||
CHECK(service_changed(a, b));
|
||||
b = a;
|
||||
b.env = {"A=1"};
|
||||
CHECK(service_changed(a, b));
|
||||
b = a;
|
||||
b.service_class = "other";
|
||||
CHECK(service_changed(a, b));
|
||||
|
||||
// status_line renders running/stopped state + flags
|
||||
Service svc;
|
||||
svc.name = "web";
|
||||
svc.service_class = "default";
|
||||
CHECK_EQ(status_line(svc), std::string("web stopped (last exit 0) class default\n"));
|
||||
svc.running = true;
|
||||
svc.pid = 42;
|
||||
CHECK_EQ(status_line(svc), std::string("web running pid 42 class default\n"));
|
||||
svc.oneshot = true;
|
||||
svc.service_class = "tools";
|
||||
CHECK_EQ(status_line(svc),
|
||||
std::string("web running pid 42 oneshot class tools\n"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_tokenize();
|
||||
test_parse_respawn();
|
||||
test_resolve_import_path();
|
||||
test_parse_basic();
|
||||
test_imports();
|
||||
test_supervisor_helpers();
|
||||
|
||||
std::printf("%d checks, %d failures\n", g_checks, g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue