add Lua sandbox with timer system (milestones 1-5 complete)

This commit is contained in:
2026-01-18 14:28:44 +01:00
parent 2c36ac005d
commit a4ecb0f132
36 changed files with 10884 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
#include "audit_log.h"
#include <algorithm>
namespace mosis {
//=============================================================================
// CONSTRUCTOR
//=============================================================================
AuditLog::AuditLog(size_t max_entries)
: m_max_entries(max_entries)
{
m_entries.resize(max_entries);
}
//=============================================================================
// LOGGING
//=============================================================================
void AuditLog::Log(AuditEvent event, const std::string& app_id,
const std::string& details, bool success) {
std::lock_guard<std::mutex> lock(m_mutex);
AuditEntry entry{
.timestamp = std::chrono::system_clock::now(),
.event = event,
.app_id = app_id,
.details = details,
.success = success
};
m_entries[m_write_index] = std::move(entry);
m_write_index = (m_write_index + 1) % m_max_entries;
m_total_logged++;
if (m_total_logged > m_max_entries) {
m_wrapped = true;
}
}
//=============================================================================
// QUERIES
//=============================================================================
std::vector<AuditEntry> AuditLog::GetEntries(size_t count) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<AuditEntry> result;
size_t stored = GetStoredEntries();
count = std::min(count, stored);
result.reserve(count);
// Read from most recent backwards
for (size_t i = 0; i < count; i++) {
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
result.push_back(m_entries[idx]);
}
return result;
}
std::vector<AuditEntry> AuditLog::GetEntriesForApp(const std::string& app_id,
size_t count) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<AuditEntry> result;
result.reserve(count);
size_t stored = GetStoredEntries();
for (size_t i = 0; i < stored && result.size() < count; i++) {
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
if (m_entries[idx].app_id == app_id) {
result.push_back(m_entries[idx]);
}
}
return result;
}
std::vector<AuditEntry> AuditLog::GetEntriesByEvent(AuditEvent event,
size_t count) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<AuditEntry> result;
result.reserve(count);
size_t stored = GetStoredEntries();
for (size_t i = 0; i < stored && result.size() < count; i++) {
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
if (m_entries[idx].event == event) {
result.push_back(m_entries[idx]);
}
}
return result;
}
//=============================================================================
// STATISTICS
//=============================================================================
size_t AuditLog::GetTotalEntries() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_total_logged;
}
size_t AuditLog::GetStoredEntries() const {
// Note: caller should hold lock
if (m_wrapped) {
return m_max_entries;
}
return m_write_index;
}
size_t AuditLog::CountEvents(AuditEvent event, const std::string& app_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
size_t count = 0;
size_t stored = GetStoredEntries();
for (size_t i = 0; i < stored; i++) {
const auto& entry = m_entries[i];
if (entry.event == event) {
if (app_id.empty() || entry.app_id == app_id) {
count++;
}
}
}
return count;
}
//=============================================================================
// CLEAR
//=============================================================================
void AuditLog::Clear() {
std::lock_guard<std::mutex> lock(m_mutex);
m_write_index = 0;
m_total_logged = 0;
m_wrapped = false;
// Clear all entries
for (auto& entry : m_entries) {
entry = AuditEntry{};
}
}
//=============================================================================
// UTILITIES
//=============================================================================
const char* AuditLog::EventToString(AuditEvent event) {
switch (event) {
case AuditEvent::AppStart: return "AppStart";
case AuditEvent::AppStop: return "AppStop";
case AuditEvent::PermissionCheck: return "PermissionCheck";
case AuditEvent::PermissionGranted: return "PermissionGranted";
case AuditEvent::PermissionDenied: return "PermissionDenied";
case AuditEvent::NetworkRequest: return "NetworkRequest";
case AuditEvent::NetworkBlocked: return "NetworkBlocked";
case AuditEvent::FileAccess: return "FileAccess";
case AuditEvent::FileBlocked: return "FileBlocked";
case AuditEvent::DatabaseAccess: return "DatabaseAccess";
case AuditEvent::CameraAccess: return "CameraAccess";
case AuditEvent::MicrophoneAccess: return "MicrophoneAccess";
case AuditEvent::LocationAccess: return "LocationAccess";
case AuditEvent::SandboxViolation: return "SandboxViolation";
case AuditEvent::ResourceLimitHit: return "ResourceLimitHit";
case AuditEvent::RateLimitHit: return "RateLimitHit";
case AuditEvent::Custom: return "Custom";
default: return "Unknown";
}
}
//=============================================================================
// GLOBAL INSTANCE
//=============================================================================
AuditLog& GetAuditLog() {
static AuditLog instance(10000);
return instance;
}
} // namespace mosis

View File

@@ -0,0 +1,94 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <chrono>
namespace mosis {
enum class AuditEvent {
// Lifecycle
AppStart,
AppStop,
// Permissions
PermissionCheck,
PermissionGranted,
PermissionDenied,
// Network
NetworkRequest,
NetworkBlocked,
// Storage
FileAccess,
FileBlocked,
DatabaseAccess,
// Hardware
CameraAccess,
MicrophoneAccess,
LocationAccess,
// Security
SandboxViolation,
ResourceLimitHit,
RateLimitHit,
// Other
Custom
};
struct AuditEntry {
std::chrono::system_clock::time_point timestamp;
AuditEvent event;
std::string app_id;
std::string details;
bool success;
};
class AuditLog {
public:
explicit AuditLog(size_t max_entries = 10000);
// Log an event
void Log(AuditEvent event, const std::string& app_id,
const std::string& details = "", bool success = true);
// Query entries (returns most recent first)
std::vector<AuditEntry> GetEntries(size_t count = 100) const;
std::vector<AuditEntry> GetEntriesForApp(const std::string& app_id,
size_t count = 100) const;
std::vector<AuditEntry> GetEntriesByEvent(AuditEvent event,
size_t count = 100) const;
// Statistics
size_t GetTotalEntries() const;
size_t GetStoredEntries() const;
size_t CountEvents(AuditEvent event, const std::string& app_id = "") const;
// Clear all entries
void Clear();
// Convert event to string for logging
static const char* EventToString(AuditEvent event);
private:
mutable std::mutex m_mutex;
std::vector<AuditEntry> m_entries;
size_t m_max_entries;
size_t m_write_index = 0;
size_t m_total_logged = 0;
bool m_wrapped = false;
};
// Global audit log (singleton)
AuditLog& GetAuditLog();
} // namespace mosis
// Convenience alias
using AuditLog = mosis::AuditLog;
using AuditEvent = mosis::AuditEvent;
using AuditEntry = mosis::AuditEntry;

View File

