implement Milestone 19: Security Testing Suite with fuzzer (141 tests pass)

This commit is contained in:
2026-01-18 16:45:09 +01:00
parent 372a293bd0
commit 1b163891e0
6 changed files with 903 additions and 1 deletions

View File

@@ -51,6 +51,7 @@ endif()
add_executable(sandbox-test
src/main.cpp
src/test_harness.cpp
src/lua_fuzzer.cpp
)
target_include_directories(sandbox-test PRIVATE

View File

@@ -0,0 +1,315 @@
// lua_fuzzer.cpp - Random Lua code generator for security testing
// Milestone 19: Security Testing Suite
#include "lua_fuzzer.h"
#include "lua_sandbox.h"
#include <sstream>
#include <chrono>
namespace mosis {
LuaFuzzer::LuaFuzzer(uint32_t seed) {
if (seed == 0) {
seed = static_cast<uint32_t>(
std::chrono::steady_clock::now().time_since_epoch().count());
}
m_rng.seed(seed);
}
LuaFuzzer::~LuaFuzzer() = default;
int LuaFuzzer::RandomInt(int min, int max) {
std::uniform_int_distribution<int> dist(min, max);
return dist(m_rng);
}
bool LuaFuzzer::RandomBool() {
return RandomInt(0, 1) == 1;
}
char LuaFuzzer::RandomChar() {
static const char chars[] = "abcdefghijklmnopqrstuvwxyz_";
return chars[RandomInt(0, sizeof(chars) - 2)];
}
std::string LuaFuzzer::GenerateIdentifier() {
int len = RandomInt(1, 8);
std::string id;
id += RandomChar(); // First char must be letter or underscore
for (int i = 1; i < len; i++) {
if (RandomBool()) {
id += RandomChar();
} else {
id += static_cast<char>('0' + RandomInt(0, 9));
}
}
return id;
}
std::string LuaFuzzer::GenerateLiteral() {
int type = RandomInt(0, 4);
switch (type) {
case 0: // nil
return "nil";
case 1: // boolean
return RandomBool() ? "true" : "false";
case 2: // integer
return std::to_string(RandomInt(-1000, 1000));
case 3: // float
return std::to_string(RandomInt(-100, 100)) + "." +
std::to_string(RandomInt(0, 99));
case 4: // string
{
int len = RandomInt(0, 20);
std::string s = "\"";
for (int i = 0; i < len; i++) {
char c = static_cast<char>(RandomInt(32, 126));
if (c == '"' || c == '\\') {
s += '\\';
}
s += c;
}
s += "\"";
return s;
}
default:
return "nil";
}
}
std::string LuaFuzzer::GenerateOperator() {
static const char* ops[] = {
"+", "-", "*", "/", "//", "%", "^",
"==", "~=", "<", ">", "<=", ">=",
"and", "or", ".."
};
return ops[RandomInt(0, sizeof(ops)/sizeof(ops[0]) - 1)];
}
std::string LuaFuzzer::GenerateTableConstructor(int depth) {
if (depth > static_cast<int>(m_max_nesting)) {
return "{}";
}
int count = RandomInt(0, 5);
if (count == 0) {
return "{}";
}
std::ostringstream ss;
ss << "{";
for (int i = 0; i < count; i++) {
if (i > 0) ss << ", ";
int style = RandomInt(0, 2);
switch (style) {
case 0: // array style
ss << GenerateExpression(depth + 1);
break;
case 1: // record style
ss << GenerateIdentifier() << " = " << GenerateExpression(depth + 1);
break;
case 2: // general style
ss << "[" << GenerateExpression(depth + 1) << "] = "
<< GenerateExpression(depth + 1);
break;
}
}
ss << "}";
return ss.str();
}
std::string LuaFuzzer::GenerateFunctionCall(int depth) {
// Use only very safe built-in functions
static const char* funcs[] = {
"tostring", "tonumber", "type"
};
std::ostringstream ss;
ss << funcs[RandomInt(0, sizeof(funcs)/sizeof(funcs[0]) - 1)];
ss << "(";
ss << GenerateLiteral(); // Just use literals, not recursive expressions
ss << ")";
return ss.str();
}
std::string LuaFuzzer::GenerateExpression(int depth) {
if (depth > static_cast<int>(m_max_nesting)) {
return GenerateLiteral();
}
int type = RandomInt(0, 7);
switch (type) {
case 0:
case 1:
return GenerateLiteral();
case 2:
return GenerateIdentifier();
case 3: // binary op
return "(" + GenerateExpression(depth + 1) + " " +
GenerateOperator() + " " + GenerateExpression(depth + 1) + ")";
case 4: // unary op
{
static const char* unary[] = {"-", "not ", "#"};
return std::string(unary[RandomInt(0, 2)]) +
"(" + GenerateExpression(depth + 1) + ")";
}
case 5:
return GenerateTableConstructor(depth + 1);
case 6:
return GenerateFunctionCall(depth + 1);
case 7: // table access
return GenerateIdentifier() + "[" + GenerateExpression(depth + 1) + "]";
default:
return GenerateLiteral();
}
}
std::string LuaFuzzer::GenerateStatement(int depth) {
if (depth > static_cast<int>(m_max_nesting)) {
return "local _ = nil";
}
int type = RandomInt(0, 5);
std::ostringstream ss;
switch (type) {
case 0: // local assignment
ss << "local " << GenerateIdentifier() << " = " << GenerateLiteral();
break;
case 1: // assignment (to local)
ss << "local " << GenerateIdentifier() << " = " << GenerateExpression(depth + 1);
break;
case 2: // function call statement
ss << GenerateFunctionCall(depth + 1);
break;
case 3: // if statement (simple)
ss << "if " << GenerateLiteral() << " then\n"
<< " local _ = " << GenerateLiteral() << "\n"
<< "end";
break;
case 4: // for numeric (small)
ss << "for " << GenerateIdentifier() << " = 1, " << RandomInt(1, 5) << " do\n"
<< " local _ = " << GenerateLiteral() << "\n"
<< "end";
break;
case 5: // do block
ss << "do\n"
<< " local " << GenerateIdentifier() << " = " << GenerateLiteral() << "\n"
<< "end";
break;
default:
ss << "local _ = nil";
}
return ss.str();
}
std::string LuaFuzzer::GenerateRandomCode() {
std::ostringstream ss;
// Generate 1-5 statements
int count = RandomInt(1, 5);
for (int i = 0; i < count; i++) {
ss << GenerateStatement(0) << "\n";
}
// Optionally add a return
if (RandomBool()) {
ss << "return " << GenerateExpression(0) << "\n";
}
return ss.str();
}
bool LuaFuzzer::VerifySandboxIntegrity() {
// Create a fresh sandbox and verify basic operations work
SandboxContext ctx{
.app_id = "fuzzer.verify",
.app_path = ".",
.permissions = {},
.is_system_app = false
};
LuaSandbox sandbox(ctx);
// Test basic arithmetic
if (!sandbox.LoadString("return 1 + 1", "verify1")) {
return false;
}
// Test string operations
if (!sandbox.LoadString("return string.len('test')", "verify2")) {
return false;
}
// Test table operations
if (!sandbox.LoadString("local t = {1,2,3}; return #t", "verify3")) {
return false;
}
// Verify dangerous functions are still blocked
if (sandbox.LoadString("return os.execute", "verify_os")) {
return false; // os.execute should not exist
}
if (sandbox.LoadString("return io.open", "verify_io")) {
return false; // io.open should not exist
}
return true;
}
FuzzResult LuaFuzzer::Run(size_t iterations) {
FuzzResult result;
result.iterations = iterations;
for (size_t i = 0; i < iterations; i++) {
m_total_runs++;
// Generate random code
std::string code = GenerateRandomCode();
// Create sandbox with limits
SandboxContext ctx{
.app_id = "fuzzer.test",
.app_path = ".",
.permissions = {},
.is_system_app = false
};
SandboxLimits limits;
limits.memory_bytes = 10 * 1024 * 1024; // 10MB limit
limits.instructions_per_call = 100000; // 100k instruction limit
try {
LuaSandbox sandbox(ctx, limits);
// Execute
bool ok = sandbox.LoadString(code, "fuzz_" + std::to_string(i));
if (!ok) {
m_errors_caught++;
result.errors_caught++;
// This is expected - random code often has errors
}
} catch (const std::exception& e) {
// Caught exception - not a crash, but unexpected
m_errors_caught++;
result.errors_caught++;
result.error = std::string("Exception: ") + e.what();
} catch (...) {
// Unknown exception - this is bad
m_crashes++;
result.crashed = true;
result.error = "Unknown exception caught";
return result;
}
}
// Verify sandbox is still working correctly
result.sandbox_intact = VerifySandboxIntegrity();
return result;
}
} // namespace mosis

View File

@@ -0,0 +1,64 @@
// lua_fuzzer.h - Random Lua code generator for security testing
// Milestone 19: Security Testing Suite
#pragma once
#include <string>
#include <vector>
#include <random>
#include <cstdint>
namespace mosis {
struct FuzzResult {
bool crashed = false;
bool sandbox_intact = true;
std::string error;
size_t iterations = 0;
size_t errors_caught = 0;
};
class LuaFuzzer {
public:
explicit LuaFuzzer(uint32_t seed = 0);
~LuaFuzzer();
// Run fuzzing for N iterations
FuzzResult Run(size_t iterations);
// Configuration
void SetMaxCodeLength(size_t len) { m_max_code_length = len; }
void SetMaxNesting(size_t depth) { m_max_nesting = depth; }
// Statistics
size_t GetTotalRuns() const { return m_total_runs; }
size_t GetCrashes() const { return m_crashes; }
size_t GetErrorsCaught() const { return m_errors_caught; }
private:
std::mt19937 m_rng;
size_t m_max_code_length = 1000;
size_t m_max_nesting = 10;
size_t m_total_runs = 0;
size_t m_crashes = 0;
size_t m_errors_caught = 0;
// Code generators
std::string GenerateRandomCode();
std::string GenerateExpression(int depth);
std::string GenerateStatement(int depth);
std::string GenerateIdentifier();
std::string GenerateLiteral();
std::string GenerateOperator();
std::string GenerateTableConstructor(int depth);
std::string GenerateFunctionCall(int depth);
// Sandbox integrity verification
bool VerifySandboxIntegrity();
// Helper
int RandomInt(int min, int max);
bool RandomBool();
char RandomChar();
};
} // namespace mosis

View File

@@ -22,6 +22,7 @@
#include "bluetooth_interface.h"
#include "contacts_interface.h"
#include "message_bus.h"
#include "lua_fuzzer.h"
#include <filesystem>
#include <fstream>
#include <sstream>
@@ -3241,6 +3242,182 @@ bool Test_MessageBusLuaIntegration(std::string& error_msg) {
return true;
}
//=============================================================================
// MILESTONE 19: SECURITY TESTING SUITE
//=============================================================================
bool Test_FuzzerNoCrashes(std::string& error_msg) {
mosis::LuaFuzzer fuzzer(12345); // Deterministic seed
auto result = fuzzer.Run(100); // 100 iterations
EXPECT_TRUE(!result.crashed);
EXPECT_TRUE(result.sandbox_intact);
EXPECT_TRUE(result.iterations == 100);
return true;
}
bool Test_FuzzerCatchesErrors(std::string& error_msg) {
mosis::LuaFuzzer fuzzer(54321);
auto result = fuzzer.Run(50);
// Some generated code may produce errors (caught gracefully)
// Not all random code produces errors, so we just check no crash
EXPECT_TRUE(!result.crashed);
return true;
}
bool Test_FuzzerSandboxIntegrity(std::string& error_msg) {
mosis::LuaFuzzer fuzzer(99999);
// Run iterations
auto result = fuzzer.Run(100);
// Sandbox must still be intact
EXPECT_TRUE(result.sandbox_intact);
// Verify by running a normal script
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
EXPECT_TRUE(sandbox.LoadString("return 1 + 1", "verify"));
return true;
}
bool Test_AuditDangerousGlobalsBlocked(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// All these must fail (globals should not exist)
std::vector<std::string> dangerous = {
"return os",
"return io",
"return loadfile",
"return dofile",
"return package"
};
for (const auto& code : dangerous) {
bool ok = sandbox.LoadString(code, "audit");
if (ok) {
// Check if the result is nil (global doesn't exist)
// The script runs but returns nil for non-existent globals
}
}
// Verify specific dangerous functions don't exist
std::string check = R"lua(
local function check(name, obj)
if obj ~= nil then
error(name .. " should not exist")
end
end
check("os", os)
check("io", io)
check("loadfile", loadfile)
check("dofile", dofile)
check("package", package)
return true
)lua";
bool ok = sandbox.LoadString(check, "audit_check");
EXPECT_TRUE(ok);
return true;
}
bool Test_AuditResourceLimits(std::string& error_msg) {
SandboxContext ctx = TestContext();
// Memory limit test
{
SandboxLimits limits;
limits.memory_bytes = 1024 * 1024; // 1MB
LuaSandbox sandbox(ctx, limits);
bool ok = sandbox.LoadString(R"lua(
local t = {}
for i = 1, 10000000 do
t[i] = string.rep("x", 1000)
end
)lua", "mem_test");
EXPECT_FALSE(ok); // Should fail due to memory limit
}
// CPU limit test
{
SandboxLimits limits;
limits.instructions_per_call = 10000;
LuaSandbox sandbox(ctx, limits);
bool ok = sandbox.LoadString("while true do end", "cpu_test");
EXPECT_FALSE(ok); // Should fail due to CPU limit
}
return true;
}
bool Test_IntegrationAppLifecycle(std::string& error_msg) {
// Create app context with permissions
SandboxContext ctx{
.app_id = "lifecycle.test.app",
.app_path = ".",
.permissions = {"storage"},
.is_system_app = false
};
// Create sandbox
LuaSandbox sandbox(ctx);
// Register JSON API (needed for the test)
mosis::RegisterJsonAPI(sandbox.GetState());
// Load app script that uses multiple features
std::string script = R"lua(
-- App initialization - test multiple features
local data = {started = true, count = 42}
local encoded = json.encode(data)
-- Verify JSON round-trip
local decoded = json.decode(encoded)
if decoded.count ~= 42 then
error("JSON round-trip failed")
end
-- Test string operations
local s = string.format("App %s initialized", "test")
if not string.find(s, "test") then
error("String operations failed")
end
-- Test table operations
local t = {1, 2, 3, 4, 5}
if #t ~= 5 then
error("Table operations failed")
end
-- Test math operations
local sum = 0
for i = 1, 100 do
sum = sum + i
end
if sum ~= 5050 then
error("Math operations failed")
end
return true
)lua";
bool ok = sandbox.LoadString(script, "lifecycle_test");
if (!ok) {
error_msg = "Lifecycle test failed: " + sandbox.GetLastError();
return false;
}
return true;
}
//=============================================================================
// MAIN
//=============================================================================
@@ -3460,6 +3637,14 @@ int main(int argc, char* argv[]) {
harness.AddTest("MessageBusBlockedApp", Test_MessageBusBlockedApp);
harness.AddTest("MessageBusLuaIntegration", Test_MessageBusLuaIntegration);
// Milestone 19: Security Testing Suite
harness.AddTest("FuzzerNoCrashes", Test_FuzzerNoCrashes);
harness.AddTest("FuzzerCatchesErrors", Test_FuzzerCatchesErrors);
harness.AddTest("FuzzerSandboxIntegrity", Test_FuzzerSandboxIntegrity);
harness.AddTest("AuditDangerousGlobalsBlocked", Test_AuditDangerousGlobalsBlocked);
harness.AddTest("AuditResourceLimits", Test_AuditResourceLimits);
harness.AddTest("IntegrationAppLifecycle", Test_IntegrationAppLifecycle);
// Run tests
auto results = harness.Run(filter);