106 KiB
Mosis Lua Sandbox Security
Status: Design Phase Goal: Secure app isolation with defense-in-depth approach.
Overview
Third-party apps run in isolated Lua environments with restricted access to system resources. Each app gets its own lua_State with carefully controlled APIs.
┌─────────────────────────────────────────────────────────────────────┐
│ Mosis Kernel │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ LuaSandboxManager │ │
│ │ - Creates per-app lua_State with custom allocator │ │
│ │ - Enforces memory/CPU limits │ │
│ │ - Routes permission-gated API calls │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ App A State │ │ App B State │ │ App C State │ │
│ │ (isolated) │ │ (isolated) │ │ (isolated) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Permission Gate │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────────────┐ │
│ │ System Services │ │
│ │ Camera │ Network │ Storage │ Contacts │ Messages │ ... │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Threat Model
Threat Categories
| Category | Threat | Severity | Mitigation |
|---|---|---|---|
| Code Execution | os.execute(), io.popen() |
Critical | Remove globals |
| File Access | io.open(), loadfile() |
Critical | Remove globals |
| Bytecode Injection | load() with binary chunks |
Critical | Text-only loading |
| Memory Exhaustion | Infinite tables/strings | High | Custom allocator with limits |
| CPU Exhaustion | Infinite loops | High | Instruction count hook |
| Sandbox Escape | debug.getregistry() |
Critical | Remove debug library |
| Global Pollution | Modifying _G, string |
Medium | Frozen globals |
| Path Traversal | require("../../etc/passwd") |
Critical | Path validation |
| Data Exfiltration | Unauthorized network access | High | Permission-gated network |
| Privilege Escalation | Access other app's data | High | Per-app storage isolation |
| Timing Attacks | High-resolution timers | Low | Limit timer precision |
| Side Channels | Memory/CPU usage patterns | Low | Rate limiting |
Attacker Capabilities
We assume a malicious app developer can:
- Write arbitrary Lua code within the sandbox
- Attempt to exploit any exposed API
- Try to escape the sandbox via Lua language features
- Attempt DoS via resource exhaustion
- Try to access other apps' data
- Attempt to exfiltrate user data
Security Layers
Layer 1: Dangerous Globals Removal
Status: Designed
Remove all dangerous functions before any app code runs.
void RemoveDangerousGlobals(lua_State* L) {
const char* dangerous[] = {
// Execution
"dofile", "loadfile", "load", "loadstring",
// Raw access (bypass metatables)
"rawget", "rawset", "rawequal", "rawlen",
// Metatable manipulation
"getmetatable", "setmetatable",
// GC manipulation
"collectgarbage",
// Must not exist
"os", "io", "debug", "package", "require",
"ffi", "jit", "newproxy",
nullptr
};
for (const char** p = dangerous; *p; ++p) {
lua_pushnil(L);
lua_setglobal(L, *p);
}
// Remove string.dump (creates bytecode)
lua_getglobal(L, "string");
lua_pushnil(L);
lua_setfield(L, -2, "dump");
lua_pop(L, 1);
}
Safe globals retained:
print(redirected to logger)type,tonumber,tostringpairs,ipairs,nextpcall,xpcallassert,errorselect,unpackstring.*(exceptdump)table.*math.*utf8.*
Layer 2: Bytecode Prevention
Status: Designed
Prevent loading of binary Lua chunks which can bypass sandbox checks.
// Only allow text chunks, reject bytecode
int result = luaL_loadbufferx(L, code, len, name, "t");
// mode: "t" = text only
Bytecode starts with \27Lua signature - the "t" mode rejects this.
Layer 3: Memory Limits
Status: Designed
Custom allocator tracks and limits memory usage per app.
void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
auto* sandbox = static_cast<LuaSandbox*>(ud);
size_t new_usage = sandbox->m_memory_used - osize + nsize;
if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) {
return nullptr; // Allocation fails
}
sandbox->m_memory_used = new_usage;
if (nsize == 0) {
free(ptr);
return nullptr;
}
return realloc(ptr, nsize);
}
lua_State* L = lua_newstate(SandboxAlloc, sandbox);
Default limits:
| Resource | Limit | Rationale |
|---|---|---|
| Memory | 16 MB | Enough for UI, prevents DoS |
| String size | 1 MB | Prevent single huge allocation |
| Table entries | 100,000 | Prevent hash DoS |
Layer 4: CPU Limits
Status: Designed
Instruction count hook prevents infinite loops and excessive computation.
void InstructionHook(lua_State* L, lua_Debug* ar) {
auto* sandbox = GetSandbox(L);
sandbox->m_instructions += 1000;
if (sandbox->m_instructions > sandbox->m_limits.instructions) {
luaL_error(L, "instruction limit exceeded");
}
}
lua_sethook(L, InstructionHook, LUA_MASKCOUNT, 1000);
Default limits:
| Context | Limit | Approx Time |
|---|---|---|
| Event handler | 1,000,000 | ~10ms |
| Frame update | 100,000 | ~1ms |
| App initialization | 10,000,000 | ~100ms |
Layer 5: Path Sandboxing
Status: Designed
All file access restricted to app's own directory.
bool ValidatePath(const std::string& app_path, const std::string& requested) {
// Reject obvious traversal attempts
if (requested.find("..") != std::string::npos) return false;
if (requested.find(':') != std::string::npos) return false; // Windows drive
if (requested[0] == '/' || requested[0] == '\\') return false; // Absolute
// Canonicalize and verify within app directory
std::filesystem::path base = std::filesystem::canonical(app_path);
std::filesystem::path full = std::filesystem::weakly_canonical(
base / requested
);
// Must be within app directory
auto [base_end, full_it] = std::mismatch(
base.begin(), base.end(), full.begin()
);
return base_end == base.end();
}
Layer 6: Safe require Replacement
Status: Designed
Custom module loader only loads from app's scripts directory.
int SafeRequire(lua_State* L) {
const char* module = luaL_checkstring(L, 1);
auto* sandbox = GetSandbox(L);
// Validate module name
std::string name(module);
if (!IsValidModuleName(name)) {
return luaL_error(L, "invalid module name");
}
// Check cache
lua_getfield(L, LUA_REGISTRYINDEX, "__loaded");
lua_getfield(L, -1, module);
if (!lua_isnil(L, -1)) return 1;
lua_pop(L, 2);
// Load from app/scripts/name.lua
std::string path = sandbox->app_path + "/scripts/" + name + ".lua";
if (!ValidatePath(sandbox->app_path, "scripts/" + name + ".lua")) {
return luaL_error(L, "path validation failed");
}
// Load as text only
if (luaL_loadfilex(L, path.c_str(), "t") != LUA_OK) {
return lua_error(L);
}
// Execute and cache
lua_call(L, 0, 1);
lua_getfield(L, LUA_REGISTRYINDEX, "__loaded");
lua_pushvalue(L, -2);
lua_setfield(L, -2, module);
lua_pop(L, 1);
return 1;
}
Layer 7: Metatable Protection
Status: Designed
Prevent modification of built-in metatables and sandbox escape via metatables.
void ProtectBuiltinTables(lua_State* L) {
// Protect string metatable
lua_pushstring(L, "");
lua_getmetatable(L, -1);
lua_pushstring(L, "string");
lua_setfield(L, -2, "__metatable"); // Prevents getmetatable
lua_pop(L, 2);
// Freeze _G
lua_pushglobaltable(L);
lua_newtable(L);
lua_pushstring(L, "globals");
lua_setfield(L, -2, "__metatable");
lua_pushcfunction(L, [](lua_State* L) -> int {
return luaL_error(L, "cannot modify global environment");
});
lua_setfield(L, -2, "__newindex");
lua_setmetatable(L, -2);
lua_pop(L, 1);
}
Safe metatable access:
// getmetatable returns __metatable field (not real metatable)
// setmetatable blocked on protected tables
Layer 8: Permission-Gated APIs
Status: Designed
Sensitive APIs check permissions before execution.
class PermissionGate {
public:
static bool Check(lua_State* L, const std::string& permission) {
auto* sandbox = GetSandbox(L);
auto& perms = sandbox->m_context.permissions;
return std::find(perms.begin(), perms.end(), permission) != perms.end();
}
static int RequirePermission(lua_State* L, const char* perm) {
if (!Check(L, perm)) {
return luaL_error(L, "permission denied: %s", perm);
}
return 0;
}
};
// Usage in API
int CameraCapture(lua_State* L) {
PermissionGate::RequirePermission(L, "camera");
// ... actual implementation
}
Permission categories:
| Permission | Required For | Risk Level |
|---|---|---|
storage |
App-private file access | Normal (auto-granted) |
camera |
Camera access | Dangerous |
microphone |
Audio recording | Dangerous |
location |
GPS/location | Dangerous |
contacts.read |
Read contacts | Dangerous |
contacts.write |
Modify contacts | Dangerous |
network |
HTTP requests | Dangerous |
network.websocket |
WebSocket connections | Dangerous |
bluetooth |
Bluetooth access | Dangerous |
system.notifications |
Show notifications | Normal |
Additional Security Measures
Network Request Sandboxing
Status: Planned
Network requests are proxied through the kernel with domain filtering.
struct NetworkPolicy {
std::vector<std::string> allowed_domains; // Whitelist
std::vector<std::string> blocked_domains; // Blacklist
bool allow_localhost = false;
bool allow_private_ips = false;
size_t max_request_size = 10 * 1024 * 1024; // 10 MB
size_t max_response_size = 50 * 1024 * 1024; // 50 MB
int timeout_ms = 30000;
};
class NetworkSandbox {
public:
bool ValidateRequest(const HttpRequest& req) {
// Parse URL
auto url = ParseUrl(req.url);
// Block private IPs
if (!m_policy.allow_private_ips && IsPrivateIP(url.host)) {
return false;
}
// Block localhost
if (!m_policy.allow_localhost && IsLocalhost(url.host)) {
return false;
}
// Check domain whitelist/blacklist
if (!m_policy.allowed_domains.empty()) {
if (!MatchesDomain(url.host, m_policy.allowed_domains)) {
return false;
}
}
if (MatchesDomain(url.host, m_policy.blocked_domains)) {
return false;
}
// Size limits
if (req.body.size() > m_policy.max_request_size) {
return false;
}
return true;
}
private:
bool IsPrivateIP(const std::string& host) {
// 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 169.254.x.x
// Also check for IPv6 private ranges
}
};
Manifest declaration:
{
"permissions": ["network"],
"network": {
"allowed_domains": [
"api.example.com",
"*.cdn.example.com"
]
}
}
Timer and Callback Security
Status: Planned
Timers and callbacks are managed by the kernel, not exposed directly.
class TimerManager {
public:
uint32_t SetTimeout(LuaSandbox* sandbox, int callback_ref, int ms) {
// Validate delay
if (ms < 0) return 0;
if (ms < 10) ms = 10; // Minimum 10ms granularity
// Limit active timers per app
if (m_app_timers[sandbox->app_id()].size() >= MAX_TIMERS_PER_APP) {
return 0;
}
Timer timer{
.id = ++m_next_id,
.sandbox = sandbox,
.callback_ref = callback_ref,
.fire_time = Now() + ms,
.repeat = false
};
m_timers.push(timer);
m_app_timers[sandbox->app_id()].insert(timer.id);
return timer.id;
}
void ClearAppTimers(const std::string& app_id) {
// Called when app is stopped
for (uint32_t id : m_app_timers[app_id]) {
// Mark timer as cancelled
}
m_app_timers.erase(app_id);
}
private:
static constexpr size_t MAX_TIMERS_PER_APP = 100;
};
Lua API:
-- Safe timer APIs (kernel-managed)
local id = setTimeout(function()
print("fired!")
end, 1000)
clearTimeout(id)
local id = setInterval(function()
print("tick")
end, 1000)
clearInterval(id)
Inter-App Communication
Status: Planned
Apps communicate through kernel-mediated message passing.
struct AppMessage {
std::string from_app;
std::string to_app;
std::string action;
std::string data; // JSON
std::vector<std::string> required_permissions;
};
class MessageBus {
public:
bool Send(const AppMessage& msg) {
// Validate sender
if (!IsAppRunning(msg.from_app)) return false;
// Check if target accepts this action
if (!CanReceive(msg.to_app, msg.action)) return false;
// Check permissions
for (const auto& perm : msg.required_permissions) {
if (!HasPermission(msg.from_app, perm)) return false;
}
// Deliver (async)
QueueMessage(msg);
return true;
}
};
Manifest declaration:
{
"intents": {
"receive": [
{
"action": "share",
"types": ["image/*", "text/plain"]
}
],
"send": [
"share", "view"
]
}
}
Audit Logging
Status: Planned
All security-relevant operations are logged for debugging and forensics.
enum class AuditEvent {
AppStart,
AppStop,
PermissionCheck,
PermissionDenied,
NetworkRequest,
FileAccess,
StorageAccess,
TimerCreated,
MessageSent,
SandboxViolation,
ResourceLimitHit
};
struct AuditEntry {
std::chrono::system_clock::time_point timestamp;
std::string app_id;
AuditEvent event;
std::string detail;
bool success;
};
class AuditLog {
public:
void Log(const AuditEntry& entry) {
// Thread-safe append
std::lock_guard lock(m_mutex);
m_entries.push_back(entry);
// Trim old entries
if (m_entries.size() > MAX_ENTRIES) {
m_entries.erase(m_entries.begin(),
m_entries.begin() + TRIM_COUNT);
}
// Log security events to system log
if (entry.event == AuditEvent::SandboxViolation ||
entry.event == AuditEvent::PermissionDenied) {
Logger::Warn("[SECURITY] {} - {} - {}",
entry.app_id, EventName(entry.event), entry.detail);
}
}
std::vector<AuditEntry> GetAppLogs(const std::string& app_id, size_t limit);
};
Coroutine Safety
Status: Planned
Coroutines are allowed but resource-tracked.
void SetupSafeCoroutines(lua_State* L) {
// Allow coroutine.* but track
luaL_requiref(L, "coroutine", luaopen_coroutine, 1);
lua_pop(L, 1);
// Wrap coroutine.create to track
lua_getglobal(L, "coroutine");
lua_getfield(L, -1, "create");
lua_pushcclosure(L, [](lua_State* L) -> int {
auto* sandbox = GetSandbox(L);
// Limit active coroutines
if (sandbox->m_coroutine_count >= MAX_COROUTINES) {
return luaL_error(L, "too many coroutines");
}
// Call original create
lua_pushvalue(L, lua_upvalueindex(1));
lua_pushvalue(L, 1);
lua_call(L, 1, 1);
sandbox->m_coroutine_count++;
return 1;
}, 1);
lua_setfield(L, -2, "create");
lua_pop(L, 1);
}
String Pattern DoS Prevention
Status: Planned
Complex regex patterns can cause catastrophic backtracking.
int SafeStringMatch(lua_State* L) {
size_t slen, plen;
const char* s = luaL_checklstring(L, 1, &slen);
const char* p = luaL_checklstring(L, 2, &plen);
// Limit pattern complexity
if (plen > 256) {
return luaL_error(L, "pattern too long");
}
// Count pattern operators
int complexity = 0;
for (size_t i = 0; i < plen; i++) {
if (p[i] == '*' || p[i] == '+' || p[i] == '-' || p[i] == '?') {
complexity++;
}
}
// Reject overly complex patterns
if (complexity > 10) {
return luaL_error(L, "pattern too complex");
}
// Also limit based on string length * complexity
if (slen * complexity > 1000000) {
return luaL_error(L, "operation too expensive");
}
// Call original
return original_match(L);
}
Runtime Security Audit
Audit Function
std::vector<SecurityIssue> AuditSandbox(lua_State* L) {
std::vector<SecurityIssue> issues;
// Check dangerous globals
const char* dangerous[] = {
"os", "io", "debug", "package", "ffi", "jit",
"dofile", "loadfile", "load", "loadstring",
"rawget", "rawset", "rawequal", "rawlen",
"collectgarbage", "newproxy",
nullptr
};
for (const char** p = dangerous; *p; ++p) {
lua_getglobal(L, *p);
if (!lua_isnil(L, -1)) {
issues.push_back({
.severity = Severity::Critical,
.type = "dangerous_global",
.detail = *p
});
}
lua_pop(L, 1);
}
// Check string.dump
lua_getglobal(L, "string");
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "dump");
if (!lua_isnil(L, -1)) {
issues.push_back({
.severity = Severity::Critical,
.type = "bytecode_dump",
.detail = "string.dump exists"
});
}
lua_pop(L, 1);
}
lua_pop(L, 1);
// Check instruction hook
if (lua_gethook(L) == nullptr) {
issues.push_back({
.severity = Severity::High,
.type = "no_cpu_limit",
.detail = "instruction hook not set"
});
}
// Check allocator (verify it's our sandbox allocator)
// ...
return issues;
}
Startup Audit
bool LuaSandbox::Initialize() {
SetupSandbox();
// Audit before running any app code
auto issues = AuditSandbox(L);
for (const auto& issue : issues) {
if (issue.severity == Severity::Critical) {
Logger::Error("Sandbox audit failed: {} - {}",
issue.type, issue.detail);
return false;
}
}
return true;
}
Testing Strategy
Unit Tests
// test_sandbox_security.cpp
TEST(LuaSandbox, DangerousGlobalsRemoved) {
LuaSandbox sandbox(TestContext());
EXPECT_FALSE(sandbox.LoadString("return os"));
EXPECT_FALSE(sandbox.LoadString("return io"));
EXPECT_FALSE(sandbox.LoadString("return debug"));
EXPECT_FALSE(sandbox.LoadString("os.execute('ls')"));
EXPECT_FALSE(sandbox.LoadString("io.open('/etc/passwd')"));
}
TEST(LuaSandbox, BytecodeRejected) {
LuaSandbox sandbox(TestContext());
// Try to load bytecode
EXPECT_FALSE(sandbox.LoadString("\x1bLuaS\x00..."));
EXPECT_TRUE(sandbox.GetLastError().find("binary") != std::string::npos);
}
TEST(LuaSandbox, MemoryLimitEnforced) {
SandboxLimits limits;
limits.memory_limit_bytes = 1024 * 1024; // 1 MB
LuaSandbox sandbox(TestContext(limits));
EXPECT_FALSE(sandbox.LoadString(R"(
local t = {}
while true do
t[#t+1] = string.rep('x', 100000)
end
)"));
}
TEST(LuaSandbox, CPULimitEnforced) {
SandboxLimits limits;
limits.instruction_limit = 10000;
LuaSandbox sandbox(TestContext(limits));
EXPECT_FALSE(sandbox.LoadString("while true do end"));
EXPECT_TRUE(sandbox.GetLastError().find("instruction") != std::string::npos);
}
TEST(LuaSandbox, PathTraversalBlocked) {
LuaSandbox sandbox(TestContext());
EXPECT_FALSE(sandbox.LoadString("require('../../../etc/passwd')"));
EXPECT_FALSE(sandbox.LoadString("require('..\\\\..\\\\..\\\\windows\\\\system32\\\\config')"));
EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')"));
EXPECT_FALSE(sandbox.LoadString("require('C:\\\\Windows\\\\System32\\\\config')"));
}
TEST(LuaSandbox, MetatableProtected) {
LuaSandbox sandbox(TestContext());
// Cannot access string metatable internals
EXPECT_TRUE(sandbox.LoadString(R"(
local mt = getmetatable("")
assert(mt == "string", "metatable should be protected")
)"));
// Cannot modify globals
EXPECT_FALSE(sandbox.LoadString("_G.print = nil"));
}
TEST(LuaSandbox, SafeOperationsWork) {
LuaSandbox sandbox(TestContext());
EXPECT_TRUE(sandbox.LoadString(R"(
-- Math
local x = math.sin(1.5) + math.floor(3.7)
-- String
local s = string.format("hello %d", 42)
local upper = string.upper("test")
-- Table
local t = {1, 2, 3}
table.insert(t, 4)
table.sort(t)
-- Iteration
for i, v in ipairs(t) do end
for k, v in pairs({a=1, b=2}) do end
-- Error handling
local ok, err = pcall(function() error("test") end)
assert(not ok)
-- Type checks
assert(type({}) == "table")
assert(type("") == "string")
)"));
}
Fuzzing
// Fuzz the sandbox with random Lua code
void FuzzSandbox() {
std::random_device rd;
std::mt19937 gen(rd());
for (int i = 0; i < 10000; i++) {
std::string code = GenerateRandomLuaCode(gen);
LuaSandbox sandbox(TestContext());
sandbox.LoadString(code); // Should never crash
// Verify sandbox integrity after each run
auto issues = AuditSandbox(sandbox.GetState());
ASSERT(issues.empty()) << "Sandbox compromised by: " << code;
}
}
Implementation Status
Core Sandbox (Milestone 1)
| Component | Status | File |
|---|---|---|
| LuaSandbox class | ✅ Implemented | src/main/cpp/sandbox/lua_sandbox.h/cpp |
| Custom allocator | ✅ Implemented | src/main/cpp/sandbox/lua_sandbox.cpp |
| Instruction hook | ✅ Implemented | src/main/cpp/sandbox/lua_sandbox.cpp |
| Globals removal | ✅ Implemented | src/main/cpp/sandbox/lua_sandbox.cpp |
| Bytecode prevention | ✅ Implemented | src/main/cpp/sandbox/lua_sandbox.cpp |
| Safe require | ❌ Planned | Milestone 4 |
| Metatable protection | ✅ Implemented | src/main/cpp/sandbox/lua_sandbox.cpp |
| Path validation | ❌ Planned | Milestone 4 |
APIs
| Component | Status | File |
|---|---|---|
| Storage API | ❌ Planned | Milestone 7 |
| Timer API | ❌ Planned | Milestone 5 |
| Network API | ❌ Planned | Milestone 9-10 |
| RmlUi bridge | ❌ Planned | Milestone 20 |
Security
| Component | Status | File |
|---|---|---|
| Permission gate | ❌ Planned | Milestone 2 |
| Audit logging | ❌ Planned | Milestone 3 |
| Network sandbox | ❌ Planned | Milestone 9 |
| Message bus | ❌ Planned | Milestone 18 |
Testing
| Component | Status | File |
|---|---|---|
| Unit tests | ✅ Implemented | sandbox-test/ (11 tests) |
| Integration tests | ❌ Planned | Milestone 19 |
| Fuzzing | ❌ Planned | Milestone 19 |
Additional Security Measures (Continued)
JSON Parsing Security
Status: Planned
Untrusted JSON from network/storage must be safely parsed.
struct JsonLimits {
size_t max_depth = 32;
size_t max_string_length = 1024 * 1024; // 1 MB
size_t max_array_elements = 100000;
size_t max_object_keys = 10000;
};
int SafeJsonDecode(lua_State* L) {
size_t len;
const char* json = luaL_checklstring(L, 1, &len);
// Size check
if (len > 10 * 1024 * 1024) { // 10 MB max
return luaL_error(L, "JSON too large");
}
auto* sandbox = GetSandbox(L);
try {
auto parsed = ParseJsonWithLimits(json, len, sandbox->json_limits);
PushJsonValue(L, parsed);
return 1;
} catch (const JsonDepthError&) {
return luaL_error(L, "JSON nesting too deep");
} catch (const JsonSizeError&) {
return luaL_error(L, "JSON element too large");
}
}
Lua API:
local data = json.decode(json_string) -- Safe parsing
local str = json.encode(table) -- Safe encoding
Cryptographic APIs
Status: Planned
Provide safe crypto primitives (apps shouldn't implement their own).
void SetupCryptoAPI(lua_State* L) {
lua_newtable(L);
// crypto.randomBytes(n) - cryptographically secure random
lua_pushcfunction(L, [](lua_State* L) -> int {
int n = luaL_checkinteger(L, 1);
if (n < 1 || n > 1024) {
return luaL_error(L, "invalid byte count (1-1024)");
}
std::vector<uint8_t> bytes(n);
if (!CSPRNG(bytes.data(), n)) {
return luaL_error(L, "random generation failed");
}
lua_pushlstring(L, reinterpret_cast<char*>(bytes.data()), n);
return 1;
});
lua_setfield(L, -2, "randomBytes");
// crypto.hash(algorithm, data) - SHA-256, SHA-512
lua_pushcfunction(L, [](lua_State* L) -> int {
const char* algo = luaL_checkstring(L, 1);
size_t len;
const char* data = luaL_checklstring(L, 2, &len);
if (strcmp(algo, "sha256") == 0) {
auto hash = SHA256(data, len);
lua_pushlstring(L, hash.data(), hash.size());
return 1;
} else if (strcmp(algo, "sha512") == 0) {
auto hash = SHA512(data, len);
lua_pushlstring(L, hash.data(), hash.size());
return 1;
}
return luaL_error(L, "unknown algorithm: %s", algo);
});
lua_setfield(L, -2, "hash");
// crypto.hmac(algorithm, key, data)
// crypto.encrypt(algorithm, key, iv, plaintext) - AES-256-GCM only
// crypto.decrypt(algorithm, key, iv, ciphertext)
lua_setglobal(L, "crypto");
}
Random Number Security
Status: Planned
math.random() uses a per-app seed to prevent cross-app prediction.
void SetupSafeRandom(lua_State* L) {
auto* sandbox = GetSandbox(L);
// Seed with cryptographic random + app ID hash
uint64_t seed;
CSPRNG(&seed, sizeof(seed));
seed ^= std::hash<std::string>{}(sandbox->app_id);
// Store RNG state in registry
auto* rng = new std::mt19937_64(seed);
lua_pushlightuserdata(L, rng);
lua_setfield(L, LUA_REGISTRYINDEX, "__rng");
// Replace math.random
lua_getglobal(L, "math");
lua_pushcfunction(L, [](lua_State* L) -> int {
lua_getfield(L, LUA_REGISTRYINDEX, "__rng");
auto* rng = static_cast<std::mt19937_64*>(lua_touserdata(L, -1));
lua_pop(L, 1);
int nargs = lua_gettop(L);
if (nargs == 0) {
// [0, 1)
std::uniform_real_distribution<double> dist(0.0, 1.0);
lua_pushnumber(L, dist(*rng));
} else if (nargs == 1) {
// [1, n]
int n = luaL_checkinteger(L, 1);
std::uniform_int_distribution<int> dist(1, n);
lua_pushinteger(L, dist(*rng));
} else {
// [m, n]
int m = luaL_checkinteger(L, 1);
int n = luaL_checkinteger(L, 2);
std::uniform_int_distribution<int> dist(m, n);
lua_pushinteger(L, dist(*rng));
}
return 1;
});
lua_setfield(L, -2, "random");
// Remove math.randomseed (prevent manipulation)
lua_pushnil(L);
lua_setfield(L, -2, "randomseed");
lua_pop(L, 1);
}
Environment Fingerprinting Prevention
Status: Planned
Limit information leakage about device/user.
void SetupEnvironmentAPI(lua_State* L) {
lua_newtable(L);
// Only expose safe, non-identifying info
lua_pushstring(L, "1.0.0");
lua_setfield(L, -2, "mosis_version");
lua_pushinteger(L, 540);
lua_setfield(L, -2, "screen_width");
lua_pushinteger(L, 960);
lua_setfield(L, -2, "screen_height");
// NOT exposed:
// - Device model
// - OS version
// - Unique identifiers
// - Installed apps list
// - Precise time (only second precision)
// - Memory/CPU info
// - Network interfaces
lua_setglobal(L, "env");
}
// Time with reduced precision
int SafeTime(lua_State* L) {
// Round to seconds (no milliseconds)
time_t now = time(nullptr);
lua_pushinteger(L, static_cast<lua_Integer>(now));
return 1;
}
Rate Limiting
Status: Planned
Prevent abuse of expensive operations.
class RateLimiter {
public:
struct Limit {
int max_calls;
std::chrono::milliseconds window;
};
bool Check(const std::string& operation) {
auto now = std::chrono::steady_clock::now();
auto& bucket = m_buckets[operation];
// Remove old entries
while (!bucket.empty() &&
now - bucket.front() > m_limits[operation].window) {
bucket.pop_front();
}
// Check limit
if (bucket.size() >= m_limits[operation].max_calls) {
return false;
}
bucket.push_back(now);
return true;
}
private:
std::unordered_map<std::string, Limit> m_limits = {
{"network.request", {100, std::chrono::minutes(1)}},
{"storage.write", {1000, std::chrono::minutes(1)}},
{"crypto.hash", {1000, std::chrono::seconds(1)}},
{"timer.create", {100, std::chrono::seconds(1)}},
};
std::unordered_map<std::string, std::deque<TimePoint>> m_buckets;
};
// Usage
int NetworkRequest(lua_State* L) {
auto* sandbox = GetSandbox(L);
if (!sandbox->rate_limiter.Check("network.request")) {
return luaL_error(L, "rate limit exceeded");
}
// ... proceed with request
}
Resource Cleanup on Termination
Status: Planned
Ensure all resources are released when app stops.
class LuaSandbox {
public:
~LuaSandbox() {
Cleanup();
}
void Cleanup() {
// Cancel all timers
m_timer_manager->CancelAll(m_context.app_id);
// Close all network connections
m_network_manager->CloseAll(m_context.app_id);
// Release all file handles
m_file_manager->CloseAll(m_context.app_id);
// Clear message queue
m_message_bus->ClearPending(m_context.app_id);
// Release Lua callbacks
for (int ref : m_callback_refs) {
luaL_unref(L, LUA_REGISTRYINDEX, ref);
}
// Close Lua state (releases memory)
if (L) {
lua_close(L);
L = nullptr;
}
// Log cleanup
AuditLog::Log({
.app_id = m_context.app_id,
.event = AuditEvent::AppStop,
.detail = "cleanup completed"
});
}
private:
std::vector<int> m_callback_refs;
};
App Signature Verification
Status: Planned
Verify app packages haven't been tampered with.
struct AppSignature {
std::string algorithm; // "ed25519"
std::string public_key; // Base64
std::string signature; // Base64
};
class PackageVerifier {
public:
bool Verify(const std::string& mpkg_path) {
// Read manifest
auto manifest = ReadManifest(mpkg_path);
if (!manifest) return false;
// Check signature exists
if (!manifest->signature) {
// Unsigned packages allowed in dev mode only
return IsDevMode();
}
// Get developer's public key from store/registry
auto pub_key = GetDeveloperKey(manifest->author_id);
if (!pub_key) return false;
// Compute package hash (all files except signature)
auto hash = ComputePackageHash(mpkg_path);
// Verify signature
return VerifyEd25519(
pub_key->data(),
hash.data(), hash.size(),
Base64Decode(manifest->signature->signature)
);
}
private:
std::vector<uint8_t> ComputePackageHash(const std::string& path) {
// Hash all files in deterministic order
SHA256_CTX ctx;
SHA256_Init(&ctx);
for (const auto& file : ListFilesRecursive(path)) {
if (file.name == "signature.json") continue;
// Include filename in hash
SHA256_Update(&ctx, file.name.data(), file.name.size());
// Include content
auto content = ReadFile(file.path);
SHA256_Update(&ctx, content.data(), content.size());
}
std::vector<uint8_t> hash(32);
SHA256_Final(hash.data(), &ctx);
return hash;
}
};
Debug Mode Restrictions
Status: Planned
Development features disabled in production.
class DebugMode {
public:
static bool IsEnabled() {
#ifdef NDEBUG
return false;
#else
return s_debug_enabled;
#endif
}
// Only available in debug mode
static void EnableHotReload(LuaSandbox* sandbox) {
if (!IsEnabled()) return;
// ...
}
static void EnableInspector(LuaSandbox* sandbox) {
if (!IsEnabled()) return;
// ...
}
// Log debug state for security audit
static void LogDebugState() {
if (IsEnabled()) {
Logger::Warn("[SECURITY] Debug mode enabled - "
"sandbox restrictions relaxed");
}
}
private:
static bool s_debug_enabled;
};
// In production builds, these are completely compiled out
#ifdef NDEBUG
#define DEBUG_ONLY(x) ((void)0)
#else
#define DEBUG_ONLY(x) x
#endif
Clipboard Isolation
Status: Planned
Apps can only access clipboard with permission and user gesture.
class ClipboardManager {
public:
// Requires permission + recent user gesture
std::optional<std::string> Read(LuaSandbox* sandbox) {
if (!sandbox->HasPermission("clipboard.read")) {
return std::nullopt;
}
// Must be within 1 second of user gesture (click/tap)
if (!sandbox->HasRecentUserGesture(1000)) {
Logger::Warn("Clipboard read blocked - no recent user gesture");
return std::nullopt;
}
return GetSystemClipboard();
}
bool Write(LuaSandbox* sandbox, const std::string& text) {
if (!sandbox->HasPermission("clipboard.write")) {
return false;
}
// Size limit
if (text.size() > 1024 * 1024) {
return false;
}
SetSystemClipboard(text);
return true;
}
};
Stack Depth Limiting
Status: Planned
Prevent stack overflow via deep recursion.
void SetupStackLimit(lua_State* L, int max_depth) {
// Set C stack limit
// Note: Lua 5.4 has LUAI_MAXCSTACK
// Also use debug hook to check Lua call depth
lua_sethook(L, [](lua_State* L, lua_Debug* ar) {
lua_Debug info;
int depth = 0;
while (lua_getstack(L, depth, &info)) {
depth++;
}
auto* sandbox = GetSandbox(L);
if (depth > sandbox->m_limits.stack_depth) {
luaL_error(L, "stack overflow (depth %d)", depth);
}
}, LUA_MASKCALL, 0);
}
Security Testing Checklist
Before Release
- All dangerous globals removed
- Bytecode loading blocked
- Memory limits enforced
- CPU limits enforced
- Path traversal blocked
- Metatable protection working
- Permission checks on all APIs
- Network domain filtering working
- Rate limiting active
- Audit logging enabled
- Debug mode disabled in release
- Fuzzing completed with no crashes
- Security audit by external party
Per-App Review (Store Submission)
- Manifest permissions justified
- No obfuscated code
- Network domains declared
- Package signature valid
- No known vulnerability patterns
- Resource usage reasonable
Virtual Hardware Interfaces
Mosis provides virtualized hardware interfaces that apps can access through permission-gated APIs. Each interface has specific security requirements based on the sensitivity of the data it exposes.
Hardware Permission Model
┌─────────────────────────────────────────────────────────────────────┐
│ Permission Categories │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ NORMAL (auto-granted) DANGEROUS (user prompt required) │
│ ├── storage.app ├── camera │
│ ├── network.metadata ├── microphone │
│ ├── system.vibrate ├── location.fine │
│ └── audio.playback ├── location.coarse │
│ ├── contacts.read │
│ SIGNATURE (system apps only) ├── contacts.write │
│ ├── system.settings ├── storage.shared │
│ ├── app.install ├── network.internet │
│ └── hardware.control ├── bluetooth │
│ ├── phone.call │
│ ├── sms.send │
│ └── sensors.body │
│ │
└─────────────────────────────────────────────────────────────────────┘
Hardware Security Threat Model
| Interface | Threat | Impact | Mitigation |
|---|---|---|---|
| Camera | Silent recording | Critical privacy | Indicator, gesture gate |
| Microphone | Eavesdropping | Critical privacy | Indicator, gesture gate |
| Location | Tracking | High privacy | Precision reduction, rate limit |
| Network | Data exfiltration | High privacy | Domain filtering, inspection |
| Filesystem | Data theft/corruption | High integrity | Path sandboxing |
| Database | SQL injection | High integrity | Prepared statements only |
| Bluetooth | Device tracking | Medium privacy | Pairing consent |
| Sensors | Fingerprinting | Medium privacy | Reduced precision |
| Speaker | Audio spam | Low annoyance | Volume limits, rate limit |
Network Interface Security
Network Sandbox Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ App Lua Code │
│ │ │
│ network.request(url, opts) │
│ │ │
├──────────────────────────────▼──────────────────────────────────────┤
│ NetworkSandbox │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │
│ │ Permission │ │ Domain │ │ Content │ │
│ │ Check │→ │ Validation │→ │ Inspection │ │
│ └─────────────┘ └──────────────┘ └────────────────────────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ Rate Limiter │ │
│ └─────────┬──────────┘ │
│ │ │
├──────────────────────────────▼──────────────────────────────────────┤
│ System HTTP Client │
│ (libcurl / Android HttpClient) │
└─────────────────────────────────────────────────────────────────────┘
HTTP Request Security
Status: Designed
struct HttpRequestPolicy {
// Domain restrictions
std::vector<std::string> allowed_domains; // Whitelist from manifest
std::vector<std::string> blocked_domains; // System blacklist
bool allow_localhost = false; // Block 127.0.0.1, ::1
bool allow_private_ips = false; // Block 10.x, 192.168.x, etc.
bool allow_metadata_ips = false; // Block 169.254.x (cloud metadata)
// Protocol restrictions
bool require_https = true; // Block plain HTTP
std::vector<std::string> allowed_schemes = {"https"};
// Size limits
size_t max_request_body = 10 * 1024 * 1024; // 10 MB
size_t max_response_body = 50 * 1024 * 1024; // 50 MB
size_t max_header_size = 64 * 1024; // 64 KB
// Timeout
int connect_timeout_ms = 10000;
int read_timeout_ms = 30000;
int total_timeout_ms = 60000;
// Rate limits
int max_concurrent_requests = 6;
int max_requests_per_minute = 100;
};
class HttpRequestValidator {
public:
enum class Result {
Allowed,
PermissionDenied,
DomainBlocked,
PrivateIPBlocked,
MetadataIPBlocked,
LocalhostBlocked,
InsecureProtocol,
RequestTooLarge,
RateLimitExceeded
};
Result Validate(const HttpRequest& req, const AppContext& ctx) {
// 1. Permission check
if (!ctx.HasPermission("network.internet")) {
return Result::PermissionDenied;
}
// 2. Parse URL
auto url = ParseUrl(req.url);
if (!url) return Result::DomainBlocked;
// 3. Protocol check
if (m_policy.require_https && url->scheme != "https") {
return Result::InsecureProtocol;
}
// 4. Resolve hostname to IP (detect IP-based bypass)
auto ips = ResolveHostname(url->host);
// 5. Block private IPs
for (const auto& ip : ips) {
if (IsPrivateIP(ip) && !m_policy.allow_private_ips) {
return Result::PrivateIPBlocked;
}
if (IsMetadataIP(ip) && !m_policy.allow_metadata_ips) {
return Result::MetadataIPBlocked;
}
if (IsLocalhost(ip) && !m_policy.allow_localhost) {
return Result::LocalhostBlocked;
}
}
// 6. Domain whitelist/blacklist
if (!m_policy.allowed_domains.empty()) {
if (!MatchesDomainList(url->host, m_policy.allowed_domains)) {
return Result::DomainBlocked;
}
}
if (MatchesDomainList(url->host, m_policy.blocked_domains)) {
return Result::DomainBlocked;
}
// 7. Size check
if (req.body.size() > m_policy.max_request_body) {
return Result::RequestTooLarge;
}
// 8. Rate limit
if (!m_rate_limiter.Check(ctx.app_id, "http_request")) {
return Result::RateLimitExceeded;
}
return Result::Allowed;
}
private:
bool IsPrivateIP(const std::string& ip) {
// IPv4 private ranges
if (ip.starts_with("10.")) return true;
if (ip.starts_with("172.")) {
int second_octet = std::stoi(ip.substr(4, ip.find('.', 4) - 4));
if (second_octet >= 16 && second_octet <= 31) return true;
}
if (ip.starts_with("192.168.")) return true;
// Link-local
if (ip.starts_with("169.254.")) return true;
// IPv6 private ranges
if (ip.starts_with("fc") || ip.starts_with("fd")) return true; // ULA
if (ip.starts_with("fe80:")) return true; // Link-local
return false;
}
bool IsMetadataIP(const std::string& ip) {
// AWS/GCP/Azure metadata endpoints
return ip == "169.254.169.254" ||
ip == "metadata.google.internal" ||
ip.starts_with("fd00:ec2::");
}
};
WebSocket Security
Status: Designed
struct WebSocketPolicy {
bool enabled = true;
int max_connections_per_app = 5;
size_t max_message_size = 1 * 1024 * 1024; // 1 MB
int ping_interval_ms = 30000;
int idle_timeout_ms = 300000; // 5 minutes
// Same domain validation as HTTP
std::vector<std::string> allowed_origins;
};
class WebSocketManager {
public:
struct Connection {
uint32_t id;
std::string app_id;
std::string url;
ConnectionState state;
std::chrono::steady_clock::time_point created_at;
std::chrono::steady_clock::time_point last_activity;
size_t bytes_sent = 0;
size_t bytes_received = 0;
};
std::optional<uint32_t> Connect(LuaSandbox* sandbox, const std::string& url) {
// Permission check
if (!sandbox->HasPermission("network.websocket")) {
return std::nullopt;
}
// Validate URL (same as HTTP)
auto result = m_validator.Validate({.url = url}, sandbox->context());
if (result != HttpRequestValidator::Result::Allowed) {
return std::nullopt;
}
// Connection limit
if (CountAppConnections(sandbox->app_id()) >= m_policy.max_connections_per_app) {
return std::nullopt;
}
// Create connection
auto conn = CreateConnection(url);
conn.app_id = sandbox->app_id();
m_connections[conn.id] = conn;
return conn.id;
}
bool Send(LuaSandbox* sandbox, uint32_t conn_id, const std::string& data) {
auto it = m_connections.find(conn_id);
if (it == m_connections.end()) return false;
// Verify ownership
if (it->second.app_id != sandbox->app_id()) return false;
// Size limit
if (data.size() > m_policy.max_message_size) return false;
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "ws_send")) return false;
// Send
it->second.bytes_sent += data.size();
it->second.last_activity = std::chrono::steady_clock::now();
return DoSend(conn_id, data);
}
void CloseAll(const std::string& app_id) {
// Called when app stops
for (auto it = m_connections.begin(); it != m_connections.end(); ) {
if (it->second.app_id == app_id) {
DoClose(it->first);
it = m_connections.erase(it);
} else {
++it;
}
}
}
private:
std::unordered_map<uint32_t, Connection> m_connections;
WebSocketPolicy m_policy;
HttpRequestValidator m_validator;
RateLimiter m_rate_limiter;
};
Network Lua API
-- HTTP Requests (requires "network.internet" permission)
local response = network.request({
url = "https://api.example.com/data",
method = "POST", -- GET, POST, PUT, DELETE, PATCH
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. token
},
body = json.encode({key = "value"}),
timeout = 30000 -- milliseconds
})
-- Response structure
-- response.status: number (200, 404, etc.)
-- response.headers: table
-- response.body: string
-- response.error: string (nil if success)
-- WebSocket (requires "network.websocket" permission)
local ws = network.websocket("wss://api.example.com/ws")
ws:on("open", function()
ws:send(json.encode({type = "hello"}))
end)
ws:on("message", function(data)
local msg = json.decode(data)
print("Received:", msg.type)
end)
ws:on("close", function(code, reason)
print("Closed:", code, reason)
end)
ws:on("error", function(err)
print("Error:", err)
end)
ws:close()
Manifest Declaration
{
"permissions": ["network.internet", "network.websocket"],
"network": {
"allowed_domains": [
"api.example.com",
"*.cdn.example.com",
"wss://realtime.example.com"
],
"allow_http": false,
"max_connections": 10
}
}
Filesystem Interface Security
Virtual Filesystem Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Physical Storage │
│ /data/data/com.omixlab.mosis/ │
│ ├── apps/ │
│ │ ├── com.example.app1/ │
│ │ │ ├── data/ ← App-private storage │
│ │ │ ├── cache/ ← Clearable cache │
│ │ │ └── temp/ ← Session-only │
│ │ └── com.example.app2/ │
│ ├── shared/ │
│ │ ├── photos/ ← Shared media (permission required) │
│ │ ├── downloads/ │
│ │ └── documents/ │
│ └── system/ ← System apps only │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────▼───────┐
│ VirtualFS API │
└───────┬───────┘
│
┌───────────────────────────────────▼─────────────────────────────────┐
│ App's Virtual View │
│ / │
│ ├── data/ → /apps/<app_id>/data/ (auto-granted) │
│ ├── cache/ → /apps/<app_id>/cache/ (auto-granted) │
│ ├── temp/ → /apps/<app_id>/temp/ (auto-granted) │
│ ├── shared/ → /shared/ (permission required) │
│ └── (nothing else visible) │
└─────────────────────────────────────────────────────────────────────┘
Filesystem Security Implementation
Status: Designed
struct FilesystemPolicy {
// Storage quotas
size_t max_app_storage = 100 * 1024 * 1024; // 100 MB per app
size_t max_cache_storage = 50 * 1024 * 1024; // 50 MB cache
size_t max_file_size = 50 * 1024 * 1024; // 50 MB single file
// File limits
size_t max_files_per_directory = 10000;
size_t max_path_depth = 32;
size_t max_filename_length = 255;
// Allowed extensions (whitelist approach)
std::vector<std::string> allowed_extensions = {
".txt", ".json", ".xml", ".html", ".css", ".js", ".lua",
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg",
".mp3", ".ogg", ".wav",
".mp4", ".webm",
".db", ".sqlite"
};
// Blocked patterns
std::vector<std::string> blocked_patterns = {
"*.exe", "*.dll", "*.so", "*.dylib", // Executables
"*.sh", "*.bat", "*.cmd", "*.ps1", // Scripts
".*", // Hidden files
};
};
class VirtualFilesystem {
public:
VirtualFilesystem(const std::string& app_id, const std::string& base_path)
: m_app_id(app_id)
, m_base_path(base_path)
, m_app_root(base_path + "/apps/" + app_id) {}
// Resolve virtual path to physical path with security checks
std::optional<std::string> ResolvePath(const std::string& virtual_path,
AccessMode mode) {
// 1. Validate path format
if (!IsValidPathFormat(virtual_path)) {
AuditLog::Log({m_app_id, AuditEvent::FileAccess,
"invalid path format: " + virtual_path, false});
return std::nullopt;
}
// 2. Parse virtual path
auto parts = SplitPath(virtual_path);
if (parts.empty()) return std::nullopt;
// 3. Determine physical root based on virtual root
std::string physical_root;
bool requires_permission = false;
if (parts[0] == "data") {
physical_root = m_app_root + "/data";
} else if (parts[0] == "cache") {
physical_root = m_app_root + "/cache";
} else if (parts[0] == "temp") {
physical_root = m_app_root + "/temp";
} else if (parts[0] == "shared") {
physical_root = m_base_path + "/shared";
requires_permission = true;
} else {
// Unknown root - access denied
return std::nullopt;
}
// 4. Permission check for shared storage
if (requires_permission) {
std::string perm = (mode == AccessMode::Read)
? "storage.shared.read"
: "storage.shared.write";
if (!HasPermission(perm)) {
AuditLog::Log({m_app_id, AuditEvent::PermissionDenied, perm, false});
return std::nullopt;
}
}
// 5. Build full path
std::filesystem::path full_path = physical_root;
for (size_t i = 1; i < parts.size(); ++i) {
full_path /= parts[i];
}
// 6. Canonicalize and verify containment
std::filesystem::path canonical;
try {
if (mode == AccessMode::Read) {
// Must exist for read
canonical = std::filesystem::canonical(full_path);
} else {
// May not exist for write - canonicalize parent
canonical = std::filesystem::weakly_canonical(full_path);
}
} catch (const std::filesystem::filesystem_error&) {
return std::nullopt;
}
// 7. Verify path is within allowed root (prevent traversal)
std::string canonical_str = canonical.string();
if (!canonical_str.starts_with(physical_root)) {
AuditLog::Log({m_app_id, AuditEvent::SandboxViolation,
"path traversal attempt: " + virtual_path, false});
return std::nullopt;
}
// 8. Check file extension
if (!IsAllowedExtension(canonical.extension().string())) {
return std::nullopt;
}
return canonical_str;
}
// Quota enforcement
bool HasStorageQuota(size_t bytes_needed) {
size_t current = CalculateDirectorySize(m_app_root + "/data");
return (current + bytes_needed) <= m_policy.max_app_storage;
}
std::optional<size_t> GetUsedStorage() {
return CalculateDirectorySize(m_app_root + "/data");
}
std::optional<size_t> GetAvailableStorage() {
auto used = GetUsedStorage();
if (!used) return std::nullopt;
return m_policy.max_app_storage - *used;
}
private:
bool IsValidPathFormat(const std::string& path) {
// Must start with /
if (path.empty() || path[0] != '/') return false;
// No .. components
if (path.find("..") != std::string::npos) return false;
// No double slashes
if (path.find("//") != std::string::npos) return false;
// No null bytes
if (path.find('\0') != std::string::npos) return false;
// Length check
if (path.length() > 4096) return false;
// Check each component
for (const auto& part : SplitPath(path)) {
if (part.length() > m_policy.max_filename_length) return false;
if (part.empty()) return false;
if (MatchesBlockedPattern(part)) return false;
}
return true;
}
bool IsAllowedExtension(const std::string& ext) {
if (ext.empty()) return true; // Directories
std::string lower_ext = ToLower(ext);
return std::find(m_policy.allowed_extensions.begin(),
m_policy.allowed_extensions.end(),
lower_ext) != m_policy.allowed_extensions.end();
}
std::string m_app_id;
std::string m_base_path;
std::string m_app_root;
FilesystemPolicy m_policy;
};
File Operations with Security Gates
class SecureFileAPI {
public:
// Read file
std::optional<std::string> ReadFile(LuaSandbox* sandbox,
const std::string& path) {
auto physical = m_vfs.ResolvePath(path, AccessMode::Read);
if (!physical) return std::nullopt;
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "file_read")) {
return std::nullopt;
}
// Read with size limit
std::ifstream file(*physical, std::ios::binary);
if (!file) return std::nullopt;
file.seekg(0, std::ios::end);
size_t size = file.tellg();
if (size > m_policy.max_file_size) return std::nullopt;
file.seekg(0, std::ios::beg);
std::string content(size, '\0');
file.read(content.data(), size);
AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess,
"read: " + path + " (" + std::to_string(size) + " bytes)", true});
return content;
}
// Write file
bool WriteFile(LuaSandbox* sandbox, const std::string& path,
const std::string& content) {
auto physical = m_vfs.ResolvePath(path, AccessMode::Write);
if (!physical) return false;
// Size check
if (content.size() > m_policy.max_file_size) return false;
// Quota check
if (!m_vfs.HasStorageQuota(content.size())) {
AuditLog::Log({sandbox->app_id(), AuditEvent::ResourceLimitHit,
"storage quota exceeded", false});
return false;
}
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "file_write")) {
return false;
}
// Create parent directories
std::filesystem::create_directories(
std::filesystem::path(*physical).parent_path());
// Write atomically (temp file + rename)
std::string temp_path = *physical + ".tmp." + GenerateRandomId();
{
std::ofstream file(temp_path, std::ios::binary);
if (!file) return false;
file.write(content.data(), content.size());
}
std::error_code ec;
std::filesystem::rename(temp_path, *physical, ec);
if (ec) {
std::filesystem::remove(temp_path);
return false;
}
AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess,
"write: " + path + " (" + std::to_string(content.size()) + " bytes)",
true});
return true;
}
// List directory
std::optional<std::vector<FileInfo>> ListDirectory(LuaSandbox* sandbox,
const std::string& path) {
auto physical = m_vfs.ResolvePath(path, AccessMode::Read);
if (!physical) return std::nullopt;
std::vector<FileInfo> entries;
for (const auto& entry : std::filesystem::directory_iterator(*physical)) {
if (entries.size() >= m_policy.max_files_per_directory) break;
FileInfo info;
info.name = entry.path().filename().string();
info.is_directory = entry.is_directory();
info.size = entry.is_regular_file() ? entry.file_size() : 0;
info.modified = GetModificationTime(entry.path());
entries.push_back(info);
}
return entries;
}
// Delete file
bool Delete(LuaSandbox* sandbox, const std::string& path) {
auto physical = m_vfs.ResolvePath(path, AccessMode::Write);
if (!physical) return false;
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "file_delete")) {
return false;
}
std::error_code ec;
bool removed = std::filesystem::remove(*physical, ec);
AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess,
"delete: " + path, removed});
return removed;
}
private:
VirtualFilesystem m_vfs;
FilesystemPolicy m_policy;
RateLimiter m_rate_limiter;
};
Filesystem Lua API
-- File operations (app-private storage is auto-granted)
local content = storage.read("/data/config.json")
local success = storage.write("/data/config.json", json.encode(config))
-- Directory operations
local files = storage.list("/data/saves/")
for _, file in ipairs(files) do
print(file.name, file.size, file.is_directory)
end
-- File info
local info = storage.info("/data/save.json")
-- info.exists, info.size, info.modified, info.is_directory
-- Delete
storage.delete("/data/old_file.json")
-- Create directory
storage.mkdir("/data/saves/slot1/")
-- Check storage quota
local quota = storage.quota()
-- quota.used, quota.available, quota.total
-- Shared storage (requires "storage.shared.read" / "storage.shared.write")
local photos = storage.list("/shared/photos/")
storage.write("/shared/documents/report.txt", "Hello")
Database Interface Security
SQLite Sandbox
Status: Designed
Apps can use SQLite databases with security restrictions.
struct DatabasePolicy {
size_t max_database_size = 50 * 1024 * 1024; // 50 MB
size_t max_databases_per_app = 5;
size_t max_query_time_ms = 5000;
size_t max_result_rows = 10000;
bool allow_attach = false; // Prevent ATTACH to other DBs
bool allow_load_extension = false; // No extensions
};
class SecureDatabase {
public:
SecureDatabase(const std::string& app_id, const std::string& db_name)
: m_app_id(app_id)
, m_db_name(db_name) {
// Validate database name
if (!IsValidDatabaseName(db_name)) {
throw std::invalid_argument("invalid database name");
}
// Open database in app's data directory
std::string path = GetAppDataPath(app_id) + "/databases/" + db_name + ".db";
int rc = sqlite3_open_v2(path.c_str(), &m_db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
SQLITE_OPEN_NOMUTEX, // Single-threaded access per app
nullptr);
if (rc != SQLITE_OK) {
throw std::runtime_error("failed to open database");
}
// Security configuration
ConfigureSecurity();
}
~SecureDatabase() {
if (m_db) sqlite3_close(m_db);
}
// Execute query with prepared statement (prevents SQL injection)
QueryResult Execute(const std::string& sql,
const std::vector<SqlValue>& params) {
// 1. Validate SQL (basic sanity checks)
if (!ValidateSql(sql)) {
return {.error = "invalid SQL"};
}
// 2. Prepare statement
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK) {
return {.error = sqlite3_errmsg(m_db)};
}
// 3. Bind parameters
for (size_t i = 0; i < params.size(); ++i) {
BindParameter(stmt, i + 1, params[i]);
}
// 4. Execute with timeout
QueryResult result;
auto start = std::chrono::steady_clock::now();
while (true) {
rc = sqlite3_step(stmt);
// Check timeout
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed > std::chrono::milliseconds(m_policy.max_query_time_ms)) {
sqlite3_finalize(stmt);
return {.error = "query timeout"};
}
if (rc == SQLITE_ROW) {
// Check row limit
if (result.rows.size() >= m_policy.max_result_rows) {
result.truncated = true;
break;
}
result.rows.push_back(ExtractRow(stmt));
} else if (rc == SQLITE_DONE) {
break;
} else {
result.error = sqlite3_errmsg(m_db);
break;
}
}
result.changes = sqlite3_changes(m_db);
result.last_insert_id = sqlite3_last_insert_rowid(m_db);
sqlite3_finalize(stmt);
return result;
}
private:
void ConfigureSecurity() {
// Disable dangerous features
sqlite3_db_config(m_db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, nullptr);
// Set authorizer to block dangerous operations
sqlite3_set_authorizer(m_db, Authorizer, this);
// Set database size limit
sqlite3_soft_heap_limit64(m_policy.max_database_size);
// Set busy timeout
sqlite3_busy_timeout(m_db, 1000);
}
static int Authorizer(void* user_data, int action, const char* arg1,
const char* arg2, const char* arg3, const char* arg4) {
auto* self = static_cast<SecureDatabase*>(user_data);
switch (action) {
case SQLITE_ATTACH:
case SQLITE_DETACH:
// Block ATTACH/DETACH to prevent accessing other databases
return SQLITE_DENY;
case SQLITE_PRAGMA:
// Allow only safe pragmas
if (arg1 && !IsSafePragma(arg1)) {
return SQLITE_DENY;
}
break;
case SQLITE_FUNCTION:
// Block dangerous functions
if (arg2 && IsDangerousFunction(arg2)) {
return SQLITE_DENY;
}
break;
}
return SQLITE_OK;
}
static bool IsSafePragma(const char* pragma) {
// Whitelist of safe pragmas
static const std::unordered_set<std::string> safe = {
"table_info", "index_list", "foreign_keys",
"journal_mode", "synchronous", "cache_size"
};
return safe.count(pragma) > 0;
}
static bool IsDangerousFunction(const char* func) {
// Block functions that can access filesystem or execute code
static const std::unordered_set<std::string> dangerous = {
"load_extension", "readfile", "writefile", "edit",
"fts3_tokenizer", "sqlite_compileoption_get"
};
return dangerous.count(func) > 0;
}
bool ValidateSql(const std::string& sql) {
std::string upper = ToUpper(sql);
// Block dangerous statements
if (upper.find("ATTACH") != std::string::npos) return false;
if (upper.find("DETACH") != std::string::npos) return false;
if (upper.find("LOAD_EXTENSION") != std::string::npos) return false;
return true;
}
sqlite3* m_db = nullptr;
std::string m_app_id;
std::string m_db_name;
DatabasePolicy m_policy;
};
Database Lua API
-- Open database (stored in /data/databases/)
local db = database.open("myapp")
-- Create table
db:execute([[
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
]])
-- Insert with parameters (SQL injection safe)
db:execute("INSERT INTO users (name, email) VALUES (?, ?)", {"John", "john@example.com"})
-- Query with parameters
local result = db:query("SELECT * FROM users WHERE name LIKE ?", {"%John%"})
for _, row in ipairs(result.rows) do
print(row.id, row.name, row.email)
end
-- Transaction
db:transaction(function()
db:execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", {100, 1})
db:execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", {100, 2})
end)
-- Close
db:close()
Camera Interface Security
Camera Security Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ App Lua Code │
│ camera.capture(callback) │
│ │ │
├──────────────────────────────▼──────────────────────────────────────┤
│ CameraManager │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │
│ │ Permission │ │ Active │ │ Frame Rate │ │
│ │ Check │→ │ Indicator │→ │ Limiter │ │
│ └─────────────┘ └──────────────┘ └────────────────────────────┘ │
│ │ │
├──────────────────────────────▼──────────────────────────────────────┤
│ Game Engine Camera │
│ (Unity RenderTexture / Unreal SceneCapture) │
└─────────────────────────────────────────────────────────────────────┘
Camera Security Implementation
Status: Designed
struct CameraPolicy {
// Access control
bool require_user_gesture = true; // Must be triggered by tap/click
int max_active_sessions = 1; // Only one app can use camera
// Frame limits
int max_fps = 30;
int max_resolution_width = 1920;
int max_resolution_height = 1080;
// Privacy
bool show_indicator = true; // Always show recording indicator
int indicator_min_duration_ms = 500; // Indicator shows at least 500ms
bool allow_silent_capture = false; // Require shutter sound
// Storage
bool auto_save_to_gallery = false; // App must explicitly save
std::string watermark_text = ""; // Optional watermark
};
class CameraManager {
public:
struct CameraSession {
uint32_t id;
std::string app_id;
CameraFacing facing;
Resolution resolution;
bool active;
std::chrono::steady_clock::time_point started_at;
};
// Start camera session
std::optional<uint32_t> StartSession(LuaSandbox* sandbox,
const CameraOptions& opts) {
// 1. Permission check
if (!sandbox->HasPermission("camera")) {
AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied,
"camera", false});
return std::nullopt;
}
// 2. User gesture check
if (m_policy.require_user_gesture && !sandbox->HasRecentUserGesture(1000)) {
AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess,
"blocked - no user gesture", false});
return std::nullopt;
}
// 3. Check exclusive access
if (HasActiveSession() && m_active_session->app_id != sandbox->app_id()) {
return std::nullopt; // Another app is using camera
}
// 4. Validate resolution
Resolution res = opts.resolution;
res.width = std::min(res.width, m_policy.max_resolution_width);
res.height = std::min(res.height, m_policy.max_resolution_height);
// 5. Start session
CameraSession session{
.id = ++m_next_session_id,
.app_id = sandbox->app_id(),
.facing = opts.facing,
.resolution = res,
.active = true,
.started_at = std::chrono::steady_clock::now()
};
m_active_session = session;
// 6. Show indicator
if (m_policy.show_indicator) {
ShowCameraIndicator(true);
}
// 7. Request frames from game engine
m_hardware_camera->Start(res, [this, session_id = session.id](Frame frame) {
OnFrameReceived(session_id, frame);
});
AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess,
"session started", true});
return session.id;
}
// Capture single frame
std::optional<ImageData> CaptureFrame(LuaSandbox* sandbox, uint32_t session_id) {
// Verify session ownership
if (!m_active_session || m_active_session->id != session_id ||
m_active_session->app_id != sandbox->app_id()) {
return std::nullopt;
}
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "camera_capture")) {
return std::nullopt;
}
// Get latest frame
auto frame = m_latest_frame;
if (!frame) return std::nullopt;
// Play shutter sound (if configured)
if (!m_policy.allow_silent_capture) {
PlayShutterSound();
}
// Apply watermark if configured
if (!m_policy.watermark_text.empty()) {
ApplyWatermark(frame, m_policy.watermark_text);
}
return frame;
}
// Stop session
void StopSession(LuaSandbox* sandbox, uint32_t session_id) {
if (!m_active_session || m_active_session->id != session_id ||
m_active_session->app_id != sandbox->app_id()) {
return;
}
m_hardware_camera->Stop();
// Keep indicator visible for minimum duration
auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at;
if (elapsed < std::chrono::milliseconds(m_policy.indicator_min_duration_ms)) {
std::this_thread::sleep_for(
std::chrono::milliseconds(m_policy.indicator_min_duration_ms) - elapsed);
}
ShowCameraIndicator(false);
m_active_session.reset();
AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess,
"session stopped", true});
}
// Stop all sessions for app (called on app termination)
void StopAllSessions(const std::string& app_id) {
if (m_active_session && m_active_session->app_id == app_id) {
m_hardware_camera->Stop();
ShowCameraIndicator(false);
m_active_session.reset();
}
}
private:
void OnFrameReceived(uint32_t session_id, Frame frame) {
if (!m_active_session || m_active_session->id != session_id) {
return;
}
// Apply frame rate limit
auto now = std::chrono::steady_clock::now();
auto elapsed = now - m_last_frame_time;
if (elapsed < std::chrono::milliseconds(1000 / m_policy.max_fps)) {
return;
}
m_last_frame_time = now;
m_latest_frame = frame;
// Notify app (if callback registered)
if (m_frame_callback) {
m_frame_callback(frame);
}
}
std::optional<CameraSession> m_active_session;
std::optional<Frame> m_latest_frame;
std::chrono::steady_clock::time_point m_last_frame_time;
CameraPolicy m_policy;
IHardwareCamera* m_hardware_camera;
RateLimiter m_rate_limiter;
uint32_t m_next_session_id = 0;
};
Camera Lua API
-- Request camera permission (shows system dialog)
local granted = permissions.request("camera")
if not granted then return end
-- Start camera session
local session = camera.start({
facing = "back", -- "back" or "front"
resolution = {width = 1280, height = 720}
})
-- Preview frames
session:onFrame(function(frame)
-- frame.width, frame.height, frame.data (base64 JPEG)
ui.setImage("preview", frame.data)
end)
-- Capture photo
local photo = session:capture()
if photo then
-- Save to app storage
storage.write("/data/photos/photo_" .. os.time() .. ".jpg",
base64.decode(photo.data))
-- Or save to shared gallery (requires storage.shared.write)
storage.write("/shared/photos/MyApp/photo.jpg", base64.decode(photo.data))
end
-- Stop session
session:stop()
Microphone Interface Security
Microphone Security Implementation
Status: Designed
struct MicrophonePolicy {
// Access control
bool require_user_gesture = true;
int max_active_sessions = 1;
// Audio limits
int max_sample_rate = 48000;
int max_duration_ms = 300000; // 5 minutes max recording
int max_buffer_size = 4096;
// Privacy
bool show_indicator = true;
int indicator_min_duration_ms = 500;
bool allow_background_recording = false;
};
class MicrophoneManager {
public:
struct AudioSession {
uint32_t id;
std::string app_id;
int sample_rate;
int channels;
bool active;
std::chrono::steady_clock::time_point started_at;
size_t samples_recorded = 0;
};
std::optional<uint32_t> StartRecording(LuaSandbox* sandbox,
const AudioOptions& opts) {
// 1. Permission check
if (!sandbox->HasPermission("microphone")) {
AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied,
"microphone", false});
return std::nullopt;
}
// 2. User gesture check
if (m_policy.require_user_gesture && !sandbox->HasRecentUserGesture(1000)) {
return std::nullopt;
}
// 3. Exclusive access
if (HasActiveSession() && m_active_session->app_id != sandbox->app_id()) {
return std::nullopt;
}
// 4. Background recording check
if (!m_policy.allow_background_recording && !sandbox->IsAppForeground()) {
return std::nullopt;
}
// 5. Validate options
int sample_rate = std::min(opts.sample_rate, m_policy.max_sample_rate);
AudioSession session{
.id = ++m_next_session_id,
.app_id = sandbox->app_id(),
.sample_rate = sample_rate,
.channels = opts.channels,
.active = true,
.started_at = std::chrono::steady_clock::now()
};
m_active_session = session;
// 6. Show indicator
if (m_policy.show_indicator) {
ShowMicrophoneIndicator(true);
}
// 7. Start hardware recording
m_hardware_mic->Start(sample_rate, opts.channels,
[this, session_id = session.id](AudioBuffer buffer) {
OnAudioReceived(session_id, buffer);
});
AuditLog::Log({sandbox->app_id(), AuditEvent::MicrophoneAccess,
"recording started", true});
return session.id;
}
void StopRecording(LuaSandbox* sandbox, uint32_t session_id) {
if (!m_active_session || m_active_session->id != session_id ||
m_active_session->app_id != sandbox->app_id()) {
return;
}
m_hardware_mic->Stop();
// Minimum indicator duration
auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at;
if (elapsed < std::chrono::milliseconds(m_policy.indicator_min_duration_ms)) {
std::this_thread::sleep_for(
std::chrono::milliseconds(m_policy.indicator_min_duration_ms) - elapsed);
}
ShowMicrophoneIndicator(false);
m_active_session.reset();
AuditLog::Log({sandbox->app_id(), AuditEvent::MicrophoneAccess,
"recording stopped", true});
}
private:
void OnAudioReceived(uint32_t session_id, AudioBuffer buffer) {
if (!m_active_session || m_active_session->id != session_id) {
return;
}
// Check duration limit
auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at;
if (elapsed > std::chrono::milliseconds(m_policy.max_duration_ms)) {
// Auto-stop after max duration
StopRecordingInternal(session_id);
return;
}
m_active_session->samples_recorded += buffer.samples;
// Deliver to app callback
if (m_audio_callback) {
m_audio_callback(buffer);
}
}
std::optional<AudioSession> m_active_session;
MicrophonePolicy m_policy;
IHardwareMicrophone* m_hardware_mic;
uint32_t m_next_session_id = 0;
};
Microphone Lua API
-- Request permission
local granted = permissions.request("microphone")
if not granted then return end
-- Start recording
local session = microphone.start({
sample_rate = 44100,
channels = 1
})
-- Receive audio data
session:onAudio(function(buffer)
-- buffer.samples: number of samples
-- buffer.data: base64 encoded PCM data
end)
-- Stop recording
session:stop()
-- Get recorded audio
local audio = session:getRecording()
storage.write("/data/recordings/voice.wav", audio)
Speaker/Audio Output Security
Audio Output Policy
Status: Designed
struct AudioOutputPolicy {
float max_volume = 1.0f;
int max_concurrent_sounds = 8;
int max_sound_duration_ms = 300000; // 5 minutes
size_t max_audio_data_size = 50 * 1024 * 1024; // 50 MB
bool require_audio_focus = true; // Respect other apps
};
class AudioOutputManager {
public:
std::optional<uint32_t> PlaySound(LuaSandbox* sandbox,
const AudioData& audio,
const PlaybackOptions& opts) {
// No dangerous permission needed for audio output,
// but still track and limit
// 1. Validate audio data
if (audio.size() > m_policy.max_audio_data_size) {
return std::nullopt;
}
// 2. Check concurrent sounds limit
int app_sounds = CountAppSounds(sandbox->app_id());
if (app_sounds >= m_policy.max_concurrent_sounds) {
return std::nullopt;
}
// 3. Clamp volume
float volume = std::clamp(opts.volume, 0.0f, m_policy.max_volume);
// 4. Request audio focus
if (m_policy.require_audio_focus) {
if (!RequestAudioFocus(sandbox->app_id())) {
return std::nullopt;
}
}
// 5. Create sound instance
Sound sound{
.id = ++m_next_id,
.app_id = sandbox->app_id(),
.volume = volume,
.looping = opts.loop,
.started_at = std::chrono::steady_clock::now()
};
m_sounds[sound.id] = sound;
m_hardware_audio->Play(audio, volume, opts.loop);
return sound.id;
}
void StopSound(LuaSandbox* sandbox, uint32_t sound_id) {
auto it = m_sounds.find(sound_id);
if (it == m_sounds.end() || it->second.app_id != sandbox->app_id()) {
return;
}
m_hardware_audio->Stop(sound_id);
m_sounds.erase(it);
}
void StopAllSounds(const std::string& app_id) {
for (auto it = m_sounds.begin(); it != m_sounds.end(); ) {
if (it->second.app_id == app_id) {
m_hardware_audio->Stop(it->first);
it = m_sounds.erase(it);
} else {
++it;
}
}
}
private:
std::unordered_map<uint32_t, Sound> m_sounds;
AudioOutputPolicy m_policy;
IHardwareAudio* m_hardware_audio;
uint32_t m_next_id = 0;
};
Audio Lua API
-- Play sound (no permission required)
local sound = audio.play("/data/sounds/notification.wav", {
volume = 0.8,
loop = false
})
-- Control playback
sound:pause()
sound:resume()
sound:stop()
-- System sounds
audio.playSystem("notification")
audio.playSystem("click")
-- Vibration (requires nothing or system.vibrate on some platforms)
audio.vibrate(100) -- 100ms
audio.vibrate({100, 50, 100}) -- pattern: vibrate, pause, vibrate
Location Interface Security
Location Security Implementation
Status: Designed
struct LocationPolicy {
// Precision control
int coarse_accuracy_meters = 1000; // City-level
int fine_accuracy_meters = 10; // GPS precision
// Rate limiting
int min_update_interval_ms = 1000; // Max 1 update/sec
int background_update_interval_ms = 60000; // 1/min in background
// Privacy
bool require_user_gesture_for_first = true;
bool allow_background_updates = false;
int max_cached_locations = 100;
};
class LocationManager {
public:
std::optional<Location> GetLocation(LuaSandbox* sandbox, LocationAccuracy accuracy) {
// 1. Permission check
std::string perm = (accuracy == LocationAccuracy::Fine)
? "location.fine" : "location.coarse";
if (!sandbox->HasPermission(perm)) {
AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied,
perm, false});
return std::nullopt;
}
// 2. Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "location")) {
return std::nullopt;
}
// 3. Get location from hardware
auto location = m_hardware_location->GetCurrent();
if (!location) return std::nullopt;
// 4. Apply precision reduction for coarse
if (accuracy == LocationAccuracy::Coarse) {
location = ReducePrecision(*location, m_policy.coarse_accuracy_meters);
}
AuditLog::Log({sandbox->app_id(), AuditEvent::LocationAccess,
"single location request", true});
return location;
}
std::optional<uint32_t> WatchLocation(LuaSandbox* sandbox,
LocationAccuracy accuracy,
LocationCallback callback) {
std::string perm = (accuracy == LocationAccuracy::Fine)
? "location.fine" : "location.coarse";
if (!sandbox->HasPermission(perm)) {
return std::nullopt;
}
// Background check
if (!sandbox->IsAppForeground() && !m_policy.allow_background_updates) {
return std::nullopt;
}
uint32_t watch_id = ++m_next_watch_id;
LocationWatch watch{
.id = watch_id,
.app_id = sandbox->app_id(),
.accuracy = accuracy,
.callback = callback,
.last_update = std::chrono::steady_clock::time_point::min()
};
m_watches[watch_id] = watch;
// Start hardware updates if first watch
if (m_watches.size() == 1) {
m_hardware_location->StartUpdates([this](Location loc) {
OnLocationUpdate(loc);
});
}
return watch_id;
}
void ClearWatch(LuaSandbox* sandbox, uint32_t watch_id) {
auto it = m_watches.find(watch_id);
if (it == m_watches.end() || it->second.app_id != sandbox->app_id()) {
return;
}
m_watches.erase(it);
if (m_watches.empty()) {
m_hardware_location->StopUpdates();
}
}
private:
Location ReducePrecision(const Location& loc, int meters) {
// Round coordinates to reduce precision
// 0.01 degree ≈ 1.1km at equator
double precision = meters / 111000.0; // degrees per meter
Location reduced = loc;
reduced.latitude = std::round(loc.latitude / precision) * precision;
reduced.longitude = std::round(loc.longitude / precision) * precision;
reduced.accuracy = std::max(loc.accuracy, static_cast<float>(meters));
return reduced;
}
void OnLocationUpdate(Location loc) {
auto now = std::chrono::steady_clock::now();
for (auto& [id, watch] : m_watches) {
// Check update interval
int interval = m_policy.min_update_interval_ms;
auto elapsed = now - watch.last_update;
if (elapsed < std::chrono::milliseconds(interval)) {
continue;
}
// Apply precision reduction
Location delivered = loc;
if (watch.accuracy == LocationAccuracy::Coarse) {
delivered = ReducePrecision(loc, m_policy.coarse_accuracy_meters);
}
watch.last_update = now;
watch.callback(delivered);
}
}
std::unordered_map<uint32_t, LocationWatch> m_watches;
LocationPolicy m_policy;
IHardwareLocation* m_hardware_location;
RateLimiter m_rate_limiter;
uint32_t m_next_watch_id = 0;
};
Location Lua API
-- Request permission
local granted = permissions.request("location.fine") -- or "location.coarse"
-- Get current location
local loc = location.get()
if loc then
print(loc.latitude, loc.longitude, loc.accuracy)
end
-- Watch location changes
local watch_id = location.watch(function(loc)
print("Moved to:", loc.latitude, loc.longitude)
end, {
accuracy = "fine", -- "fine" or "coarse"
interval = 5000 -- minimum ms between updates
})
-- Stop watching
location.clearWatch(watch_id)
Sensor Interface Security
Sensor Security (Accelerometer, Gyroscope, etc.)
Status: Designed
struct SensorPolicy {
// Rate limiting (prevent fingerprinting via sensor noise patterns)
int max_sample_rate_hz = 60;
// Precision reduction
float accelerometer_precision = 0.01f; // m/s²
float gyroscope_precision = 0.001f; // rad/s
// Background access
bool allow_background_sensors = false;
// Body sensors (heart rate, etc.) require special permission
std::vector<std::string> body_sensors = {"heart_rate", "blood_pressure"};
};
class SensorManager {
public:
std::optional<uint32_t> RegisterListener(LuaSandbox* sandbox,
SensorType type,
SensorCallback callback) {
// Body sensors require dangerous permission
if (IsBodySensor(type) && !sandbox->HasPermission("sensors.body")) {
return std::nullopt;
}
// Background check
if (!sandbox->IsAppForeground() && !m_policy.allow_background_sensors) {
return std::nullopt;
}
uint32_t listener_id = ++m_next_listener_id;
SensorListener listener{
.id = listener_id,
.app_id = sandbox->app_id(),
.type = type,
.callback = callback,
.last_reading = std::chrono::steady_clock::time_point::min()
};
m_listeners[listener_id] = listener;
// Start sensor if first listener
EnableSensorIfNeeded(type);
return listener_id;
}
void UnregisterListener(LuaSandbox* sandbox, uint32_t listener_id) {
auto it = m_listeners.find(listener_id);
if (it == m_listeners.end() || it->second.app_id != sandbox->app_id()) {
return;
}
SensorType type = it->second.type;
m_listeners.erase(it);
// Stop sensor if no more listeners
DisableSensorIfUnneeded(type);
}
private:
void OnSensorReading(SensorType type, SensorReading reading) {
auto now = std::chrono::steady_clock::now();
int min_interval_ms = 1000 / m_policy.max_sample_rate_hz;
for (auto& [id, listener] : m_listeners) {
if (listener.type != type) continue;
// Rate limit
auto elapsed = now - listener.last_reading;
if (elapsed < std::chrono::milliseconds(min_interval_ms)) {
continue;
}
// Apply precision reduction
SensorReading reduced = ReducePrecision(reading, type);
listener.last_reading = now;
listener.callback(reduced);
}
}
SensorReading ReducePrecision(const SensorReading& reading, SensorType type) {
SensorReading reduced = reading;
float precision = 0.01f;
switch (type) {
case SensorType::Accelerometer:
precision = m_policy.accelerometer_precision;
break;
case SensorType::Gyroscope:
precision = m_policy.gyroscope_precision;
break;
default:
break;
}
reduced.x = std::round(reading.x / precision) * precision;
reduced.y = std::round(reading.y / precision) * precision;
reduced.z = std::round(reading.z / precision) * precision;
return reduced;
}
std::unordered_map<uint32_t, SensorListener> m_listeners;
SensorPolicy m_policy;
uint32_t m_next_listener_id = 0;
};
Sensor Lua API
-- Motion sensors (no special permission)
local accel_id = sensors.listen("accelerometer", function(reading)
print("Acceleration:", reading.x, reading.y, reading.z)
end)
local gyro_id = sensors.listen("gyroscope", function(reading)
print("Rotation:", reading.x, reading.y, reading.z)
end)
-- Stop listening
sensors.unlisten(accel_id)
sensors.unlisten(gyro_id)
-- Body sensors (requires "sensors.body" permission)
local granted = permissions.request("sensors.body")
if granted then
sensors.listen("heart_rate", function(reading)
print("Heart rate:", reading.value, "bpm")
end)
end
Bluetooth Interface Security
Bluetooth Security Implementation
Status: Designed
struct BluetoothPolicy {
// Discovery
bool allow_discovery = true;
int discovery_timeout_seconds = 30;
int max_discovered_devices = 50;
// Connections
int max_connections_per_app = 3;
bool require_pairing_consent = true;
// Data
size_t max_transfer_size = 1 * 1024 * 1024; // 1 MB
};
class BluetoothManager {
public:
// Discover nearby devices
std::optional<uint32_t> StartDiscovery(LuaSandbox* sandbox,
DiscoveryCallback callback) {
if (!sandbox->HasPermission("bluetooth")) {
return std::nullopt;
}
// Only one discovery at a time per app
if (HasActiveDiscovery(sandbox->app_id())) {
return std::nullopt;
}
uint32_t discovery_id = ++m_next_discovery_id;
Discovery discovery{
.id = discovery_id,
.app_id = sandbox->app_id(),
.callback = callback,
.devices = {},
.started_at = std::chrono::steady_clock::now()
};
m_discoveries[discovery_id] = discovery;
// Start hardware discovery with timeout
m_hardware_bt->StartDiscovery(
m_policy.discovery_timeout_seconds,
[this, discovery_id](BluetoothDevice device) {
OnDeviceDiscovered(discovery_id, device);
});
return discovery_id;
}
// Connect to device
std::optional<uint32_t> Connect(LuaSandbox* sandbox,
const std::string& device_address) {
if (!sandbox->HasPermission("bluetooth")) {
return std::nullopt;
}
// Connection limit
if (CountAppConnections(sandbox->app_id()) >= m_policy.max_connections_per_app) {
return std::nullopt;
}
// Require user consent for pairing
if (m_policy.require_pairing_consent) {
if (!ShowPairingDialog(device_address)) {
return std::nullopt;
}
}
// Connect
auto conn = m_hardware_bt->Connect(device_address);
if (!conn) return std::nullopt;
uint32_t conn_id = ++m_next_conn_id;
BluetoothConnection connection{
.id = conn_id,
.app_id = sandbox->app_id(),
.device_address = device_address,
.hardware_handle = *conn
};
m_connections[conn_id] = connection;
return conn_id;
}
private:
void OnDeviceDiscovered(uint32_t discovery_id, BluetoothDevice device) {
auto it = m_discoveries.find(discovery_id);
if (it == m_discoveries.end()) return;
// Limit discovered devices
if (it->second.devices.size() >= m_policy.max_discovered_devices) {
return;
}
it->second.devices.push_back(device);
it->second.callback(device);
}
std::unordered_map<uint32_t, Discovery> m_discoveries;
std::unordered_map<uint32_t, BluetoothConnection> m_connections;
BluetoothPolicy m_policy;
IHardwareBluetooth* m_hardware_bt;
uint32_t m_next_discovery_id = 0;
uint32_t m_next_conn_id = 0;
};
Bluetooth Lua API
-- Request permission
local granted = permissions.request("bluetooth")
if not granted then return end
-- Discover devices
local discovery = bluetooth.discover(function(device)
print("Found:", device.name, device.address)
end)
-- Stop discovery
discovery:stop()
-- Connect to device (shows pairing dialog)
local conn = bluetooth.connect("AA:BB:CC:DD:EE:FF")
if conn then
-- Send data
conn:send("Hello")
-- Receive data
conn:onData(function(data)
print("Received:", data)
end)
-- Disconnect
conn:close()
end
Contacts Interface Security
Contacts Security Implementation
Status: Designed
struct ContactsPolicy {
// Access scope
bool allow_read_all = true;
bool allow_write = true;
bool allow_delete = true;
// Rate limits
int max_contacts_per_query = 1000;
int max_writes_per_minute = 100;
// Privacy
bool require_purpose_string = true; // App must explain why
bool log_all_access = true;
};
class ContactsManager {
public:
std::optional<std::vector<Contact>> GetContacts(LuaSandbox* sandbox,
const ContactQuery& query) {
if (!sandbox->HasPermission("contacts.read")) {
return std::nullopt;
}
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_read")) {
return std::nullopt;
}
// Query contacts
auto contacts = m_contacts_store->Query(query);
// Limit results
if (contacts.size() > m_policy.max_contacts_per_query) {
contacts.resize(m_policy.max_contacts_per_query);
}
AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess,
"read " + std::to_string(contacts.size()) + " contacts", true});
return contacts;
}
bool CreateContact(LuaSandbox* sandbox, const Contact& contact) {
if (!sandbox->HasPermission("contacts.write")) {
return false;
}
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_write")) {
return false;
}
// Validate contact data
if (!ValidateContact(contact)) {
return false;
}
bool success = m_contacts_store->Create(contact);
AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess,
"create contact: " + contact.name, success});
return success;
}
bool DeleteContact(LuaSandbox* sandbox, const std::string& contact_id) {
if (!sandbox->HasPermission("contacts.write")) {
return false;
}
// Rate limit
if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_write")) {
return false;
}
bool success = m_contacts_store->Delete(contact_id);
AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess,
"delete contact: " + contact_id, success});
return success;
}
private:
bool ValidateContact(const Contact& contact) {
// Name required
if (contact.name.empty()) return false;
if (contact.name.length() > 256) return false;
// Validate phone numbers
for (const auto& phone : contact.phones) {
if (phone.number.length() > 50) return false;
}
// Validate emails
for (const auto& email : contact.emails) {
if (!IsValidEmail(email.address)) return false;
}
return true;
}
ContactsPolicy m_policy;
IContactsStore* m_contacts_store;
RateLimiter m_rate_limiter;
};
Contacts Lua API
-- Request permission
local granted = permissions.request("contacts.read")
-- Query contacts
local contacts = contacts.getAll()
for _, c in ipairs(contacts) do
print(c.name, c.phones[1], c.emails[1])
end
-- Search contacts
local results = contacts.search("John")
-- Get single contact
local contact = contacts.get(contact_id)
-- Create contact (requires contacts.write)
contacts.create({
name = "John Doe",
phones = {{type = "mobile", number = "+1234567890"}},
emails = {{type = "home", address = "john@example.com"}}
})
-- Update contact
contacts.update(contact_id, {
name = "John Smith"
})
-- Delete contact
contacts.delete(contact_id)
Implementation Status - Virtual Hardware
| Interface | Design | Implementation | Testing |
|---|---|---|---|
| Network HTTP | ✅ Complete | ❌ Not started | ❌ |
| Network WebSocket | ✅ Complete | ❌ Not started | ❌ |
| Filesystem | ✅ Complete | ❌ Not started | ❌ |
| Database | ✅ Complete | ❌ Not started | ❌ |
| Camera | ✅ Complete | ❌ Not started | ❌ |
| Microphone | ✅ Complete | ❌ Not started | ❌ |
| Speaker/Audio | ✅ Complete | ❌ Not started | ❌ |
| Location | ✅ Complete | ❌ Not started | ❌ |
| Sensors | ✅ Complete | ❌ Not started | ❌ |
| Bluetooth | ✅ Complete | ❌ Not started | ❌ |
| Contacts | ✅ Complete | ❌ Not started | ❌ |
References
- Lua 5.4 Manual - Sandboxing
- Sandboxing Lua
- Lapis Lua Sandbox
- Cloudflare Workers Lua Isolation
- OWASP Sandboxing Cheat Sheet
- Android Permission Model
- iOS App Sandbox Design Guide
- SQLite Security
Last updated: 2026-01-18