@@ -0,0 +1,448 @@
#include "lua_sandbox.h"
#include <lua.hpp>
#include <fstream>
#include <sstream>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cctype>
namespace mosis {
//=============================================================================
// ALLOCATOR
//=============================================================================
void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
auto* sandbox = static_cast<LuaSandbox*>(ud);
// Calculate new usage
// osize is the old size (0 for new allocations)
// nsize is the new size (0 for frees)
size_t new_usage = sandbox->m_memory_used - osize + nsize;
// Check limit (only when allocating, not freeing)
if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) {
// Allocation would exceed limit - return nullptr to signal failure
// Lua will raise a memory error
return nullptr;
}
// Update tracking
sandbox->m_memory_used = new_usage;
// Free operation
if (nsize == 0) {
free(ptr);
return nullptr;
}
// Alloc or realloc
return realloc(ptr, nsize);
}
//=============================================================================
// INSTRUCTION HOOK
//=============================================================================
void LuaSandbox::InstructionHook(lua_State* L, lua_Debug* ar) {
(void)ar; // Unused
// Get sandbox pointer from registry
lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox");
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
if (!sandbox) return;
// Increment by hook interval (called every 1000 instructions)
sandbox->m_instructions_used += 1000;
// Check limit
if (sandbox->m_instructions_used > sandbox->m_limits.instructions_per_call) {
luaL_error(L, "instruction limit exceeded");
}
}
//=============================================================================
// SAFE PRINT
//=============================================================================
int LuaSandbox::SafePrint(lua_State* L) {
int n = lua_gettop(L); // number of arguments
lua_getglobal(L, "tostring");
for (int i = 1; i <= n; i++) {
if (i > 1) std::cout << "\t";
lua_pushvalue(L, -1); // push tostring
lua_pushvalue(L, i); // push argument
lua_call(L, 1, 1); // call tostring
const char* s = lua_tostring(L, -1);
if (s) {
std::cout << s;
}
lua_pop(L, 1);
}
std::cout << std::endl;
return 0;
}
//=============================================================================
// CONSTRUCTOR / DESTRUCTOR
//=============================================================================
LuaSandbox::LuaSandbox(const SandboxContext& context, const SandboxLimits& limits)
: m_context(context), m_limits(limits) {
// Create Lua state with custom allocator
m_L = lua_newstate(SandboxAlloc, this);
if (!m_L) {
m_last_error = "Failed to create Lua state";
return;
}
// Store sandbox pointer in registry for hooks to access
lua_pushlightuserdata(m_L, this);
lua_setfield(m_L, LUA_REGISTRYINDEX, "__mosis_sandbox");
// Setup the sandbox
SetupSandbox();
}
LuaSandbox::~LuaSandbox() {
if (m_L) {
lua_close(m_L);
m_L = nullptr;
}
}
//=============================================================================
// SETUP
//=============================================================================
void LuaSandbox::SetupSandbox() {
// Open safe standard libraries
luaL_openlibs(m_L);
// Remove dangerous globals FIRST
RemoveDangerousGlobals();
// Setup safe replacements
SetupSafeGlobals();
// Protect metatables
ProtectBuiltinTables();
// Setup instruction hook for CPU limiting
SetupInstructionHook();
}
void LuaSandbox::RemoveDangerousGlobals() {
// List of dangerous globals to remove
const char* dangerous_globals[] = {
// Code execution from files/strings
"dofile",
"loadfile",
"load",
"loadstring", // Lua 5.1 compat
// Raw access (bypasses metatables)
"rawget",
"rawset",
"rawequal",
"rawlen",
// Metatable manipulation
// Note: We keep getmetatable but protect the actual metatables
// setmetatable is removed to prevent modifications
"setmetatable",
// GC manipulation
"collectgarbage",
// Dangerous libraries
"os",
"io",
"debug",
"package",
// LuaJIT / FFI (if present)
"ffi",
"jit",
"newproxy",
// Module system (we'll add safe version later)
"require",
nullptr
};
for (const char** p = dangerous_globals; *p; ++p) {
lua_pushnil(m_L);
lua_setglobal(m_L, *p);
}
// Remove string.dump (can create bytecode from functions)
lua_getglobal(m_L, "string");
if (lua_istable(m_L, -1)) {
lua_pushnil(m_L);
lua_setfield(m_L, -2, "dump");
}
lua_pop(m_L, 1);
}
void LuaSandbox::SetupSafeGlobals() {
// Replace print with safe version
lua_pushcfunction(m_L, SafePrint);
lua_setglobal(m_L, "print");
// Setup safe require if app_path is set
if (!m_context.app_path.empty()) {
SetupSafeRequire();
}
}
//=============================================================================
// SAFE REQUIRE
//=============================================================================
// Registry key for loaded modules cache
static const char* LOADED_KEY = "mosis.loaded_modules";
// Validate module name for require() - alphanumeric, underscore, dots only
static bool IsValidModuleName(const std::string& name) {
if (name.empty()) return false;
for (size_t i = 0; i < name.length(); i++) {
char c = name[i];
if (std::isalnum(static_cast<unsigned char>(c))) continue;
if (c == '_') continue;
if (c == '.') {
if (i == 0 || i == name.length() - 1) return false;
if (i > 0 && name[i-1] == '.') return false;
continue;
}
return false;
}
if (name.find("..") != std::string::npos) return false;
return true;
}
// Convert module name to path (e.g., "ui.button" -> "scripts/ui/button.lua")
static std::string ModuleToPath(const std::string& module_name) {
std::string path = module_name;
std::replace(path.begin(), path.end(), '.', '/');
return "scripts/" + path + ".lua";
}
int LuaSandbox::SafeRequire(lua_State* L) {
// Get module name
const char* module_name = luaL_checkstring(L, 1);
// Validate module name
if (!IsValidModuleName(module_name)) {
return luaL_error(L, "invalid module name: %s", module_name);
}
// Check cache first
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, module_name);
if (!lua_isnil(L, -1)) {
return 1; // Return cached module
}
lua_pop(L, 1);
}
lua_pop(L, 1);
// Get sandbox pointer from registry
lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox");
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
if (!sandbox) {
return luaL_error(L, "require not properly initialized");
}
// Build full path
std::string relative_path = ModuleToPath(module_name);
std::string full_path = sandbox->m_context.app_path;
if (!full_path.empty() && full_path.back() != '/' && full_path.back() != '\\') {
full_path += '/';
}
full_path += relative_path;
// Read the file
std::ifstream file(full_path);
if (!file.is_open()) {
return luaL_error(L, "module '%s' not found at '%s'", module_name, full_path.c_str());
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string source = buffer.str();
file.close();
// Load as text only (no bytecode)
std::string chunk_name = "@" + std::string(module_name);
int status = luaL_loadbufferx(L, source.c_str(), source.size(),
chunk_name.c_str(), "t");
if (status != LUA_OK) {
return lua_error(L);
}
// Execute the chunk
lua_call(L, 0, 1);
// If chunk returned nil, use true as the module value
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushboolean(L, 1);
}
// Cache the result
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
}
lua_pushvalue(L, -2);
lua_setfield(L, -2, module_name);
lua_pop(L, 1);
return 1;
}
void LuaSandbox::SetupSafeRequire() {
// Create loaded modules cache
lua_newtable(m_L);
lua_setfield(m_L, LUA_REGISTRYINDEX, LOADED_KEY);
// Register require function
lua_pushcfunction(m_L, SafeRequire);
lua_setglobal(m_L, "require");
}
void LuaSandbox::ProtectBuiltinTables() {
// Protect string metatable
// When someone calls getmetatable(""), they get "string" instead of the real metatable
lua_pushstring(m_L, "");
if (lua_getmetatable(m_L, -1)) {
lua_pushstring(m_L, "string");
lua_setfield(m_L, -2, "__metatable");
lua_pop(m_L, 1); // pop metatable
}
lua_pop(m_L, 1); // pop string
// Freeze _G using a proxy pattern
// This is needed because __newindex only fires for NEW keys, not existing ones
// We create: empty_proxy -> metatable { __index = real_G, __newindex = error }
// Get the current _G (with all our safe functions)
lua_pushglobaltable(m_L); // stack: real_G
// Create a new empty table to be the proxy
lua_newtable(m_L); // stack: real_G, proxy
// Create metatable for proxy
lua_newtable(m_L); // stack: real_G, proxy, mt
// __metatable - prevent access to real metatable
lua_pushstring(m_L, "globals");
lua_setfield(m_L, -2, "__metatable");
// __index - read from real_G
lua_pushvalue(m_L, -3); // push real_G
lua_setfield(m_L, -2, "__index");
// __newindex - block all writes
lua_pushcfunction(m_L, [](lua_State* L) -> int {
const char* key = lua_tostring(L, 2);
return luaL_error(L, "cannot modify global '%s'", key ? key : "(unknown)");
});
lua_setfield(m_L, -2, "__newindex");
// Set metatable on proxy: setmetatable(proxy, mt)
lua_setmetatable(m_L, -2); // stack: real_G, proxy
// Now we need to replace _G with proxy
// In Lua 5.2+, we use lua_rawseti on the registry
lua_pushvalue(m_L, -1); // stack: real_G, proxy, proxy
lua_rawseti(m_L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); // stack: real_G, proxy
// Also update _G variable in real_G to point to proxy
// This is critical: when code does _G.foo = bar, it accesses _G variable
lua_setfield(m_L, -2, "_G"); // stack: real_G (sets real_G["_G"] = proxy)
lua_pop(m_L, 1); // pop real_G
}
void LuaSandbox::SetupInstructionHook() {
// Set hook to fire every 1000 VM instructions
lua_sethook(m_L, InstructionHook, LUA_MASKCOUNT, 1000);
}
//=============================================================================
// LOAD AND EXECUTE
//=============================================================================
bool LuaSandbox::LoadString(const std::string& code, const std::string& chunk_name) {
if (!m_L) {
m_last_error = "Lua state not initialized";
return false;
}
// Reset instruction count for this execution
ResetInstructionCount();
// Load as TEXT ONLY - "t" mode rejects bytecode (starts with \x1bLua)
int result = luaL_loadbufferx(m_L, code.c_str(), code.size(),
chunk_name.c_str(), "t");
if (result != LUA_OK) {
m_last_error = lua_tostring(m_L, -1);
lua_pop(m_L, 1);
return false;
}
// Execute the loaded chunk
result = lua_pcall(m_L, 0, 0, 0);
if (result != LUA_OK) {
m_last_error = lua_tostring(m_L, -1);
lua_pop(m_L, 1);
return false;
}
m_last_error.clear();
return true;
}
bool LuaSandbox::LoadFile(const std::string& path) {
// Read file contents
std::ifstream f(path);
if (!f) {
m_last_error = "Cannot open file: " + path;
return false;
}
std::stringstream ss;
ss << f.rdbuf();
std::string code = ss.str();
// Load as string
return LoadString(code, "@" + path);
}
void LuaSandbox::ResetInstructionCount() {
m_instructions_used = 0;
}
} // namespace mosis

View File

@@ -0,0 +1,101 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
// Forward declare lua_State to avoid including lua.h in header
struct lua_State;
struct lua_Debug;
namespace mosis {
// Resource limits for sandbox
struct SandboxLimits {
size_t memory_bytes = 16 * 1024 * 1024; // 16 MB default
size_t max_string_size = 1 * 1024 * 1024; // 1 MB max string
size_t max_table_entries = 100000; // Prevent hash DoS
uint64_t instructions_per_call = 1000000; // ~10ms execution
int stack_depth = 200; // Recursion limit
};
// Context for sandbox (app identity, permissions, etc.)
struct SandboxContext {
std::string app_id;
std::string app_path;
std::vector<std::string> permissions;
bool is_system_app = false;
};
// Isolated Lua execution environment
class LuaSandbox {
public:
explicit LuaSandbox(const SandboxContext& context,
const SandboxLimits& limits = {});
~LuaSandbox();
// Non-copyable, non-movable
LuaSandbox(const LuaSandbox&) = delete;
LuaSandbox& operator=(const LuaSandbox&) = delete;
LuaSandbox(LuaSandbox&&) = delete;
LuaSandbox& operator=(LuaSandbox&&) = delete;
// Load and execute Lua code (text only, bytecode rejected)
bool LoadString(const std::string& code, const std::string& chunk_name = "chunk");
bool LoadFile(const std::string& path);
// State access
lua_State* GetState() const { return m_L; }
const std::string& GetLastError() const { return m_last_error; }
// Resource usage
size_t GetMemoryUsed() const { return m_memory_used; }
uint64_t GetInstructionsUsed() const { return m_instructions_used; }
// Context access
const SandboxContext& GetContext() const { return m_context; }
const SandboxLimits& GetLimits() const { return m_limits; }
const std::string& app_id() const { return m_context.app_id; }
// Reset instruction counter (call before each event handler)
void ResetInstructionCount();
// Check if sandbox is in valid state
bool IsValid() const { return m_L != nullptr; }
private:
// Setup functions
void SetupSandbox();
void RemoveDangerousGlobals();
void ProtectBuiltinTables();
void SetupInstructionHook();
void SetupSafeGlobals();
void SetupSafeRequire();
// Allocator callback (static for C compatibility)
static void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize);
// Instruction hook callback (static for C compatibility)
static void InstructionHook(lua_State* L, lua_Debug* ar);
// Safe print function
static int SafePrint(lua_State* L);
// Safe require function
static int SafeRequire(lua_State* L);
lua_State* m_L = nullptr;
SandboxContext m_context;
SandboxLimits m_limits;
size_t m_memory_used = 0;
uint64_t m_instructions_used = 0;
std::string m_last_error;
};
} // namespace mosis
// Convenience alias for tests
using SandboxContext = mosis::SandboxContext;
using SandboxLimits = mosis::SandboxLimits;
using LuaSandbox = mosis::LuaSandbox;

View File

@@ -0,0 +1,344 @@
#include "path_sandbox.h"
#include <lua.hpp>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream>
namespace mosis {
//=============================================================================
// CONSTRUCTOR
//=============================================================================
PathSandbox::PathSandbox(const std::string& app_path)
: m_app_path(app_path)
{
// Normalize the app path
if (!m_app_path.empty()) {
// Ensure trailing separator for prefix matching
if (m_app_path.back() != '/' && m_app_path.back() != '\\') {
m_app_path += '/';
}
// Normalize separators to forward slash
std::replace(m_app_path.begin(), m_app_path.end(), '\\', '/');
}
}
//=============================================================================
// PATH VALIDATION
//=============================================================================
bool PathSandbox::ContainsTraversal(const std::string& path) {
std::string normalized = NormalizePath(path);
// Check for .. anywhere in the path
size_t pos = 0;
while ((pos = normalized.find("..", pos)) != std::string::npos) {
// Make sure it's actually a parent directory reference, not part of a filename
bool at_start = (pos == 0);
bool before_is_sep = (pos > 0 && (normalized[pos-1] == '/' || normalized[pos-1] == '\\'));
size_t after_pos = pos + 2;
bool at_end = (after_pos >= normalized.size());
bool after_is_sep = (!at_end && (normalized[after_pos] == '/' || normalized[after_pos] == '\\'));
if ((at_start || before_is_sep) && (at_end || after_is_sep)) {
return true;
}
pos++;
}
return false;
}
bool PathSandbox::IsAbsolutePath(const std::string& path) {
if (path.empty()) return false;
// Unix absolute path
if (path[0] == '/') return true;
// Windows absolute path (C:\ or C:/)
if (path.length() >= 2) {
char first = path[0];
if (std::isalpha(static_cast<unsigned char>(first)) && path[1] == ':') {
return true;
}
}
// UNC path (\\server\share or //server/share)
if (path.length() >= 2) {
if ((path[0] == '\\' && path[1] == '\\') ||
(path[0] == '/' && path[1] == '/')) {
return true;
}
}
return false;
}
std::string PathSandbox::NormalizePath(const std::string& path) {
std::string result = path;
// Convert backslashes to forward slashes
std::replace(result.begin(), result.end(), '\\', '/');
// Remove leading ./
while (result.length() >= 2 && result[0] == '.' && result[1] == '/') {
result = result.substr(2);
}
// Remove duplicate slashes
std::string cleaned;
bool last_was_slash = false;
for (char c : result) {
if (c == '/') {
if (!last_was_slash) {
cleaned += c;
}
last_was_slash = true;
} else {
cleaned += c;
last_was_slash = false;
}
}
return cleaned;
}
bool PathSandbox::ValidatePath(const std::string& path, std::string& out_canonical) {
// Reject empty paths
if (path.empty()) {
return false;
}
// Reject absolute paths
if (IsAbsolutePath(path)) {
return false;
}
// Reject traversal attempts
if (ContainsTraversal(path)) {
return false;
}
// Normalize and resolve the path
std::string normalized = NormalizePath(path);
std::string resolved = ResolvePath(normalized);
// Use filesystem to get canonical path (resolves any remaining .)
try {
std::filesystem::path fs_path(resolved);
// If the file exists, use canonical path for strict checking
if (std::filesystem::exists(fs_path)) {
std::filesystem::path canonical = std::filesystem::canonical(fs_path);
std::string canonical_str = canonical.string();
std::replace(canonical_str.begin(), canonical_str.end(), '\\', '/');
// Verify the canonical path is still within app_path
std::string app_canonical = std::filesystem::canonical(
std::filesystem::path(m_app_path)).string();
std::replace(app_canonical.begin(), app_canonical.end(), '\\', '/');
if (!app_canonical.empty() && app_canonical.back() != '/') {
app_canonical += '/';
}
if (canonical_str.rfind(app_canonical, 0) != 0) {
return false; // Path escaped sandbox via symlink
}
out_canonical = canonical_str;
} else {
// File doesn't exist, just use the resolved path
out_canonical = resolved;
}
} catch (const std::filesystem::filesystem_error&) {
// Filesystem error, use the resolved path as-is
out_canonical = resolved;
}
return true;
}
std::string PathSandbox::ResolvePath(const std::string& relative_path) {
std::string normalized = NormalizePath(relative_path);
// Combine with app path
std::string result = m_app_path + normalized;
return result;
}
//=============================================================================
// MODULE NAME VALIDATION
//=============================================================================
bool PathSandbox::IsValidModuleName(const std::string& name) {
if (name.empty()) {
return false;
}
// Check each character
for (size_t i = 0; i < name.length(); i++) {
char c = name[i];
// Allow alphanumeric
if (std::isalnum(static_cast<unsigned char>(c))) {
continue;
}
// Allow underscore
if (c == '_') {
continue;
}
// Allow dot for submodules, but not at start/end or consecutive
if (c == '.') {
if (i == 0 || i == name.length() - 1) {
return false; // Dot at start or end
}
if (i > 0 && name[i-1] == '.') {
return false; // Consecutive dots
}
continue;
}
// Any other character is invalid
return false;
}
// Reject names that look like traversal
if (name.find("..") != std::string::npos) {
return false;
}
return true;
}
std::string PathSandbox::ModuleToPath(const std::string& module_name) {
// Convert dots to path separators
std::string path = module_name;
std::replace(path.begin(), path.end(), '.', '/');
// Add scripts/ prefix and .lua suffix
return "scripts/" + path + ".lua";
}
//=============================================================================
// SAFE REQUIRE
//=============================================================================
// Registry key for PathSandbox pointer
static const char* SANDBOX_KEY = "mosis.path_sandbox";
// Registry key for loaded modules cache
static const char* LOADED_KEY = "mosis.loaded_modules";
int SafeRequire(lua_State* L) {
// Get module name
const char* module_name = luaL_checkstring(L, 1);
// Validate module name
if (!PathSandbox::IsValidModuleName(module_name)) {
return luaL_error(L, "invalid module name: %s", module_name);
}
// Check cache first
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, module_name);
if (!lua_isnil(L, -1)) {
// Module already loaded, return cached value
return 1;
}
lua_pop(L, 1); // Pop nil
}
lua_pop(L, 1); // Pop cache table (or nil if not exists)
// Get PathSandbox from registry
lua_getfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "require not properly initialized");
}
PathSandbox* sandbox = static_cast<PathSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Convert module name to path
std::string relative_path = PathSandbox::ModuleToPath(module_name);
// Validate the path
std::string canonical;
if (!sandbox->ValidatePath(relative_path, canonical)) {
return luaL_error(L, "cannot load module '%s': path validation failed", module_name);
}
// Read the file
std::ifstream file(canonical);
if (!file.is_open()) {
// Try with the resolved path directly (in case canonical check failed)
std::string resolved = sandbox->ResolvePath(relative_path);
file.open(resolved);
if (!file.is_open()) {
return luaL_error(L, "module '%s' not found", module_name);
}
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string source = buffer.str();
file.close();
// Load as text only (no bytecode)
std::string chunk_name = "@" + std::string(module_name);
int status = luaL_loadbufferx(L, source.c_str(), source.size(),
chunk_name.c_str(), "t");
if (status != LUA_OK) {
return lua_error(L); // Propagate error
}
// Execute the chunk
lua_call(L, 0, 1);
// If chunk returned nil, use true as the module value
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushboolean(L, 1);
}
// Cache the result
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (!lua_istable(L, -1)) {
// Create cache table if it doesn't exist
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
}
// cache[module_name] = result
lua_pushvalue(L, -2); // Push the result
lua_setfield(L, -2, module_name);
lua_pop(L, 1); // Pop cache table
// Return the module
return 1;
}
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox) {
// Store PathSandbox pointer in registry
lua_pushlightuserdata(L, sandbox);
lua_setfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY);
// Create loaded modules cache
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
// Register require function
lua_pushcfunction(L, SafeRequire);
lua_setglobal(L, "require");
}
} // namespace mosis

View File

@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <filesystem>
struct lua_State;
namespace mosis {
class PathSandbox {
public:
explicit PathSandbox(const std::string& app_path);
// Validate a path is within the sandbox
// Returns true if valid, sets out_canonical to the resolved path
bool ValidatePath(const std::string& path, std::string& out_canonical);
// Check if path contains traversal attempts (..)
static bool ContainsTraversal(const std::string& path);
// Check if path is absolute
static bool IsAbsolutePath(const std::string& path);
// Normalize path separators and remove redundant ./ components
static std::string NormalizePath(const std::string& path);
// Validate module name for require() - alphanumeric, underscore, dots only
static bool IsValidModuleName(const std::string& name);
// Convert module name to relative path (e.g., "ui.button" -> "scripts/ui/button.lua")
static std::string ModuleToPath(const std::string& module_name);
// Get the app's base path
const std::string& GetAppPath() const { return m_app_path; }
// Resolve a relative path to full path within sandbox
std::string ResolvePath(const std::string& relative_path);
private:
std::string m_app_path;
};
// Safe require implementation for Lua
// Loads modules only from app_path/scripts/<module>.lua
// Caches modules in registry
int SafeRequire(lua_State* L);
// Register safe require as global "require"
// The PathSandbox pointer is stored in registry for use by SafeRequire
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox);
} // namespace mosis

View File

@@ -0,0 +1,197 @@
#include "permission_gate.h"
#include "lua_sandbox.h"
#include <lua.hpp>
#include <algorithm>
namespace mosis {
//=============================================================================
// PERMISSION DATABASE
//=============================================================================
static const std::unordered_map<std::string, PermissionInfo> PERMISSIONS = {
// Normal permissions (auto-granted when declared)
{"internet", {PermissionCategory::Normal, "Access the internet"}},
{"vibrate", {PermissionCategory::Normal, "Vibrate the device"}},
{"wake_lock", {PermissionCategory::Normal, "Keep device awake"}},
{"notifications", {PermissionCategory::Normal, "Show notifications"}},
{"alarms", {PermissionCategory::Normal, "Set alarms"}},
{"nfc", {PermissionCategory::Normal, "Use NFC"}},
// Dangerous permissions (require user consent)
{"camera", {PermissionCategory::Dangerous, "Access the camera"}},
{"microphone", {PermissionCategory::Dangerous, "Record audio"}},
{"location.fine", {PermissionCategory::Dangerous, "Access precise location"}},
{"location.coarse", {PermissionCategory::Dangerous, "Access approximate location"}},
{"contacts.read", {PermissionCategory::Dangerous, "Read contacts"}},
{"contacts.write", {PermissionCategory::Dangerous, "Modify contacts"}},
{"storage.external", {PermissionCategory::Dangerous, "Access external storage"}},
{"storage.shared", {PermissionCategory::Dangerous, "Access shared storage"}},
{"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}},
{"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}},
{"bluetooth.scan", {PermissionCategory::Dangerous, "Scan for Bluetooth devices"}},
{"calendar.read", {PermissionCategory::Dangerous, "Read calendar"}},
{"calendar.write", {PermissionCategory::Dangerous, "Modify calendar"}},
{"phone.call", {PermissionCategory::Dangerous, "Make phone calls"}},
{"phone.read_state", {PermissionCategory::Dangerous, "Read phone state"}},
{"sms.read", {PermissionCategory::Dangerous, "Read SMS messages"}},
{"sms.send", {PermissionCategory::Dangerous, "Send SMS messages"}},
// Signature permissions (system apps only)
{"system.settings", {PermissionCategory::Signature, "Modify system settings"}},
{"system.install", {PermissionCategory::Signature, "Install apps"}},
{"system.uninstall", {PermissionCategory::Signature, "Uninstall apps"}},
{"system.admin", {PermissionCategory::Signature, "Device administrator"}},
{"system.overlay", {PermissionCategory::Signature, "Display over other apps"}},
{"system.wallpaper", {PermissionCategory::Signature, "Set wallpaper"}},
};
//=============================================================================
// CONSTRUCTOR
//=============================================================================
PermissionGate::PermissionGate(const SandboxContext& context)
: m_context(context)
, m_last_gesture(std::chrono::steady_clock::time_point::min())
{
}
//=============================================================================
// PERMISSION INFO
//=============================================================================
PermissionCategory PermissionGate::GetCategory(const std::string& permission) {
auto it = PERMISSIONS.find(permission);
if (it != PERMISSIONS.end()) {
return it->second.category;
}
// Unknown permissions default to Dangerous for safety
return PermissionCategory::Dangerous;
}
const PermissionInfo* PermissionGate::GetPermissionInfo(const std::string& permission) {
auto it = PERMISSIONS.find(permission);
if (it != PERMISSIONS.end()) {
return &it->second;
}
return nullptr;
}
//=============================================================================
// PERMISSION CHECKING
//=============================================================================
bool PermissionGate::HasPermission(const std::string& permission) const {
auto category = GetCategory(permission);
switch (category) {
case PermissionCategory::Normal:
return CheckNormalPermission(permission);
case PermissionCategory::Dangerous:
return CheckDangerousPermission(permission);
case PermissionCategory::Signature:
return CheckSignaturePermission(permission);
}
return false;
}
bool PermissionGate::Check(lua_State* L, const std::string& permission) {
if (!HasPermission(permission)) {
luaL_error(L, "permission denied: %s", permission.c_str());
return false;
}
return true;
}
bool PermissionGate::IsDeclared(const std::string& permission) const {
const auto& declared = m_context.permissions;
return std::find(declared.begin(), declared.end(), permission) != declared.end();
}
bool PermissionGate::CheckNormalPermission(const std::string& permission) const {
// Normal permissions are auto-granted if declared in manifest
return IsDeclared(permission);
}
bool PermissionGate::CheckDangerousPermission(const std::string& permission) const {
// Must be declared in manifest
if (!IsDeclared(permission)) {
return false;
}
// System apps get dangerous permissions automatically
if (m_context.is_system_app) {
return true;
}
// Regular apps need runtime grant
return m_runtime_grants.count(permission) > 0;
}
bool PermissionGate::CheckSignaturePermission(const std::string& permission) const {
// Only system apps get signature permissions
if (!m_context.is_system_app) {
return false;
}
// Must still be declared
return IsDeclared(permission);
}
//=============================================================================
// USER GESTURE TRACKING
//=============================================================================
void PermissionGate::RecordUserGesture() {
m_last_gesture = std::chrono::steady_clock::now();
}
bool PermissionGate::HasRecentUserGesture(int ms) const {
// If no gesture has been recorded, return false
if (m_last_gesture == std::chrono::steady_clock::time_point::min()) {
return false;
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_last_gesture);
return elapsed.count() < ms;
}
//=============================================================================
// RUNTIME GRANTS
//=============================================================================
void PermissionGate::GrantPermission(const std::string& permission) {
// Can only grant dangerous permissions
auto category = GetCategory(permission);
if (category == PermissionCategory::Dangerous) {
m_runtime_grants.insert(permission);
}
}
void PermissionGate::RevokePermission(const std::string& permission) {
m_runtime_grants.erase(permission);
}
//=============================================================================
// QUERIES
//=============================================================================
const std::vector<std::string>& PermissionGate::GetDeclaredPermissions() const {
return m_context.permissions;
}
std::vector<std::string> PermissionGate::GetGrantedPermissions() const {
std::vector<std::string> granted;
for (const auto& perm : m_context.permissions) {
if (HasPermission(perm)) {
granted.push_back(perm);
}
}
return granted;
}
} // namespace mosis

View File

@@ -0,0 +1,73 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>
#include <chrono>
struct lua_State;
namespace mosis {
struct SandboxContext; // Forward declaration
enum class PermissionCategory {
Normal, // Auto-granted when declared (e.g., internet, vibrate)
Dangerous, // Requires user consent (e.g., camera, location)
Signature // System apps only (e.g., system.settings)
};
struct PermissionInfo {
PermissionCategory category;
std::string description;
};
class PermissionGate {
public:
explicit PermissionGate(const SandboxContext& context);
// Check if app has permission (throws Lua error if not)
bool Check(lua_State* L, const std::string& permission);
// Check without throwing (returns false if denied)
bool HasPermission(const std::string& permission) const;
// Get permission category
static PermissionCategory GetCategory(const std::string& permission);
// Get permission info (returns nullptr if unknown)
static const PermissionInfo* GetPermissionInfo(const std::string& permission);
// User gesture tracking
void RecordUserGesture();
bool HasRecentUserGesture(int ms = 5000) const;
// Runtime permission grant (called after user consent)
void GrantPermission(const std::string& permission);
void RevokePermission(const std::string& permission);
// Get all declared permissions
const std::vector<std::string>& GetDeclaredPermissions() const;
// Get all granted permissions
std::vector<std::string> GetGrantedPermissions() const;
// Check if permission is declared in manifest
bool IsDeclared(const std::string& permission) const;
private:
const SandboxContext& m_context;
std::unordered_set<std::string> m_runtime_grants; // Runtime-granted dangerous perms
std::chrono::steady_clock::time_point m_last_gesture;
bool CheckNormalPermission(const std::string& permission) const;
bool CheckDangerousPermission(const std::string& permission) const;
bool CheckSignaturePermission(const std::string& permission) const;
};
} // namespace mosis
// Convenience alias
using PermissionGate = mosis::PermissionGate;
using PermissionCategory = mosis::PermissionCategory;

View File

@@ -0,0 +1,209 @@
#include "rate_limiter.h"
#include <algorithm>
namespace mosis {
//=============================================================================
// CONSTRUCTOR (with default limits)
//=============================================================================
RateLimiter::RateLimiter() {
// Network operations
SetLimit("network.request", {10.0, 100.0}); // 10/sec, burst 100
SetLimit("network.websocket", {2.0, 10.0}); // 2/sec, burst 10
SetLimit("network.download", {5.0, 20.0}); // 5/sec, burst 20
// Storage operations
SetLimit("storage.read", {100.0, 500.0}); // 100/sec, burst 500
SetLimit("storage.write", {20.0, 100.0}); // 20/sec, burst 100
SetLimit("storage.delete", {10.0, 50.0}); // 10/sec, burst 50
SetLimit("database.query", {50.0, 200.0}); // 50/sec, burst 200
// Hardware access
SetLimit("camera.capture", {30.0, 30.0}); // 30 fps max
SetLimit("microphone.record", {1.0, 1.0}); // 1 session at a time
SetLimit("location.request", {1.0, 5.0}); // 1/sec, burst 5
SetLimit("sensor.read", {60.0, 60.0}); // 60 Hz max
// Timers
SetLimit("timer.create", {10.0, 100.0}); // 10/sec, burst 100
// Crypto
SetLimit("crypto.random", {100.0, 1000.0}); // 100/sec, burst 1000
SetLimit("crypto.hash", {100.0, 1000.0}); // 100/sec, burst 1000
}
//=============================================================================
// CONFIGURATION
//=============================================================================
void RateLimiter::SetLimit(const std::string& operation, const RateLimitConfig& config) {
std::lock_guard<std::mutex> lock(m_mutex);
m_configs[operation] = config;
}
const RateLimitConfig* RateLimiter::GetLimit(const std::string& operation) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_configs.find(operation);
if (it != m_configs.end()) {
return &it->second;
}
return nullptr;
}
//=============================================================================
// CHECKING
//=============================================================================
bool RateLimiter::Check(const std::string& app_id, const std::string& operation) {
std::lock_guard<std::mutex> lock(m_mutex);
// Find config
auto config_it = m_configs.find(operation);
if (config_it == m_configs.end()) {
// No limit configured, allow by default
return true;
}
const auto& config = config_it->second;
auto& bucket = GetBucket(app_id, operation);
// Refill based on elapsed time
Refill(bucket, config);
// Check if we have a token
if (bucket.tokens >= 1.0) {
bucket.tokens -= 1.0;
return true;
}
return false;
}
bool RateLimiter::CanProceed(const std::string& app_id, const std::string& operation) const {
std::lock_guard<std::mutex> lock(m_mutex);
// Find config
auto config_it = m_configs.find(operation);
if (config_it == m_configs.end()) {
return true; // No limit
}
const auto& config = config_it->second;
std::string key = MakeKey(app_id, operation);
auto bucket_it = m_buckets.find(key);
if (bucket_it == m_buckets.end()) {
return true; // New bucket would have full tokens
}
// Make a copy to check without modifying
Bucket bucket = bucket_it->second;
Refill(bucket, config);
return bucket.tokens >= 1.0;
}
double RateLimiter::GetTokens(const std::string& app_id, const std::string& operation) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::string key = MakeKey(app_id, operation);
auto bucket_it = m_buckets.find(key);
if (bucket_it == m_buckets.end()) {
// Check if there's a config
auto config_it = m_configs.find(operation);
if (config_it != m_configs.end()) {
return config_it->second.max_tokens; // Would start with full
}
return 0.0;
}
// Find config to refill
auto config_it = m_configs.find(operation);
if (config_it == m_configs.end()) {
return bucket_it->second.tokens;
}
// Make a copy to check without modifying
Bucket bucket = bucket_it->second;
Refill(bucket, config_it->second);
return bucket.tokens;
}
//=============================================================================
// RESET
//=============================================================================
void RateLimiter::ResetApp(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
// Find and remove all buckets for this app
std::string prefix = app_id + ":";
for (auto it = m_buckets.begin(); it != m_buckets.end(); ) {
if (it->first.rfind(prefix, 0) == 0) { // starts with app_id:
it = m_buckets.erase(it);
} else {
++it;
}
}
}
void RateLimiter::ClearAll() {
std::lock_guard<std::mutex> lock(m_mutex);
m_buckets.clear();
}
//=============================================================================
// INTERNAL
//=============================================================================
void RateLimiter::Refill(Bucket& bucket, const RateLimitConfig& config) const {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration<double>(now - bucket.last_refill);
// Add tokens based on elapsed time
double new_tokens = bucket.tokens + (elapsed.count() * config.tokens_per_second);
// Cap at max
bucket.tokens = std::min(new_tokens, config.max_tokens);
bucket.last_refill = now;
}
RateLimiter::Bucket& RateLimiter::GetBucket(const std::string& app_id,
const std::string& operation) {
std::string key = MakeKey(app_id, operation);
auto it = m_buckets.find(key);
if (it != m_buckets.end()) {
return it->second;
}
// Create new bucket with full tokens
auto config_it = m_configs.find(operation);
double initial = (config_it != m_configs.end()) ? config_it->second.max_tokens : 1.0;
m_buckets[key] = Bucket{
.tokens = initial,
.last_refill = std::chrono::steady_clock::now()
};
return m_buckets[key];
}
std::string RateLimiter::MakeKey(const std::string& app_id, const std::string& operation) {
return app_id + ":" + operation;
}
//=============================================================================
// GLOBAL INSTANCE
//=============================================================================
RateLimiter& GetRateLimiter() {
static RateLimiter instance;
return instance;
}
} // namespace mosis

View File

@@ -0,0 +1,68 @@
#pragma once
#include <string>
#include <unordered_map>
#include <mutex>
#include <chrono>
namespace mosis {
struct RateLimitConfig {
double tokens_per_second; // Refill rate
double max_tokens; // Bucket capacity
};
class RateLimiter {
public:
// Default limits for common operations
RateLimiter();
// Check if operation is allowed (consumes token if yes)
bool Check(const std::string& app_id, const std::string& operation);
// Check without consuming token
bool CanProceed(const std::string& app_id, const std::string& operation) const;
// Configure limits for an operation
void SetLimit(const std::string& operation, const RateLimitConfig& config);
// Get config for an operation
const RateLimitConfig* GetLimit(const std::string& operation) const;
// Get current token count for app+operation
double GetTokens(const std::string& app_id, const std::string& operation) const;
// Reset all buckets for an app (e.g., on app restart)
void ResetApp(const std::string& app_id);
// Clear all buckets
void ClearAll();
private:
struct Bucket {
double tokens;
std::chrono::steady_clock::time_point last_refill;
};
// Refill bucket based on elapsed time
void Refill(Bucket& bucket, const RateLimitConfig& config) const;
// Get or create bucket for app+operation
Bucket& GetBucket(const std::string& app_id, const std::string& operation);
// Get bucket key
static std::string MakeKey(const std::string& app_id, const std::string& operation);
mutable std::mutex m_mutex;
std::unordered_map<std::string, RateLimitConfig> m_configs;
mutable std::unordered_map<std::string, Bucket> m_buckets;
};
// Global rate limiter (singleton)
RateLimiter& GetRateLimiter();
} // namespace mosis
// Convenience alias
using RateLimiter = mosis::RateLimiter;
using RateLimitConfig = mosis::RateLimitConfig;

View File

@@ -0,0 +1,440 @@
#include "timer_manager.h"
#include <lua.hpp>
#include <algorithm>
namespace mosis {
//=============================================================================
// CONSTRUCTOR / DESTRUCTOR
//=============================================================================
TimerManager::TimerManager() = default;
TimerManager::~TimerManager() {
std::lock_guard<std::mutex> lock(m_mutex);
// Release all Lua callback references
for (auto& timer : m_timers) {
if (timer.callback_ref != LUA_NOREF && timer.L) {
luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref);
}
}
m_timers.clear();
}
//=============================================================================
// TIMER CREATION
//=============================================================================
TimerId TimerManager::SetTimeout(lua_State* L, const std::string& app_id,
int callback_ref, int delay_ms) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check per-app limit
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
// Release the callback reference since we're not using it
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
return 0;
}
// Clamp delay
if (delay_ms < MIN_TIMEOUT_MS) {
delay_ms = MIN_TIMEOUT_MS;
}
Timer timer;
timer.id = m_next_id++;
timer.app_id = app_id;
timer.fire_time = std::chrono::steady_clock::now() + Duration(delay_ms);
timer.interval = Duration(0);
timer.callback_ref = callback_ref;
timer.L = L;
timer.cancelled = false;
timer.is_interval = false;
m_timers.push_back(timer);
m_app_timer_counts[app_id]++;
m_app_timer_ids[app_id].insert(timer.id);
return timer.id;
}
TimerId TimerManager::SetInterval(lua_State* L, const std::string& app_id,
int callback_ref, int interval_ms) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check per-app limit
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
return 0;
}
// Clamp interval to minimum
if (interval_ms < MIN_INTERVAL_MS) {
interval_ms = MIN_INTERVAL_MS;
}
Timer timer;
timer.id = m_next_id++;
timer.app_id = app_id;
timer.fire_time = std::chrono::steady_clock::now() + Duration(interval_ms);
timer.interval = Duration(interval_ms);
timer.callback_ref = callback_ref;
timer.L = L;
timer.cancelled = false;
timer.is_interval = true;
m_timers.push_back(timer);
m_app_timer_counts[app_id]++;
m_app_timer_ids[app_id].insert(timer.id);
return timer.id;
}
//=============================================================================
// TIMER CANCELLATION
//=============================================================================
bool TimerManager::ClearTimer(const std::string& app_id, TimerId id) {
std::lock_guard<std::mutex> lock(m_mutex);
// Find the timer
auto it = std::find_if(m_timers.begin(), m_timers.end(),
[id, &app_id](const Timer& t) {
return t.id == id && t.app_id == app_id && !t.cancelled;
});
if (it == m_timers.end()) {
return false;
}
// Mark as cancelled (will be removed during ProcessTimers)
it->cancelled = true;
// Release the Lua callback reference
if (it->callback_ref != LUA_NOREF && it->L) {
luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref);
it->callback_ref = LUA_NOREF;
}
// Update counts
if (m_app_timer_counts[app_id] > 0) {
m_app_timer_counts[app_id]--;
}
m_app_timer_ids[app_id].erase(id);
return true;
}
void TimerManager::ClearAppTimers(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
// Get all timer IDs for this app
auto it = m_app_timer_ids.find(app_id);
if (it == m_app_timer_ids.end()) {
return;
}
// Mark all timers as cancelled and release references
for (auto& timer : m_timers) {
if (timer.app_id == app_id && !timer.cancelled) {
timer.cancelled = true;
if (timer.callback_ref != LUA_NOREF && timer.L) {
luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref);
timer.callback_ref = LUA_NOREF;
}
}
}
// Clear tracking
m_app_timer_counts[app_id] = 0;
m_app_timer_ids[app_id].clear();
}
//=============================================================================
// TIMER PROCESSING
//=============================================================================
void TimerManager::FireTimer(Timer& timer) {
if (timer.cancelled || timer.callback_ref == LUA_NOREF || !timer.L) {
return;
}
lua_State* L = timer.L;
// Get the callback from registry
lua_rawgeti(L, LUA_REGISTRYINDEX, timer.callback_ref);
if (lua_isfunction(L, -1)) {
// Call the callback with protected call
int result = lua_pcall(L, 0, 0, 0);
if (result != LUA_OK) {
// Log error but don't propagate
lua_pop(L, 1);
}
} else {
lua_pop(L, 1);
}
}
void TimerManager::RescheduleInterval(Timer& timer) {
// Update fire time for next interval
timer.fire_time = std::chrono::steady_clock::now() + timer.interval;
}
int TimerManager::ProcessTimers() {
// We need to be careful here - firing a timer might cause
// new timers to be added or timers to be cancelled
std::vector<Timer> to_fire;
std::vector<TimerId> to_reschedule;
std::vector<TimerId> to_remove;
auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(m_mutex);
// Find all timers that should fire
for (auto& timer : m_timers) {
if (timer.cancelled) {
to_remove.push_back(timer.id);
} else if (timer.fire_time <= now) {
to_fire.push_back(timer);
if (timer.is_interval) {
to_reschedule.push_back(timer.id);
} else {
to_remove.push_back(timer.id);
}
}
}
}
// Fire timers outside the lock to allow callbacks to create new timers
int fired_count = 0;
for (auto& timer : to_fire) {
FireTimer(timer);
fired_count++;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
// Reschedule intervals
for (TimerId id : to_reschedule) {
auto it = std::find_if(m_timers.begin(), m_timers.end(),
[id](const Timer& t) { return t.id == id && !t.cancelled; });
if (it != m_timers.end()) {
RescheduleInterval(*it);
}
}
// Remove completed/cancelled timers
for (TimerId id : to_remove) {
auto it = std::find_if(m_timers.begin(), m_timers.end(),
[id](const Timer& t) { return t.id == id; });
if (it != m_timers.end()) {
// Release reference if not already released
if (it->callback_ref != LUA_NOREF && it->L && !it->is_interval) {
luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref);
}
// Update counts only for non-cancelled (timeout) timers
if (!it->cancelled && !it->is_interval) {
const std::string& app_id = it->app_id;
if (m_app_timer_counts[app_id] > 0) {
m_app_timer_counts[app_id]--;
}
m_app_timer_ids[app_id].erase(id);
}
m_timers.erase(it);
}
}
}
return fired_count;
}
size_t TimerManager::GetTimerCount(const std::string& app_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_app_timer_counts.find(app_id);
if (it == m_app_timer_counts.end()) {
return 0;
}
return it->second;
}
//=============================================================================
// LUA API
//=============================================================================
// Registry keys for storing manager pointer and app_id
static const char* TIMER_MANAGER_KEY = "__mosis_timer_manager";
static const char* TIMER_APP_ID_KEY = "__mosis_timer_app_id";
// setTimeout(callback, delay_ms) -> timer_id
static int lua_setTimeout(lua_State* L) {
// Check arguments
luaL_checktype(L, 1, LUA_TFUNCTION);
int delay_ms = static_cast<int>(luaL_checkinteger(L, 2));
// Get timer manager from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "timer system not initialized");
}
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Get app_id from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
if (!lua_isstring(L, -1)) {
return luaL_error(L, "app_id not set");
}
std::string app_id = lua_tostring(L, -1);
lua_pop(L, 1);
// Store the callback in registry
lua_pushvalue(L, 1); // Push the callback
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
// Create the timer
TimerId id = manager->SetTimeout(L, app_id, callback_ref, delay_ms);
if (id == 0) {
return luaL_error(L, "timer limit exceeded");
}
lua_pushinteger(L, static_cast<lua_Integer>(id));
return 1;
}
// clearTimeout(timer_id)
static int lua_clearTimeout(lua_State* L) {
TimerId id = static_cast<TimerId>(luaL_checkinteger(L, 1));
// Get timer manager from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "timer system not initialized");
}
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Get app_id from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
if (!lua_isstring(L, -1)) {
return luaL_error(L, "app_id not set");
}
std::string app_id = lua_tostring(L, -1);
lua_pop(L, 1);
manager->ClearTimer(app_id, id);
return 0;
}
// setInterval(callback, interval_ms) -> timer_id
static int lua_setInterval(lua_State* L) {
// Check arguments
luaL_checktype(L, 1, LUA_TFUNCTION);
int interval_ms = static_cast<int>(luaL_checkinteger(L, 2));
// Get timer manager from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "timer system not initialized");
}
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Get app_id from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
if (!lua_isstring(L, -1)) {
return luaL_error(L, "app_id not set");
}
std::string app_id = lua_tostring(L, -1);
lua_pop(L, 1);
// Store the callback in registry
lua_pushvalue(L, 1); // Push the callback
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
// Create the timer
TimerId id = manager->SetInterval(L, app_id, callback_ref, interval_ms);
if (id == 0) {
return luaL_error(L, "timer limit exceeded");
}
lua_pushinteger(L, static_cast<lua_Integer>(id));
return 1;
}
// clearInterval(timer_id)
static int lua_clearInterval(lua_State* L) {
// Same as clearTimeout
return lua_clearTimeout(L);
}
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id) {
// Store timer manager pointer in registry
lua_pushlightuserdata(L, manager);
lua_setfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
// Store app_id in registry
lua_pushstring(L, app_id.c_str());
lua_setfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
// Get the real _G (not the proxy)
// We need to set these in the real global table that the proxy reads from
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if we're dealing with a proxy (has __index metatable)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// We have a proxy, use the __index table as the real _G
lua_remove(L, -2); // Remove metatable
lua_remove(L, -2); // Remove proxy
// Now top of stack is real _G
lua_pushcfunction(L, lua_setTimeout);
lua_setfield(L, -2, "setTimeout");
lua_pushcfunction(L, lua_clearTimeout);
lua_setfield(L, -2, "clearTimeout");
lua_pushcfunction(L, lua_setInterval);
lua_setfield(L, -2, "setInterval");
lua_pushcfunction(L, lua_clearInterval);
lua_setfield(L, -2, "clearInterval");
lua_pop(L, 1); // Pop real _G
return;
}
lua_pop(L, 2); // Pop __index and metatable
}
// No proxy, just use _G directly
lua_pop(L, 1); // Pop whatever we got from LUA_RIDX_GLOBALS
// Register as globals
lua_pushcfunction(L, lua_setTimeout);
lua_setglobal(L, "setTimeout");
lua_pushcfunction(L, lua_clearTimeout);
lua_setglobal(L, "clearTimeout");
lua_pushcfunction(L, lua_setInterval);
lua_setglobal(L, "setInterval");
lua_pushcfunction(L, lua_clearInterval);
lua_setglobal(L, "clearInterval");
}
} // namespace mosis

View File

@@ -0,0 +1,87 @@
#pragma once
#include <string>
#include <functional>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <chrono>
#include <mutex>
#include <cstdint>
struct lua_State;
namespace mosis {
using TimerId = uint64_t;
using TimePoint = std::chrono::steady_clock::time_point;
using Duration = std::chrono::milliseconds;
struct Timer {
TimerId id;
std::string app_id;
TimePoint fire_time;
Duration interval; // 0 for setTimeout, >0 for setInterval
int callback_ref; // Lua registry reference
lua_State* L; // Lua state that owns the callback
bool cancelled = false;
bool is_interval = false;
};
class TimerManager {
public:
TimerManager();
~TimerManager();
// Non-copyable
TimerManager(const TimerManager&) = delete;
TimerManager& operator=(const TimerManager&) = delete;
// Create timers (returns timer ID, 0 on failure)
TimerId SetTimeout(lua_State* L, const std::string& app_id,
int callback_ref, int delay_ms);
TimerId SetInterval(lua_State* L, const std::string& app_id,
int callback_ref, int interval_ms);
// Cancel timers
bool ClearTimer(const std::string& app_id, TimerId id);
// Cancel all timers for an app (call on app stop)
void ClearAppTimers(const std::string& app_id);
// Process timers (call from main loop)
// Returns number of timers fired
int ProcessTimers();
// Get timer count for an app
size_t GetTimerCount(const std::string& app_id) const;
// Configuration
static constexpr size_t MAX_TIMERS_PER_APP = 100;
static constexpr int MIN_INTERVAL_MS = 10;
static constexpr int MIN_TIMEOUT_MS = 0;
private:
TimerId m_next_id = 1;
// All timers (we use a vector and sort/search as needed)
std::vector<Timer> m_timers;
// Track timer count per app
std::unordered_map<std::string, size_t> m_app_timer_counts;
// Track which timer IDs belong to which app (for fast cancellation)
std::unordered_map<std::string, std::unordered_set<TimerId>> m_app_timer_ids;
mutable std::mutex m_mutex;
void FireTimer(Timer& timer);
void RemoveTimer(TimerId id);
void RescheduleInterval(Timer& timer);
};
// Lua API registration
// Registers: setTimeout, clearTimeout, setInterval, clearInterval
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id);
} // namespace mosis