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

@@ -33,6 +33,8 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
| Android Service | `src/main/` | Native service running RmlUi renderer |
| Desktop Designer | `designer/` | UI development with hot-reload |
| Designer Tests | `designer-test/` | Automated UI testing framework |
| Sandbox Tests | `sandbox-test/` | Lua sandbox security tests |
| Lua Sandbox | `src/main/cpp/sandbox/` | Per-app Lua isolation |
| UI Assets | `src/main/assets/` | Shared RML/RCSS/Lua assets |
## Build Commands
@@ -94,6 +96,27 @@ cmake --build build --config Debug
./build/Debug/designer-test.exe
```
### Sandbox Security Tests (CMake)
```bash
# Configure (from sandbox-test/ folder)
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
# Build
cmake --build build --config Debug
# Run all tests (uber command)
./run_tests.bat
# Or run directly
./build/Debug/sandbox-test.exe
# Run specific test
./build/Debug/sandbox-test.exe --test DangerousGlobals
./build/Debug/sandbox-test.exe --test Memory
./build/Debug/sandbox-test.exe --test CPU
```
## Environment Requirements
Required environment variables:

3569
SANDBOX.md Normal file

File diff suppressed because it is too large Load Diff

964
SANDBOX_MILESTONES.md Normal file
View File

@@ -0,0 +1,964 @@
# Sandbox Implementation Milestones
Based on SANDBOX.md design document. Implementation order optimized for dependencies.
---
## Milestone 1: Core Sandbox Foundation ✅
**Status**: Complete
**Goal**: Create isolated Lua environments with resource limits.
**Estimated Files**: 3 new files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| LuaSandbox class | `src/main/cpp/sandbox/lua_sandbox.h` | Main sandbox container |
| Custom allocator | `src/main/cpp/sandbox/lua_sandbox.cpp` | Per-app memory tracking/limits |
| Instruction hook | `src/main/cpp/sandbox/lua_sandbox.cpp` | CPU limit enforcement |
| Globals removal | `src/main/cpp/sandbox/lua_sandbox.cpp` | Remove os, io, debug, etc. |
| Bytecode prevention | `src/main/cpp/sandbox/lua_sandbox.cpp` | Text-only loading mode |
| Metatable protection | `src/main/cpp/sandbox/lua_sandbox.cpp` | Freeze _G and string metatable |
### Implementation Tasks
1. Create `LuaSandbox` class with:
- `lua_State* m_L` - isolated state
- `SandboxLimits m_limits` - memory/CPU/stack limits
- `SandboxContext m_context` - app_id, permissions, etc.
- Custom allocator that tracks `m_memory_used`
2. Implement `RemoveDangerousGlobals()`:
- Remove: `os`, `io`, `debug`, `package`, `require`, `ffi`, `jit`
- Remove: `dofile`, `loadfile`, `load`, `loadstring`
- Remove: `rawget`, `rawset`, `rawequal`, `rawlen`
- Remove: `collectgarbage`, `newproxy`
- Remove: `string.dump`
3. Implement instruction hook:
- `lua_sethook(L, InstructionHook, LUA_MASKCOUNT, 1000)`
- Throw error when limit exceeded
4. Implement `ProtectBuiltinTables()`:
- Set `__metatable` on string metatable
- Freeze `_G` with `__newindex` error
### Dependencies
- Lua library already linked (via RmlUi)
### Test Criteria
```cpp
TEST(LuaSandbox, DangerousGlobalsRemoved);
TEST(LuaSandbox, BytecodeRejected);
TEST(LuaSandbox, MemoryLimitEnforced);
TEST(LuaSandbox, CPULimitEnforced);
TEST(LuaSandbox, MetatableProtected);
```
---
## Milestone 2: Permission System ✅
**Status**: Complete
**Goal**: Gate API access based on app permissions.
**Estimated Files**: 2 new files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| PermissionGate | `src/main/cpp/sandbox/permission_gate.h` | Permission checking |
| User gesture tracking | `src/main/cpp/sandbox/permission_gate.cpp` | Recent tap/click detection |
| Permission categories | `src/main/cpp/sandbox/permission_gate.cpp` | Normal/Dangerous/Signature |
### Implementation Tasks
1. Define permission categories:
```cpp
enum class PermissionCategory { Normal, Dangerous, Signature };
```
2. Create `PermissionGate` class:
- `bool Check(lua_State* L, const std::string& permission)`
- `int RequirePermission(lua_State* L, const char* perm)` - throws Lua error
3. Implement user gesture tracking:
- `void RecordUserGesture()` - called on touch down
- `bool HasRecentUserGesture(int ms)` - check within time window
4. Define permission manifest parsing:
- Read `manifest.json` from app package
- Extract `permissions` array
### Dependencies
- Milestone 1 (LuaSandbox)
### Test Criteria
```cpp
TEST(PermissionGate, NormalPermissionAutoGranted);
TEST(PermissionGate, DangerousPermissionRequiresGrant);
TEST(PermissionGate, SignaturePermissionSystemOnly);
TEST(PermissionGate, UserGestureRequired);
```
---
## Milestone 3: Audit Logging & Rate Limiting ✅
**Status**: Complete
**Goal**: Track security events and prevent API abuse.
**Estimated Files**: 2 new files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| AuditLog | `src/main/cpp/sandbox/audit_log.h/cpp` | Security event logging |
| RateLimiter | `src/main/cpp/sandbox/rate_limiter.h/cpp` | Token bucket rate limiting |
### Implementation Tasks
1. Create `AuditLog` class:
```cpp
enum class AuditEvent {
AppStart, AppStop, PermissionCheck, PermissionDenied,
NetworkRequest, FileAccess, SandboxViolation, ResourceLimitHit
};
```
2. Implement thread-safe logging:
- Ring buffer with max entries
- Log critical events to Android logcat
3. Create `RateLimiter` class:
- Token bucket algorithm
- Per-operation limits: `network.request`, `storage.write`, etc.
- `bool Check(const std::string& app_id, const std::string& operation)`
### Dependencies
- Milestone 1 (LuaSandbox)
### Test Criteria
```cpp
TEST(AuditLog, LogsSecurityEvents);
TEST(AuditLog, ThreadSafe);
TEST(RateLimiter, EnforcesLimits);
TEST(RateLimiter, RefillsOverTime);
```
---
## Milestone 4: Safe Path & Require ✅
**Status**: Complete
**Goal**: Secure file access within app sandbox.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| Path validation | `src/main/cpp/sandbox/path_sandbox.h/cpp` | Canonical path checking |
| Safe require | `src/main/cpp/sandbox/path_sandbox.cpp` | Module loader from app/scripts/ |
### Implementation Tasks
1. Implement `ValidatePath()`:
- Reject `..` traversal
- Reject absolute paths
- Canonicalize and verify prefix match
2. Implement `SafeRequire()`:
- Validate module name (alphanumeric + underscore)
- Load from `app_path/scripts/name.lua`
- Cache in registry `__loaded`
- Text-only mode (`"t"`)
3. Register as global `require`:
```cpp
lua_pushcfunction(L, SafeRequire);
lua_setglobal(L, "require");
```
### Dependencies
- Milestone 1 (LuaSandbox)
### Test Criteria
```cpp
TEST(PathSandbox, RejectsTraversal);
TEST(PathSandbox, RejectsAbsolutePaths);
TEST(SafeRequire, LoadsFromScriptsDir);
TEST(SafeRequire, CachesModules);
```
---
## Milestone 5: Timer & Callback System
**Goal**: Safe timer APIs managed by kernel.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| TimerManager | `src/main/cpp/sandbox/timer_manager.h/cpp` | setTimeout/setInterval |
### Implementation Tasks
1. Create `TimerManager` class:
- Priority queue of pending timers
- Per-app timer limits (max 100)
- Minimum 10ms granularity
2. Implement Lua APIs:
```lua
setTimeout(callback, ms) -> id
clearTimeout(id)
setInterval(callback, ms) -> id
clearInterval(id)
```
3. Integrate with kernel main loop:
- Check and fire timers each frame
- Reset instruction count before callback
4. Implement cleanup:
- `ClearAppTimers(app_id)` on app stop
### Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 3 (RateLimiter)
### Test Criteria
```cpp
TEST(TimerManager, FiresTimeout);
TEST(TimerManager, FiresInterval);
TEST(TimerManager, LimitsPerApp);
TEST(TimerManager, CleansUpOnStop);
```
---
## Milestone 6: JSON & Crypto APIs
**Goal**: Safe data parsing and cryptographic primitives.
**Estimated Files**: 2 new files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| JSON API | `src/main/cpp/sandbox/json_api.cpp` | Safe encode/decode |
| Crypto API | `src/main/cpp/sandbox/crypto_api.cpp` | Hash, random, HMAC |
### Implementation Tasks
1. Implement `json.decode()`:
- Depth limit (32)
- String length limit (1 MB)
- Array/object size limits
2. Implement `json.encode()`:
- Cycle detection
- Output size limit
3. Implement crypto APIs:
- `crypto.randomBytes(n)` - CSPRNG
- `crypto.hash("sha256", data)`
- `crypto.hmac("sha256", key, data)`
- Remove `math.randomseed`, replace `math.random` with per-app RNG
### Dependencies
- Milestone 1 (LuaSandbox)
- nlohmann-json (already in vcpkg)
### Test Criteria
```cpp
TEST(JsonApi, DecodesValid);
TEST(JsonApi, RejectsDeepNesting);
TEST(JsonApi, RejectsTooLarge);
TEST(CryptoApi, RandomBytesSecure);
TEST(CryptoApi, HashCorrect);
```
---
## Milestone 7: Virtual Filesystem
**Goal**: Per-app isolated storage with quotas.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| VirtualFS | `src/main/cpp/sandbox/virtual_fs.h/cpp` | App storage API |
### Implementation Tasks
1. Define virtual path mapping:
```
/data/ → /data/data/com.omixlab.mosis/apps/<app_id>/data/
/cache/ → /data/data/com.omixlab.mosis/apps/<app_id>/cache/
/temp/ → (session-only, cleared on stop)
/shared/ → requires storage.shared permission
```
2. Implement Lua file API:
```lua
fs.read(path) -> string
fs.write(path, data) -> bool
fs.append(path, data) -> bool
fs.delete(path) -> bool
fs.exists(path) -> bool
fs.list(dir) -> table
fs.mkdir(path) -> bool
fs.stat(path) -> {size, modified, isDir}
```
3. Enforce limits:
- Per-app quota (50 MB default)
- Max file size (10 MB)
- Max path depth (10)
4. Cleanup `/temp/` on app stop.
### Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 2 (PermissionGate)
- Milestone 4 (Path validation)
### Test Criteria
```cpp
TEST(VirtualFS, ReadsWritesInAppDir);
TEST(VirtualFS, BlocksTraversal);
TEST(VirtualFS, EnforcesQuota);
TEST(VirtualFS, CleansUpTemp);
```
---
## Milestone 8: SQLite Database
**Goal**: Per-app SQLite with injection prevention.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| DatabaseManager | `src/main/cpp/sandbox/database_manager.h/cpp` | SQLite sandbox |
### Implementation Tasks
1. Create per-app database:
- Location: `/data/data/com.omixlab.mosis/apps/<app_id>/db/app.db`
- Max database size: 50 MB
2. Implement SQLite authorizer:
```cpp
sqlite3_set_authorizer(db, Authorizer, sandbox);
// Block: ATTACH, DETACH, dangerous PRAGMAs, load_extension
```
3. Implement Lua API:
```lua
local db = database.open("mydata")
db:execute("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)")
db:execute("INSERT INTO items (name) VALUES (?)", {"Item 1"})
local rows = db:query("SELECT * FROM items WHERE id > ?", {0})
db:close()
```
4. Enforce prepared statements only (no raw SQL interpolation).
### Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 2 (PermissionGate)
- SQLite3 (add to vcpkg)
### Test Criteria
```cpp
TEST(Database, CreatesTables);
TEST(Database, PreparedStatements);
TEST(Database, BlocksAttach);
TEST(Database, BlocksDangerousPragma);
```
---
## Milestone 9: Network - HTTP
**Goal**: Secure HTTP requests with domain filtering.
**Estimated Files**: 2 new files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| HttpRequestValidator | `src/main/cpp/sandbox/http_validator.h/cpp` | URL/domain validation |
| NetworkManager | `src/main/cpp/sandbox/network_manager.h/cpp` | HTTP client wrapper |
### Implementation Tasks
1. Implement URL parsing and validation:
- Require HTTPS
- Block private IPs, localhost, metadata IPs
- Domain whitelist from manifest
2. Implement Lua HTTP API:
```lua
local response = network.request({
url = "https://api.example.com/data",
method = "POST",
headers = {...},
body = "...",
timeout = 30000
})
```
3. Integrate with Android HttpURLConnection or libcurl.
4. Enforce limits:
- Request body: 10 MB
- Response body: 50 MB
- Timeout: 60s
- Concurrent requests: 6
### Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 2 (PermissionGate)
- Milestone 3 (RateLimiter, AuditLog)
### Test Criteria
```cpp
TEST(HttpValidator, BlocksPrivateIP);
TEST(HttpValidator, BlocksLocalhost);
TEST(HttpValidator, RequiresHttps);
TEST(HttpValidator, EnforcesDomainWhitelist);
TEST(NetworkManager, MakesRequest);
```
---
## Milestone 10: Network - WebSocket
**Goal**: Secure WebSocket connections.
**Estimated Files**: 1 new file (extends NetworkManager)
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| WebSocketManager | `src/main/cpp/sandbox/websocket_manager.h/cpp` | WS connections |
### Implementation Tasks
1. Implement connection management:
- Max 5 connections per app
- Idle timeout: 5 minutes
- Message size limit: 1 MB
2. Implement Lua WebSocket API:
```lua
local ws = network.websocket("wss://api.example.com/ws")
ws:on("open", function() ... end)
ws:on("message", function(data) ... end)
ws:on("close", function(code, reason) ... end)
ws:send(data)
ws:close()
```
3. Use same URL validation as HTTP.
4. Cleanup connections on app stop.
### Dependencies
- Milestone 9 (HttpRequestValidator)
### Test Criteria
```cpp
TEST(WebSocketManager, Connects);
TEST(WebSocketManager, LimitsPerApp);
TEST(WebSocketManager, CleansUpOnStop);
```
---
## Milestone 11: Virtual Hardware - Camera
**Goal**: Camera access with mandatory indicators.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| CameraInterface | `src/main/cpp/sandbox/camera_interface.h/cpp` | Camera API |
### Implementation Tasks
1. Implement session management:
- `startSession(options)` - requires permission + user gesture
- `capturePhoto()` - returns frame data
- `stopSession()`
2. Mandatory recording indicator:
- Show in system UI (cannot be hidden by app)
3. Implement Lua API:
```lua
local session = camera.start({facing = "back", resolution = "720p"})
session:on("frame", function(data) ... end)
local photo = session:capture()
session:stop()
```
4. Rate limit: 1 session at a time, max 30 fps.
### Dependencies
- Milestone 2 (PermissionGate + user gesture)
- Milestone 3 (AuditLog)
- Game engine camera bridge (future)
### Test Criteria
```cpp
TEST(CameraInterface, RequiresPermission);
TEST(CameraInterface, RequiresUserGesture);
TEST(CameraInterface, ShowsIndicator);
TEST(CameraInterface, StopsOnAppStop);
```
---
## Milestone 12: Virtual Hardware - Microphone
**Goal**: Audio recording with mandatory indicators.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| MicrophoneInterface | `src/main/cpp/sandbox/microphone_interface.h/cpp` | Mic API |
### Implementation Tasks
1. Implement recording session:
- `startRecording(options)` - requires permission + user gesture
- `stopRecording()` - returns audio data
2. Mandatory recording indicator (same as camera).
3. Implement Lua API:
```lua
local recording = microphone.start({sampleRate = 44100, channels = 1})
recording:on("data", function(samples) ... end)
local audio = recording:stop()
```
4. Rate limit: 1 recording at a time.
### Dependencies
- Milestone 2 (PermissionGate + user gesture)
- Milestone 3 (AuditLog)
### Test Criteria
```cpp
TEST(MicrophoneInterface, RequiresPermission);
TEST(MicrophoneInterface, RequiresUserGesture);
TEST(MicrophoneInterface, ShowsIndicator);
```
---
## Milestone 13: Virtual Hardware - Audio Output
**Goal**: Safe audio playback.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| AudioOutputInterface | `src/main/cpp/sandbox/audio_output.h/cpp` | Speaker API |
### Implementation Tasks
1. Implement playback:
- `play(audioData, options)`
- `stop()`
- `setVolume(0.0-1.0)`
2. System volume limit (app cannot exceed system volume).
3. Implement Lua API:
```lua
local player = audio.play(soundData, {loop = false, volume = 0.8})
player:on("ended", function() ... end)
player:stop()
```
4. Concurrent sound limit: 10 per app.
### Dependencies
- Milestone 1 (LuaSandbox)
### Test Criteria
```cpp
TEST(AudioOutput, PlaysSound);
TEST(AudioOutput, RespectsVolumeLimit);
TEST(AudioOutput, LimitsConcurrent);
```
---
## Milestone 14: Virtual Hardware - Location
**Goal**: Location with privacy controls.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| LocationInterface | `src/main/cpp/sandbox/location_interface.h/cpp` | GPS API |
### Implementation Tasks
1. Two permission levels:
- `location.coarse` - city-level (1km accuracy)
- `location.fine` - precise GPS
2. Implement Lua API:
```lua
location.getCurrentPosition(function(pos)
print(pos.latitude, pos.longitude, pos.accuracy)
end)
local watch = location.watchPosition(function(pos) ... end, {
enableHighAccuracy = true,
timeout = 30000
})
watch:stop()
```
3. Rate limit: 1 request per second.
4. Reduce precision for coarse permission.
### Dependencies
- Milestone 2 (PermissionGate)
- Milestone 3 (RateLimiter)
### Test Criteria
```cpp
TEST(LocationInterface, RequiresPermission);
TEST(LocationInterface, CoarseReducesPrecision);
TEST(LocationInterface, RateLimits);
```
---
## Milestone 15: Virtual Hardware - Sensors
**Goal**: Motion sensors with fingerprinting prevention.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| SensorInterface | `src/main/cpp/sandbox/sensor_interface.h/cpp` | Accelerometer, gyro, etc. |
### Implementation Tasks
1. Supported sensors:
- Accelerometer (requires `sensors.motion`)
- Gyroscope (requires `sensors.motion`)
- Magnetometer (requires `sensors.motion`)
- Proximity (auto-granted)
- Ambient light (auto-granted)
2. Implement Lua API:
```lua
local sub = sensors.subscribe("accelerometer", function(data)
print(data.x, data.y, data.z, data.timestamp)
end, {frequency = 60})
sub:stop()
```
3. Reduce precision to prevent fingerprinting:
- Round values to 2 decimal places
- Limit update frequency to 60 Hz
### Dependencies
- Milestone 2 (PermissionGate)
### Test Criteria
```cpp
TEST(SensorInterface, RequiresPermission);
TEST(SensorInterface, ReducesPrecision);
TEST(SensorInterface, LimitsFrequency);
```
---
## Milestone 16: Virtual Hardware - Bluetooth
**Goal**: Bluetooth discovery and pairing.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| BluetoothInterface | `src/main/cpp/sandbox/bluetooth_interface.h/cpp` | BT API |
### Implementation Tasks
1. Discovery with user consent:
- `startDiscovery()` requires user confirmation
- Return device name/address only (no full UUID list)
2. Implement Lua API:
```lua
bluetooth.startDiscovery(function(devices)
for _, device in ipairs(devices) do
print(device.name, device.address)
end
end, {timeout = 30})
bluetooth.connect(address, function(connection)
connection:send(data)
end)
```
3. Limit: 5 connections per app.
### Dependencies
- Milestone 2 (PermissionGate + user gesture)
### Test Criteria
```cpp
TEST(BluetoothInterface, RequiresPermission);
TEST(BluetoothInterface, RequiresUserConsent);
```
---
## Milestone 17: Virtual Hardware - Contacts
**Goal**: Contact access with granular permissions.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| ContactsInterface | `src/main/cpp/sandbox/contacts_interface.h/cpp` | Contacts API |
### Implementation Tasks
1. Separate read/write permissions:
- `contacts.read` - query contacts
- `contacts.write` - add/modify contacts
2. Implement Lua API:
```lua
local contacts = contacts.query({search = "John"})
for _, c in ipairs(contacts) do
print(c.name, c.phone, c.email)
end
contacts.add({name = "New Contact", phone = "555-1234"})
```
3. Limit fields returned (no raw account data).
### Dependencies
- Milestone 2 (PermissionGate)
### Test Criteria
```cpp
TEST(ContactsInterface, RequiresReadPermission);
TEST(ContactsInterface, RequiresWritePermission);
```
---
## Milestone 18: Inter-App Communication
**Goal**: Kernel-mediated message passing.
**Estimated Files**: 1 new file
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| MessageBus | `src/main/cpp/sandbox/message_bus.h/cpp` | App-to-app messaging |
### Implementation Tasks
1. Implement intent system:
- Manifest declares received intents
- Sender must have required permissions
2. Implement Lua API:
```lua
-- Sending
intents.send({
action = "share",
type = "text/plain",
data = "Hello world"
})
-- Receiving (in manifest + handler)
intents.on("share", function(intent)
print(intent.from, intent.data)
end)
```
3. Validate sender/receiver at runtime.
### Dependencies
- Milestone 2 (PermissionGate)
- Milestone 3 (AuditLog)
### Test Criteria
```cpp
TEST(MessageBus, SendsToRegisteredReceiver);
TEST(MessageBus, BlocksUnregisteredAction);
```
---
## Milestone 19: Security Testing Suite
**Goal**: Comprehensive test coverage.
**Estimated Files**: 3 new files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| Unit tests | `tests/test_sandbox_security.cpp` | Core sandbox tests |
| Integration tests | `tests/test_sandbox_integration.cpp` | Full system tests |
| Fuzzer | `tests/fuzz_sandbox.cpp` | Random input testing |
### Implementation Tasks
1. Write unit tests for all milestones (see individual test criteria).
2. Write integration tests:
- Full app lifecycle
- Permission flow
- Resource cleanup
3. Implement fuzzer:
- Generate random Lua code
- Verify no crashes
- Verify sandbox integrity after each run
4. Security audit checklist (from SANDBOX.md).
### Dependencies
- All previous milestones
---
## Milestone 20: Kernel Integration
**Goal**: Wire sandbox into existing kernel.
**Estimated Files**: Modify existing files
### Deliverables
| Component | File | Description |
|-----------|------|-------------|
| App lifecycle | `src/main/cpp/kernel.cpp` | App start/stop |
| Sandbox manager | `src/main/cpp/kernel.cpp` | Multi-app management |
### Implementation Tasks
1. Replace global `lua_State` with `LuaSandboxManager`:
- Create sandbox per app
- Route events to correct sandbox
2. Integrate with RmlUi:
- Bridge RmlUi document events to sandbox
- Replace `Rml::Lua::Interpreter` with sandboxed states
3. Wire up resource cleanup on app stop.
### Dependencies
- Milestones 1-18
---
## Summary
| Phase | Milestones | Description |
|-------|------------|-------------|
| **Foundation** | 1-4 | Core sandbox, permissions, logging, paths |
| **Core APIs** | 5-8 | Timers, JSON, crypto, filesystem, database |
| **Network** | 9-10 | HTTP and WebSocket |
| **Hardware** | 11-17 | Camera, mic, audio, location, sensors, BT, contacts |
| **System** | 18-20 | IPC, testing, integration |
### Recommended Order
1. **Milestone 1** - Must be first (foundation)
2. **Milestone 2** - Permissions (needed by everything)
3. **Milestone 3** - Audit/rate limiting (needed by APIs)
4. **Milestone 4** - Path security (needed by fs/require)
5. **Milestone 5-6** - Timers, JSON (high value)
6. **Milestone 7-8** - Storage, database (app data)
7. **Milestone 9-10** - Network (app connectivity)
8. **Milestones 11-17** - Hardware (as needed)
9. **Milestone 18** - IPC (multi-app)
10. **Milestone 19-20** - Testing & integration
### Quick Start
Begin with **Milestone 1** to establish the core sandbox. This is the foundation everything else builds on.

595
SANDBOX_MILESTONE_1.md Normal file
View File

@@ -0,0 +1,595 @@
# Milestone 1: Core Sandbox Foundation
**Status**: ✅ Complete
**Goal**: Create isolated Lua environments with resource limits and dangerous global removal.
---
## Overview
This milestone establishes the foundational security layer for the Mosis app sandbox. Every subsequent milestone builds on this foundation.
### Key Deliverables
1. **LuaSandbox class** - Per-app isolated Lua state
2. **Custom allocator** - Memory tracking and limits
3. **Instruction hook** - CPU limit enforcement
4. **Globals removal** - Remove dangerous functions
5. **Bytecode prevention** - Text-only loading
6. **Metatable protection** - Freeze globals
---
## File Structure
```
src/main/cpp/sandbox/
├── lua_sandbox.h # Main header
├── lua_sandbox.cpp # Implementation
└── sandbox_tests.h # Test declarations (for test runner)
sandbox-test/
├── CMakeLists.txt # Test build config
├── README.md # Test documentation
├── src/
│ ├── main.cpp # Test runner entry
│ ├── test_harness.h # Test framework
│ └── test_harness.cpp # Test framework impl
└── scripts/
├── test_globals_removed.lua
├── test_bytecode_rejected.lua
├── test_memory_limit.lua
├── test_cpu_limit.lua
├── test_metatable_protected.lua
├── test_safe_operations.lua
└── test_string_dump_removed.lua
```
---
## Implementation Details
### 1. SandboxLimits Structure
```cpp
// lua_sandbox.h
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
};
```
### 2. SandboxContext Structure
```cpp
// lua_sandbox.h
struct SandboxContext {
std::string app_id;
std::string app_path;
std::vector<std::string> permissions;
bool is_system_app = false;
};
```
### 3. LuaSandbox Class
```cpp
// lua_sandbox.h
class LuaSandbox {
public:
explicit LuaSandbox(const SandboxContext& context,
const SandboxLimits& limits = {});
~LuaSandbox();
// Non-copyable
LuaSandbox(const LuaSandbox&) = delete;
LuaSandbox& operator=(const LuaSandbox&) = delete;
// Load and execute Lua code
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 std::string& app_id() const { return m_context.app_id; }
// Reset instruction counter (call before each event handler)
void ResetInstructionCount();
private:
void SetupSandbox();
void RemoveDangerousGlobals();
void ProtectBuiltinTables();
void SetupInstructionHook();
static void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize);
static void InstructionHook(lua_State* L, lua_Debug* ar);
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;
};
```
### 4. Custom Memory Allocator
```cpp
// lua_sandbox.cpp
void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
auto* sandbox = static_cast<LuaSandbox*>(ud);
// Calculate new usage
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) {
return nullptr; // Allocation fails, triggers Lua memory error
}
// Update tracking
sandbox->m_memory_used = new_usage;
// Free
if (nsize == 0) {
free(ptr);
return nullptr;
}
// Alloc or realloc
return realloc(ptr, nsize);
}
```
### 5. Instruction Hook
```cpp
// lua_sandbox.cpp
void LuaSandbox::InstructionHook(lua_State* L, lua_Debug* ar) {
// Get sandbox from registry
lua_getfield(L, LUA_REGISTRYINDEX, "__sandbox");
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
if (!sandbox) return;
// Increment by hook interval (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 (%llu instructions)",
sandbox->m_instructions_used);
}
}
void LuaSandbox::SetupInstructionHook() {
lua_sethook(m_L, InstructionHook, LUA_MASKCOUNT, 1000);
}
```
### 6. Dangerous Globals Removal
```cpp
// lua_sandbox.cpp
void LuaSandbox::RemoveDangerousGlobals() {
const char* dangerous_globals[] = {
// Code execution
"dofile", "loadfile", "load", "loadstring",
// Raw access (bypass metatables)
"rawget", "rawset", "rawequal", "rawlen",
// Metatable manipulation (removed for protection)
"getmetatable", "setmetatable",
// GC manipulation
"collectgarbage",
// Dangerous libraries
"os", "io", "debug", "package",
// LuaJIT/FFI (if present)
"ffi", "jit", "newproxy",
nullptr
};
for (const char** p = dangerous_globals; *p; ++p) {
lua_pushnil(m_L);
lua_setglobal(m_L, *p);
}
// Remove require (we'll add safe version later)
lua_pushnil(m_L);
lua_setglobal(m_L, "require");
// Remove string.dump (creates bytecode)
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);
}
```
### 7. Metatable Protection
```cpp
// lua_sandbox.cpp
void LuaSandbox::ProtectBuiltinTables() {
// Protect string 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 with protective metatable
lua_pushglobaltable(m_L);
lua_newtable(m_L); // metatable for _G
// __metatable prevents access via getmetatable
lua_pushstring(m_L, "globals");
lua_setfield(m_L, -2, "__metatable");
// __newindex prevents modification
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");
lua_setmetatable(m_L, -2);
lua_pop(m_L, 1); // pop _G
}
```
### 8. Load with Bytecode Prevention
```cpp
// lua_sandbox.cpp
bool LuaSandbox::LoadString(const std::string& code, const std::string& chunk_name) {
// Reset instruction count for this execution
ResetInstructionCount();
// Load as TEXT ONLY - "t" mode rejects bytecode
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
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;
}
return true;
}
```
---
## Test Cases
Each test has a corresponding Lua script and C++ test function.
### Test 1: Dangerous Globals Removed
**Script**: `scripts/test_globals_removed.lua`
```lua
-- Test that dangerous globals are nil
local dangerous = {
"os", "io", "debug", "package", "ffi", "jit",
"dofile", "loadfile", "load", "loadstring",
"rawget", "rawset", "rawequal", "rawlen",
"collectgarbage", "newproxy", "require"
}
for _, name in ipairs(dangerous) do
local value = _G[name]
if value ~= nil then
error("FAIL: " .. name .. " should be nil but is " .. type(value))
end
end
print("PASS: All dangerous globals removed")
```
**C++ Test**:
```cpp
TEST(LuaSandbox, DangerousGlobalsRemoved) {
LuaSandbox sandbox(TestContext());
EXPECT_TRUE(sandbox.LoadFile("scripts/test_globals_removed.lua"));
}
```
### Test 2: Bytecode Rejected
**Script**: `scripts/test_bytecode_rejected.lua`
```lua
-- This script tests that bytecode loading is blocked
-- The actual bytecode test is done from C++ side
print("PASS: Text loading works")
```
**C++ Test**:
```cpp
TEST(LuaSandbox, BytecodeRejected) {
LuaSandbox sandbox(TestContext());
// Lua bytecode starts with signature: \x1bLua
std::string bytecode = "\x1bLua\x54\x00\x19\x93"; // Fake bytecode header
EXPECT_FALSE(sandbox.LoadString(bytecode, "bytecode_test"));
EXPECT_TRUE(sandbox.GetLastError().find("binary") != std::string::npos ||
sandbox.GetLastError().find("attempt to load") != std::string::npos);
}
```
### Test 3: Memory Limit Enforced
**Script**: `scripts/test_memory_limit.lua`
```lua
-- This script intentionally tries to exhaust memory
-- Should fail with memory error
local t = {}
local i = 0
while true do
i = i + 1
t[i] = string.rep("x", 100000) -- 100KB strings
if i > 1000 then
error("FAIL: Should have hit memory limit by now")
end
end
```
**C++ Test**:
```cpp
TEST(LuaSandbox, MemoryLimitEnforced) {
SandboxLimits limits;
limits.memory_bytes = 1024 * 1024; // 1 MB limit
LuaSandbox sandbox(TestContext(), limits);
EXPECT_FALSE(sandbox.LoadFile("scripts/test_memory_limit.lua"));
// Should fail due to memory allocation failure
}
```
### Test 4: CPU Limit Enforced
**Script**: `scripts/test_cpu_limit.lua`
```lua
-- Infinite loop - should be stopped by instruction limit
while true do
-- busy loop
end
error("FAIL: Should never reach here")
```
**C++ Test**:
```cpp
TEST(LuaSandbox, CPULimitEnforced) {
SandboxLimits limits;
limits.instructions_per_call = 10000; // Very low limit
LuaSandbox sandbox(TestContext(), limits);
EXPECT_FALSE(sandbox.LoadFile("scripts/test_cpu_limit.lua"));
EXPECT_TRUE(sandbox.GetLastError().find("instruction") != std::string::npos);
}
```
### Test 5: Metatable Protected
**Script**: `scripts/test_metatable_protected.lua`
```lua
-- Test 1: String metatable is protected
local mt = getmetatable("")
if mt ~= "string" then
error("FAIL: string metatable should return 'string', got " .. tostring(mt))
end
-- Test 2: Cannot modify global environment
local ok, err = pcall(function()
_G.my_new_global = "test"
end)
if ok then
error("FAIL: Should not be able to add globals")
end
-- Test 3: Cannot modify existing globals
local ok2, err2 = pcall(function()
_G.print = nil
end)
if ok2 then
error("FAIL: Should not be able to modify print")
end
print("PASS: Metatables protected")
```
**C++ Test**:
```cpp
TEST(LuaSandbox, MetatableProtected) {
LuaSandbox sandbox(TestContext());
EXPECT_TRUE(sandbox.LoadFile("scripts/test_metatable_protected.lua"));
}
```
### Test 6: Safe Operations Work
**Script**: `scripts/test_safe_operations.lua`
```lua
-- Test that safe operations still work
-- Math operations
local x = math.sin(1.5) + math.floor(3.7)
assert(type(x) == "number", "Math failed")
-- String operations
local s = string.format("hello %d", 42)
assert(s == "hello 42", "String format failed")
local upper = string.upper("test")
assert(upper == "TEST", "String upper failed")
-- Table operations
local t = {1, 2, 3}
table.insert(t, 4)
assert(#t == 4, "Table insert failed")
table.sort(t)
assert(t[1] == 1 and t[4] == 4, "Table sort failed")
-- Iteration
local count = 0
for i, v in ipairs(t) do count = count + 1 end
assert(count == 4, "ipairs failed")
count = 0
for k, v in pairs({a=1, b=2, c=3}) do count = count + 1 end
assert(count == 3, "pairs failed")
-- Error handling
local ok, err = pcall(function() error("test error") end)
assert(not ok, "pcall should catch error")
assert(err:find("test error"), "Error message wrong")
-- Type checks
assert(type({}) == "table", "type table failed")
assert(type("") == "string", "type string failed")
assert(type(123) == "number", "type number failed")
assert(type(function() end) == "function", "type function failed")
-- tonumber/tostring
assert(tonumber("42") == 42, "tonumber failed")
assert(tostring(42) == "42", "tostring failed")
-- select
local a, b = select(2, 1, 2, 3)
assert(a == 2 and b == 3, "select failed")
-- assert works
assert(true, "assert failed")
print("PASS: All safe operations work")
```
### Test 7: string.dump Removed
**Script**: `scripts/test_string_dump_removed.lua`
```lua
-- string.dump can create bytecode, should be removed
if string.dump ~= nil then
error("FAIL: string.dump should be nil")
end
print("PASS: string.dump removed")
```
---
## Build & Test Commands
### Build
```bash
cd sandbox-test
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
cmake --build build --config Debug
```
### Run All Tests
```bash
./build/Debug/sandbox-test.exe
```
### Run Single Test
```bash
./build/Debug/sandbox-test.exe --test DangerousGlobalsRemoved
```
### Test Output Format
```json
{
"name": "Lua Sandbox Security Tests",
"timestamp": "2024-01-15T10:30:00Z",
"summary": {
"passed": 7,
"failed": 0,
"total": 7
},
"tests": [
{
"name": "DangerousGlobalsRemoved",
"status": "passed",
"duration_ms": 5
},
...
]
}
```
---
## Acceptance Criteria
All tests must pass:
- [x] `TEST(LuaSandbox, DangerousGlobalsRemoved)` - os/io/debug/etc are nil
- [x] `TEST(LuaSandbox, BytecodeRejected)` - Binary chunks rejected
- [x] `TEST(LuaSandbox, MemoryLimitEnforced)` - OOM before system impact
- [x] `TEST(LuaSandbox, CPULimitEnforced)` - Infinite loops stopped
- [x] `TEST(LuaSandbox, MetatableProtected)` - _G frozen, string mt protected
- [x] `TEST(LuaSandbox, SafeOperationsWork)` - Normal Lua code works
- [x] `TEST(LuaSandbox, StringDumpRemoved)` - string.dump is nil
---
## Security Audit Checklist
After implementation, verify:
- [x] No way to access `os.execute` or equivalent
- [x] No way to load bytecode
- [x] No way to read arbitrary files
- [x] Memory exhaustion causes graceful failure
- [x] CPU exhaustion causes graceful failure
- [x] Cannot escape sandbox via metatable tricks
- [x] Cannot modify protected globals
---
## Next Steps
After Milestone 1 passes:
1. Integrate with kernel (replace global lua_State)
2. Add permission system (Milestone 2)
3. Add audit logging (Milestone 3)

488
SANDBOX_MILESTONE_2.md Normal file
View File

@@ -0,0 +1,488 @@
# Milestone 2: Permission System
**Status**: ✅ Complete
**Goal**: Gate API access based on app permissions.
---
## Overview
This milestone implements the permission system that controls which APIs an app can access. It defines permission categories (Normal, Dangerous, Signature) and provides mechanisms for checking permissions at runtime.
### Key Deliverables
1. **PermissionGate class** - Permission checking and enforcement
2. **Permission categories** - Normal/Dangerous/Signature classification
3. **User gesture tracking** - Detect recent user interactions
4. **Manifest parsing** - Read app permissions from manifest.json
---
## File Structure
```
src/main/cpp/sandbox/
├── lua_sandbox.h # (existing)
├── lua_sandbox.cpp # (existing)
├── permission_gate.h # NEW - Permission checking
└── permission_gate.cpp # NEW - Implementation
sandbox-test/
├── scripts/
│ ├── test_permission_normal.lua # NEW
│ ├── test_permission_dangerous.lua # NEW
│ └── test_permission_signature.lua # NEW
└── src/
└── main.cpp # Add new tests
```
---
## Implementation Details
### 1. Permission Categories
```cpp
// permission_gate.h
enum class PermissionCategory {
Normal, // Auto-granted (e.g., vibrate, internet)
Dangerous, // Requires user consent (e.g., camera, location)
Signature // System apps only (e.g., system settings, install apps)
};
```
### 2. Permission Definitions
```cpp
// permission_gate.cpp
static const std::unordered_map<std::string, PermissionInfo> PERMISSIONS = {
// Normal permissions (auto-granted)
{"internet", {PermissionCategory::Normal, "Access the internet"}},
{"vibrate", {PermissionCategory::Normal, "Vibrate the device"}},
{"wake_lock", {PermissionCategory::Normal, "Keep device awake"}},
{"notifications", {PermissionCategory::Normal, "Show notifications"}},
// 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"}},
{"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}},
{"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}},
{"calendar.read", {PermissionCategory::Dangerous, "Read calendar"}},
{"calendar.write", {PermissionCategory::Dangerous, "Modify calendar"}},
// 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"}},
};
```
### 3. PermissionGate Class
```cpp
// permission_gate.h
#pragma once
#include <string>
#include <vector>
#include <unordered_set>
#include <chrono>
struct lua_State;
namespace mosis {
struct SandboxContext; // Forward declaration
enum class PermissionCategory {
Normal,
Dangerous,
Signature
};
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);
// 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;
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;
};
// Lua helper - throws error if permission denied
int RequirePermission(lua_State* L, const char* permission);
} // namespace mosis
```
### 4. Implementation
```cpp
// permission_gate.cpp
#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
{"internet", {PermissionCategory::Normal, "Access the internet"}},
{"vibrate", {PermissionCategory::Normal, "Vibrate the device"}},
{"wake_lock", {PermissionCategory::Normal, "Keep device awake"}},
{"notifications", {PermissionCategory::Normal, "Show notifications"}},
// Dangerous
{"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"}},
{"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}},
{"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}},
// Signature
{"system.settings", {PermissionCategory::Signature, "Modify system settings"}},
{"system.install", {PermissionCategory::Signature, "Install apps"}},
{"system.admin", {PermissionCategory::Signature, "Device administrator"}},
};
PermissionGate::PermissionGate(const SandboxContext& context)
: m_context(context)
, m_last_gesture(std::chrono::steady_clock::time_point::min())
{
}
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
return PermissionCategory::Dangerous;
}
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::CheckNormalPermission(const std::string& permission) const {
// Normal permissions are auto-granted if declared in manifest
const auto& declared = m_context.permissions;
return std::find(declared.begin(), declared.end(), permission) != declared.end();
}
bool PermissionGate::CheckDangerousPermission(const std::string& permission) const {
// Must be declared in manifest
const auto& declared = m_context.permissions;
if (std::find(declared.begin(), declared.end(), permission) == declared.end()) {
return false;
}
// Must be granted at runtime (or be a system app)
if (m_context.is_system_app) {
return true;
}
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
const auto& declared = m_context.permissions;
return std::find(declared.begin(), declared.end(), permission) != declared.end();
}
void PermissionGate::RecordUserGesture() {
m_last_gesture = std::chrono::steady_clock::now();
}
bool PermissionGate::HasRecentUserGesture(int ms) const {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_last_gesture);
return elapsed.count() < ms;
}
void PermissionGate::GrantPermission(const std::string& permission) {
m_runtime_grants.insert(permission);
}
void PermissionGate::RevokePermission(const std::string& permission) {
m_runtime_grants.erase(permission);
}
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;
}
int RequirePermission(lua_State* L, const char* permission) {
// Get sandbox 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, "sandbox not initialized");
}
// TODO: Get PermissionGate from sandbox once integrated
// For now, this is a placeholder
return 0;
}
} // namespace mosis
```
---
## Test Cases
### Test 1: Normal Permission Auto-Granted
**Script**: `scripts/test_permission_normal.lua`
```lua
-- Test that normal permissions are auto-granted when declared
-- This is called from C++ which sets up context with "internet" permission
-- If we get here, permission check passed
print("PASS: Normal permission granted")
```
**C++ Test**:
```cpp
bool Test_NormalPermissionAutoGranted(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {"internet"}; // Declare normal permission
LuaSandbox sandbox(ctx);
PermissionGate gate(ctx);
// Normal permissions should be auto-granted
EXPECT_TRUE(gate.HasPermission("internet"));
return true;
}
```
### Test 2: Dangerous Permission Requires Grant
**C++ Test**:
```cpp
bool Test_DangerousPermissionRequiresGrant(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {"camera"}; // Declare dangerous permission
PermissionGate gate(ctx);
// Not granted yet
EXPECT_FALSE(gate.HasPermission("camera"));
// Grant at runtime
gate.GrantPermission("camera");
// Now should have it
EXPECT_TRUE(gate.HasPermission("camera"));
// Revoke
gate.RevokePermission("camera");
EXPECT_FALSE(gate.HasPermission("camera"));
return true;
}
```
### Test 3: Signature Permission System Only
**C++ Test**:
```cpp
bool Test_SignaturePermissionSystemOnly(std::string& error_msg) {
// Non-system app
SandboxContext ctx = TestContext();
ctx.permissions = {"system.settings"};
ctx.is_system_app = false;
PermissionGate gate(ctx);
EXPECT_FALSE(gate.HasPermission("system.settings"));
// System app
SandboxContext sys_ctx = TestContext();
sys_ctx.permissions = {"system.settings"};
sys_ctx.is_system_app = true;
PermissionGate sys_gate(sys_ctx);
EXPECT_TRUE(sys_gate.HasPermission("system.settings"));
return true;
}
```
### Test 4: User Gesture Tracking
**C++ Test**:
```cpp
bool Test_UserGestureRequired(std::string& error_msg) {
SandboxContext ctx = TestContext();
PermissionGate gate(ctx);
// No recent gesture
EXPECT_FALSE(gate.HasRecentUserGesture(5000));
// Record gesture
gate.RecordUserGesture();
// Should have recent gesture
EXPECT_TRUE(gate.HasRecentUserGesture(5000));
return true;
}
```
### Test 5: Undeclared Permission Denied
**C++ Test**:
```cpp
bool Test_UndeclaredPermissionDenied(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {}; // No permissions declared
PermissionGate gate(ctx);
// Even normal permissions need to be declared
EXPECT_FALSE(gate.HasPermission("internet"));
return true;
}
```
---
## Build & Test Commands
### Build
```bash
cd sandbox-test
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
cmake --build build --config Debug
```
### Run All Tests
```bash
./build/Debug/sandbox-test.exe
```
### Run Permission Tests Only
```bash
./build/Debug/sandbox-test.exe --test Permission
```
---
## Acceptance Criteria
All tests must pass:
- [x] `Test_NormalPermissionAutoGranted` - Normal perms auto-granted when declared
- [x] `Test_DangerousPermissionRequiresGrant` - Dangerous perms need runtime grant
- [x] `Test_SignaturePermissionSystemOnly` - Signature perms only for system apps
- [x] `Test_UserGestureTracking` - User gesture tracking works
- [x] `Test_UndeclaredPermissionDenied` - Undeclared perms always denied
- [x] `Test_SystemAppGetsDangerousAuto` - System apps get dangerous perms auto
- [x] `Test_PermissionCategoryCheck` - Permission categories are correct
---
## Integration Notes
After Milestone 2:
1. Add `PermissionGate` to `LuaSandbox` class
2. Call `RequirePermission()` before sensitive operations
3. Wire up user gesture recording from touch events
---
## Next Steps
After Milestone 2 passes:
1. Milestone 3: Audit Logging & Rate Limiting
2. Use permission gate in all subsequent API implementations

357
SANDBOX_MILESTONE_3.md Normal file
View File

@@ -0,0 +1,357 @@
# Milestone 3: Audit Logging & Rate Limiting
**Status**: Complete ✓
**Goal**: Track security events and prevent API abuse.
---
## Overview
This milestone adds security event logging and rate limiting to prevent abuse. The audit log tracks permission checks, sandbox violations, and resource usage. The rate limiter uses a token bucket algorithm to limit API call frequency.
### Key Deliverables
1. **AuditLog class** - Security event logging with ring buffer
2. **RateLimiter class** - Token bucket rate limiting
3. **Thread safety** - Concurrent access support
---
## File Structure
```
src/main/cpp/sandbox/
├── lua_sandbox.h # (existing)
├── lua_sandbox.cpp # (existing)
├── permission_gate.h # (existing)
├── permission_gate.cpp # (existing)
├── audit_log.h # NEW - Audit logging
├── audit_log.cpp # NEW - Implementation
├── rate_limiter.h # NEW - Rate limiting
└── rate_limiter.cpp # NEW - Implementation
```
---
## Implementation Details
### 1. AuditEvent Enum
```cpp
// audit_log.h
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
};
```
### 2. AuditLog Class
```cpp
// audit_log.h
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <chrono>
namespace mosis {
enum class AuditEvent { /* ... */ };
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
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 CountEvents(AuditEvent event, const std::string& app_id = "") const;
// Clear
void Clear();
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;
};
// Global audit log (singleton pattern)
AuditLog& GetAuditLog();
} // namespace mosis
```
### 3. RateLimiter Class (Token Bucket)
```cpp
// rate_limiter.h
#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
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 current token count
double GetTokens(const std::string& app_id, const std::string& operation) const;
// Reset an app's tokens (e.g., on app restart)
void ResetApp(const std::string& app_id);
private:
struct Bucket {
double tokens;
std::chrono::steady_clock::time_point last_refill;
};
void Refill(Bucket& bucket, const RateLimitConfig& config) const;
Bucket& GetBucket(const std::string& app_id, const std::string& operation);
mutable std::mutex m_mutex;
std::unordered_map<std::string, RateLimitConfig> m_configs;
std::unordered_map<std::string, Bucket> m_buckets; // Key: app_id:operation
};
// Global rate limiter
RateLimiter& GetRateLimiter();
} // namespace mosis
```
### 4. Default Rate Limits
```cpp
// rate_limiter.cpp
RateLimiter::RateLimiter() {
// Network
SetLimit("network.request", {10.0, 100.0}); // 10/sec, burst 100
SetLimit("network.websocket", {2.0, 10.0}); // 2/sec, burst 10
// Storage
SetLimit("storage.read", {100.0, 500.0}); // 100/sec, burst 500
SetLimit("storage.write", {20.0, 100.0}); // 20/sec, burst 100
SetLimit("database.query", {50.0, 200.0}); // 50/sec, burst 200
// Hardware
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
// Timers
SetLimit("timer.create", {10.0, 100.0}); // 10/sec, burst 100
}
```
---
## Test Cases
### Test 1: Audit Log Basic
```cpp
bool Test_AuditLogBasic(std::string& error_msg) {
mosis::AuditLog log(1000);
log.Log(mosis::AuditEvent::AppStart, "test.app", "App started");
log.Log(mosis::AuditEvent::PermissionCheck, "test.app", "camera", true);
log.Log(mosis::AuditEvent::PermissionDenied, "test.app", "microphone", false);
auto entries = log.GetEntries(10);
EXPECT_TRUE(entries.size() == 3);
auto app_entries = log.GetEntriesForApp("test.app", 10);
EXPECT_TRUE(app_entries.size() == 3);
return true;
}
```
### Test 2: Audit Log Ring Buffer
```cpp
bool Test_AuditLogRingBuffer(std::string& error_msg) {
mosis::AuditLog log(100); // Small buffer
// Log more than capacity
for (int i = 0; i < 200; i++) {
log.Log(mosis::AuditEvent::Custom, "test.app", std::to_string(i));
}
// Should only have latest 100
auto entries = log.GetEntries(200);
EXPECT_TRUE(entries.size() == 100);
// Total logged should be 200
EXPECT_TRUE(log.GetTotalEntries() == 200);
return true;
}
```
### Test 3: Rate Limiter Basic
```cpp
bool Test_RateLimiterBasic(std::string& error_msg) {
mosis::RateLimiter limiter;
// Should succeed initially (has tokens)
EXPECT_TRUE(limiter.Check("test.app", "network.request"));
return true;
}
```
### Test 4: Rate Limiter Exhaustion
```cpp
bool Test_RateLimiterExhaustion(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {0.0, 5.0}); // 5 tokens, no refill
// Use all tokens
for (int i = 0; i < 5; i++) {
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
}
// Should be denied now
EXPECT_FALSE(limiter.Check("test.app", "test.op"));
return true;
}
```
### Test 5: Rate Limiter Refill
```cpp
bool Test_RateLimiterRefill(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {1000.0, 1.0}); // 1000/sec, max 1
// Use the token
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
EXPECT_FALSE(limiter.Check("test.app", "test.op"));
// Wait a bit for refill (1ms = 1 token at 1000/sec)
std::this_thread::sleep_for(std::chrono::milliseconds(2));
// Should have token again
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
return true;
}
```
### Test 6: App Isolation
```cpp
bool Test_RateLimiterAppIsolation(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {0.0, 1.0}); // 1 token, no refill
// App 1 uses its token
EXPECT_TRUE(limiter.Check("app1", "test.op"));
EXPECT_FALSE(limiter.Check("app1", "test.op"));
// App 2 should still have its token
EXPECT_TRUE(limiter.Check("app2", "test.op"));
return true;
}
```
---
## Acceptance Criteria
All tests must pass:
- [x] `Test_AuditLogBasic` - Logging and retrieval work
- [x] `Test_AuditLogRingBuffer` - Ring buffer wraps correctly
- [x] `Test_AuditLogThreadSafe` - Thread safety verified
- [x] `Test_RateLimiterBasic` - Basic rate limiting works
- [x] `Test_RateLimiterExhaustion` - Tokens exhaust correctly
- [x] `Test_RateLimiterRefill` - Tokens refill over time
- [x] `Test_RateLimiterAppIsolation` - Apps have separate buckets
- [x] `Test_RateLimiterReset` - App reset clears buckets
- [x] `Test_RateLimiterNoConfig` - Unconfigured ops allowed
---
## Next Steps
After Milestone 3 passes:
1. Integrate AuditLog into LuaSandbox and PermissionGate
2. Integrate RateLimiter into API implementations
3. Milestone 4: Safe Path & Require

241
SANDBOX_MILESTONE_4.md Normal file
View File

@@ -0,0 +1,241 @@
# Milestone 4: Safe Path & Require
**Status**: Complete ✓
**Goal**: Secure file access within app sandbox.
---
## Overview
This milestone implements path validation to prevent directory traversal attacks and a safe `require()` function that loads Lua modules only from the app's scripts directory.
### Key Deliverables
1. **PathSandbox class** - Path validation and canonicalization
2. **SafeRequire function** - Secure module loader
3. **Module caching** - Registry-based cache for loaded modules
---
## File Structure
```
src/main/cpp/sandbox/
├── lua_sandbox.h # (existing)
├── lua_sandbox.cpp # (existing)
├── permission_gate.h # (existing)
├── permission_gate.cpp # (existing)
├── audit_log.h # (existing)
├── audit_log.cpp # (existing)
├── rate_limiter.h # (existing)
├── rate_limiter.cpp # (existing)
├── path_sandbox.h # NEW - Path validation
└── path_sandbox.cpp # NEW - Implementation
```
---
## Implementation Details
### 1. PathSandbox Class
```cpp
// path_sandbox.h
#pragma once
#include <string>
#include <filesystem>
namespace mosis {
class PathSandbox {
public:
explicit PathSandbox(const std::string& app_path);
// Validate a path is within the sandbox
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);
// 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;
};
} // namespace mosis
```
### 2. SafeRequire Function
```cpp
// path_sandbox.cpp
// Safe require implementation for Lua
int SafeRequire(lua_State* L);
// Register safe require as global
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox);
```
### 3. Module Name Validation
Valid module names contain only:
- Alphanumeric characters (a-z, A-Z, 0-9)
- Underscores (_)
- Dots (.) for submodules
Examples:
- `utils` → loads `scripts/utils.lua`
- `ui.button` → loads `scripts/ui/button.lua`
---
## Test Cases
### Test 1: Rejects Directory Traversal
```cpp
bool Test_PathRejectsTraversal(std::string& error_msg) {
mosis::PathSandbox sandbox("D:/test/app");
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("../etc/passwd"));
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("foo/../../../bar"));
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..\\windows\\system32"));
std::string canonical;
EXPECT_FALSE(sandbox.ValidatePath("../etc/passwd", canonical));
EXPECT_FALSE(sandbox.ValidatePath("data/../../../etc/passwd", canonical));
return true;
}
```
### Test 2: Rejects Absolute Paths
```cpp
bool Test_PathRejectsAbsolute(std::string& error_msg) {
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("/etc/passwd"));
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("C:\\Windows\\System32"));
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("D:/test/file.txt"));
EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("scripts/utils.lua"));
EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("./data/file.txt"));
return true;
}
```
### Test 3: Accepts Valid Paths
```cpp
bool Test_PathAcceptsValid(std::string& error_msg) {
mosis::PathSandbox sandbox("D:/test/app");
std::string canonical;
EXPECT_TRUE(sandbox.ValidatePath("scripts/utils.lua", canonical));
EXPECT_TRUE(sandbox.ValidatePath("data/config.json", canonical));
EXPECT_TRUE(sandbox.ValidatePath("./scripts/ui/button.lua", canonical));
return true;
}
```
### Test 4: Safe Require Loads Modules
```cpp
bool Test_SafeRequireLoads(std::string& error_msg) {
// Create sandbox with test scripts directory
SandboxContext ctx = TestContext();
ctx.app_path = GetScriptsDir(); // Use scripts/ as app path
LuaSandbox sandbox(ctx);
// Should be able to require a test module
std::string script = R"(
local m = require("test_module")
return m.value == 42
)";
EXPECT_TRUE(sandbox.LoadString(script, "require_test"));
return true;
}
```
### Test 5: Safe Require Caches Modules
```cpp
bool Test_SafeRequireCaches(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.app_path = GetScriptsDir();
LuaSandbox sandbox(ctx);
std::string script = R"(
local m1 = require("test_module")
local m2 = require("test_module")
return m1 == m2 -- Should be same table (cached)
)";
EXPECT_TRUE(sandbox.LoadString(script, "cache_test"));
return true;
}
```
### Test 6: Safe Require Rejects Invalid Names
```cpp
bool Test_SafeRequireRejectsInvalid(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.app_path = GetScriptsDir();
LuaSandbox sandbox(ctx);
// Should reject path traversal in module name
EXPECT_FALSE(sandbox.LoadString("require('../evil')", "evil_require"));
// Should reject absolute paths
EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')", "abs_require"));
// Should reject special characters
EXPECT_FALSE(sandbox.LoadString("require('foo;bar')", "special_require"));
return true;
}
```
---
## Acceptance Criteria
All tests pass:
- [x] `Test_PathRejectsTraversal` - Block .. traversal
- [x] `Test_PathRejectsAbsolute` - Block absolute paths
- [x] `Test_PathAcceptsValid` - Allow valid relative paths
- [x] `Test_ModuleNameValidation` - Validate module names
- [x] `Test_ModuleToPath` - Convert module names to paths
- [x] `Test_SafeRequireLoads` - Load modules from scripts/
- [x] `Test_SafeRequireCaches` - Cache loaded modules
- [x] `Test_SafeRequireRejectsInvalid` - Reject malicious require calls
---
## Next Steps
After Milestone 4 passes:
1. Milestone 5: Timer & Callback System
2. Milestone 6: JSON & Crypto APIs

333
SANDBOX_MILESTONE_5.md Normal file
View File

@@ -0,0 +1,333 @@
# Milestone 5: Timer & Callback System
**Status**: Complete
**Goal**: Safe timer APIs managed by kernel.
---
## Overview
This milestone implements JavaScript-style timer APIs (`setTimeout`, `setInterval`) that are:
- Managed by the kernel (not Lua coroutines)
- Subject to per-app limits
- Properly cleaned up when apps stop
- Integrated with sandbox instruction counting
### Key Deliverables
1. **TimerManager class** - Central timer management
2. **Lua timer APIs** - setTimeout, setInterval, clearTimeout, clearInterval
3. **Kernel integration** - Fire timers from main loop
---
## File Structure
```
src/main/cpp/sandbox/
├── lua_sandbox.h # (existing)
├── lua_sandbox.cpp # (existing)
├── timer_manager.h # NEW - Timer management
└── timer_manager.cpp # NEW - Implementation
```
---
## Implementation Details
### 1. TimerManager Class
```cpp
// timer_manager.h
#pragma once
#include <string>
#include <functional>
#include <queue>
#include <unordered_map>
#include <chrono>
#include <mutex>
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;
};
class TimerManager {
public:
TimerManager();
~TimerManager();
// Create timers (returns timer ID)
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
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:
struct TimerCompare {
bool operator()(const Timer& a, const Timer& b) const {
return a.fire_time > b.fire_time; // Min-heap
}
};
TimerId m_next_id = 1;
std::priority_queue<Timer, std::vector<Timer>, TimerCompare> m_timers;
std::unordered_map<std::string, size_t> m_app_timer_counts;
mutable std::mutex m_mutex;
void FireTimer(Timer& timer);
};
// Lua API registration
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id);
} // namespace mosis
```
### 2. Lua Timer APIs
```lua
-- setTimeout: fire callback once after delay
local id = setTimeout(function()
print("Fired after 1000ms")
end, 1000)
-- clearTimeout: cancel a pending timeout
clearTimeout(id)
-- setInterval: fire callback repeatedly
local id = setInterval(function()
print("Fires every 500ms")
end, 500)
-- clearInterval: stop an interval
clearInterval(id)
```
### 3. Timer Limits
| Limit | Value | Reason |
|-------|-------|--------|
| Max timers per app | 100 | Prevent resource exhaustion |
| Min interval | 10ms | Prevent CPU spinning |
| Min timeout | 0ms | Allow immediate callbacks |
---
## Test Cases
### Test 1: SetTimeout Fires
```cpp
bool Test_SetTimeoutFires(std::string& error_msg) {
mosis::TimerManager manager;
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Register timer API
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Set a timeout
sandbox.LoadString(R"(
fired = false
setTimeout(function() fired = true end, 50)
)", "timeout_test");
// Process timers after delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
manager.ProcessTimers();
// Check if callback fired
sandbox.LoadString("assert(fired == true)", "check");
return true;
}
```
### Test 2: SetInterval Fires Multiple Times
```cpp
bool Test_SetIntervalFires(std::string& error_msg) {
mosis::TimerManager manager;
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
sandbox.LoadString(R"(
count = 0
setInterval(function() count = count + 1 end, 30)
)", "interval_test");
// Process multiple times
for (int i = 0; i < 5; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(40));
manager.ProcessTimers();
}
// Should have fired multiple times
sandbox.LoadString("assert(count >= 3)", "check");
return true;
}
```
### Test 3: ClearTimeout Cancels
```cpp
bool Test_ClearTimeoutCancels(std::string& error_msg) {
mosis::TimerManager manager;
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
sandbox.LoadString(R"(
fired = false
local id = setTimeout(function() fired = true end, 100)
clearTimeout(id)
)", "clear_test");
std::this_thread::sleep_for(std::chrono::milliseconds(150));
manager.ProcessTimers();
// Should NOT have fired
sandbox.LoadString("assert(fired == false)", "check");
return true;
}
```
### Test 4: Timer Limit Enforced
```cpp
bool Test_TimerLimitEnforced(std::string& error_msg) {
mosis::TimerManager manager;
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Try to create too many timers
sandbox.LoadString(R"(
for i = 1, 150 do
setTimeout(function() end, 1000000)
end
)", "limit_test");
// Should be capped at MAX_TIMERS_PER_APP
EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) <= 100);
return true;
}
```
### Test 5: ClearAppTimers Cleanup
```cpp
bool Test_ClearAppTimersCleanup(std::string& error_msg) {
mosis::TimerManager manager;
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
sandbox.LoadString(R"(
for i = 1, 10 do
setTimeout(function() end, 1000000)
end
)", "cleanup_test");
EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) == 10);
// Clear all timers for app
manager.ClearAppTimers(ctx.app_id);
EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) == 0);
return true;
}
```
### Test 6: Minimum Interval Enforced
```cpp
bool Test_MinIntervalEnforced(std::string& error_msg) {
mosis::TimerManager manager;
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Try to set interval less than minimum
sandbox.LoadString(R"(
count = 0
setInterval(function() count = count + 1 end, 1) -- 1ms, should be clamped to 10ms
)", "min_interval_test");
// With 1ms interval, in 50ms we'd get 50 callbacks
// With 10ms minimum, we should get ~5
std::this_thread::sleep_for(std::chrono::milliseconds(55));
for (int i = 0; i < 10; i++) {
manager.ProcessTimers();
}
sandbox.LoadString("assert(count <= 10)", "check");
return true;
}
```
---
## Acceptance Criteria
All tests must pass:
- [x] `Test_SetTimeoutFires` - Timeout fires after delay
- [x] `Test_SetIntervalFires` - Interval fires repeatedly
- [x] `Test_ClearTimeoutCancels` - Cancelled timeout doesn't fire
- [x] `Test_ClearIntervalCancels` - Cancelled interval stops
- [x] `Test_TimerLimitEnforced` - Max 100 timers per app
- [x] `Test_ClearAppTimersCleanup` - All app timers cleared on stop
- [x] `Test_MinIntervalEnforced` - Interval clamped to 10ms minimum
---
## Next Steps
After Milestone 5 passes:
1. Milestone 6: JSON & Crypto APIs
2. Milestone 7: Virtual Filesystem

View File

@@ -0,0 +1,54 @@
cmake_minimum_required(VERSION 3.22.1)
project(sandbox-test)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find dependencies via vcpkg
find_package(Lua REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
# Sandbox library (the code being tested)
add_library(mosis-sandbox STATIC
../src/main/cpp/sandbox/lua_sandbox.cpp
../src/main/cpp/sandbox/permission_gate.cpp
../src/main/cpp/sandbox/audit_log.cpp
../src/main/cpp/sandbox/rate_limiter.cpp
../src/main/cpp/sandbox/path_sandbox.cpp
../src/main/cpp/sandbox/timer_manager.cpp
)
target_include_directories(mosis-sandbox PUBLIC
../src/main/cpp/sandbox
${LUA_INCLUDE_DIR}
)
target_link_libraries(mosis-sandbox PUBLIC
${LUA_LIBRARIES}
)
# Test executable
add_executable(sandbox-test
src/main.cpp
src/test_harness.cpp
)
target_include_directories(sandbox-test PRIVATE
src
../src/main/cpp/sandbox
)
target_link_libraries(sandbox-test PRIVATE
mosis-sandbox
nlohmann_json::nlohmann_json
)
# Copy test scripts to build directory
add_custom_command(TARGET sandbox-test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/scripts
$<TARGET_FILE_DIR:sandbox-test>/scripts
)
# Windows-specific
if(WIN32)
target_compile_definitions(sandbox-test PRIVATE _CRT_SECURE_NO_WARNINGS)
endif()

132
sandbox-test/README.md Normal file
View File

@@ -0,0 +1,132 @@
# Sandbox Security Tests
Automated tests for the Mosis Lua sandbox security implementation.
## Prerequisites
- CMake 3.22+
- vcpkg with packages: `lua`, `nlohmann-json`
- MSVC or compatible C++23 compiler
## Build
```bash
# From sandbox-test directory
cd D:\Dev\Mosis\MosisService\sandbox-test
# Configure with vcpkg
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
# Build
cmake --build build --config Debug
```
## Run Tests
### Run All Tests (Uber Command)
```bash
# Windows
.\run_tests.bat
# Or directly
.\build\Debug\sandbox-test.exe
```
### Run Specific Test
```bash
.\build\Debug\sandbox-test.exe --test DangerousGlobals
.\build\Debug\sandbox-test.exe --test Memory
.\build\Debug\sandbox-test.exe --test CPU
```
### Custom Output File
```bash
.\build\Debug\sandbox-test.exe --output my_results.json
```
## Test List
| Test Name | Description | Script |
|-----------|-------------|--------|
| `DangerousGlobalsRemoved` | Verifies os, io, debug, etc. are nil | `test_globals_removed.lua` |
| `BytecodeRejected` | Verifies binary Lua chunks are rejected | (C++ only) |
| `MemoryLimitEnforced` | Verifies memory allocation limit works | `test_memory_limit.lua` |
| `CPULimitEnforced` | Verifies instruction count limit works | `test_cpu_limit.lua` |
| `MetatableProtected` | Verifies _G and string metatable are frozen | `test_metatable_protected.lua` |
| `SafeOperationsWork` | Verifies normal Lua operations still work | `test_safe_operations.lua` |
| `StringDumpRemoved` | Verifies string.dump is nil | `test_string_dump_removed.lua` |
| `MemoryTracking` | Verifies memory usage is tracked | (C++ only) |
| `InstructionCounting` | Verifies instruction count is tracked | (C++ only) |
| `MultipleLoads` | Verifies multiple scripts can be loaded | (C++ only) |
| `ErrorRecovery` | Verifies sandbox recovers from errors | (C++ only) |
## Output Format
Tests produce a JSON report at `test_results.json`:
```json
{
"name": "Lua Sandbox Security Tests",
"timestamp": "2024-01-15T10:30:00Z",
"summary": {
"passed": 11,
"failed": 0,
"total": 11
},
"tests": [
{
"name": "DangerousGlobalsRemoved",
"status": "passed",
"duration_ms": 5
}
]
}
```
## Exit Codes
- `0` - All tests passed
- `1` - One or more tests failed
## Adding New Tests
1. Create Lua script in `scripts/` directory
2. Add C++ test function in `main.cpp`:
```cpp
bool Test_MyNewTest(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
// ... test logic
return true;
}
```
3. Register in `main()`:
```cpp
harness.AddTest("MyNewTest", Test_MyNewTest);
```
## Debugging Failed Tests
1. Run specific test: `--test TestName`
2. Check Lua script in `scripts/` for expected behavior
3. Check `test_results.json` for error details
4. Add print statements to Lua scripts (output goes to console)
## CI Integration
```bash
# In CI script
cd sandbox-test
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
cmake --build build --config Release
./build/Release/sandbox-test.exe --output ci_results.json
# Check exit code
if [ $? -ne 0 ]; then
echo "Sandbox tests failed!"
cat ci_results.json
exit 1
fi
```

View File

@@ -0,0 +1,59 @@
@echo off
setlocal
echo ========================================
echo MOSIS SANDBOX TEST RUNNER
echo ========================================
echo.
REM Check if build exists
if not exist "build\Debug\sandbox-test.exe" (
echo Build not found. Building...
echo.
REM Check VCPKG_ROOT
if "%VCPKG_ROOT%"=="" (
echo ERROR: VCPKG_ROOT environment variable not set
exit /b 1
)
REM Configure
echo Configuring CMake...
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
if errorlevel 1 (
echo ERROR: CMake configure failed
exit /b 1
)
REM Build
echo Building...
cmake --build build --config Debug
if errorlevel 1 (
echo ERROR: Build failed
exit /b 1
)
echo.
)
REM Run tests
echo Running tests...
echo.
cd build\Debug
sandbox-test.exe %*
set TEST_RESULT=%errorlevel%
cd ..\..
echo.
if %TEST_RESULT% equ 0 (
echo ========================================
echo ALL TESTS PASSED
echo ========================================
) else (
echo ========================================
echo SOME TESTS FAILED
echo ========================================
)
exit /b %TEST_RESULT%

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env pwsh
# Sandbox Test Runner for PowerShell
Write-Host "========================================"
Write-Host " MOSIS SANDBOX TEST RUNNER"
Write-Host "========================================"
Write-Host ""
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Push-Location $scriptDir
try {
# Check if build exists
if (-not (Test-Path "build/Debug/sandbox-test.exe")) {
Write-Host "Build not found. Building..."
Write-Host ""
# Check VCPKG_ROOT
if (-not $env:VCPKG_ROOT) {
Write-Error "VCPKG_ROOT environment variable not set"
exit 1
}
# Configure
Write-Host "Configuring CMake..."
cmake -B build "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
if ($LASTEXITCODE -ne 0) {
Write-Error "CMake configure failed"
exit 1
}
# Build
Write-Host "Building..."
cmake --build build --config Debug
if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
Write-Host ""
}
# Run tests
Write-Host "Running tests..."
Write-Host ""
Push-Location "build/Debug"
& ./sandbox-test.exe @args
$testResult = $LASTEXITCODE
Pop-Location
Write-Host ""
if ($testResult -eq 0) {
Write-Host "========================================"
Write-Host " ALL TESTS PASSED"
Write-Host "========================================"
} else {
Write-Host "========================================"
Write-Host " SOME TESTS FAILED"
Write-Host "========================================"
}
exit $testResult
}
finally {
Pop-Location
}

View File

@@ -0,0 +1,11 @@
-- Test module for SafeRequire tests
local M = {}
M.value = 42
M.name = "test_module"
function M.add(a, b)
return a + b
end
return M

View File

@@ -0,0 +1,5 @@
-- This script tests that text loading works
-- The actual bytecode rejection test is done from C++ side
-- by attempting to load a bytecode string directly
print("PASS: Text loading works")

View File

@@ -0,0 +1,12 @@
-- This script runs an infinite loop
-- It should be stopped by the instruction limit hook
local count = 0
while true do
count = count + 1
-- This loop should be interrupted by instruction hook
end
-- Should never reach here
error("FAIL: CPU limit not enforced - loop completed")

View File

@@ -0,0 +1,26 @@
-- Test that dangerous globals are nil
-- This script should run successfully if sandbox is properly configured
-- Note: 'require' is intentionally NOT in this list because the sandbox
-- provides a safe version when app_path is configured
local dangerous = {
"os", "io", "debug", "package", "ffi", "jit",
"dofile", "loadfile", "load", "loadstring",
"rawget", "rawset", "rawequal", "rawlen",
"collectgarbage", "newproxy"
}
local failed = {}
for _, name in ipairs(dangerous) do
local value = _G[name]
if value ~= nil then
table.insert(failed, name .. " (is " .. type(value) .. ")")
end
end
if #failed > 0 then
error("FAIL: These globals should be nil: " .. table.concat(failed, ", "))
end
print("PASS: All dangerous globals removed")

View File

@@ -0,0 +1,20 @@
-- This script intentionally tries to exhaust memory
-- When run with a 512KB limit, it should fail before completing
local t = {}
local i = 0
while true do
i = i + 1
-- Each string is 100KB
t[i] = string.rep("x", 100000)
-- Safety check - if we get past 100 iterations with 512KB limit,
-- something is wrong
if i > 100 then
error("FAIL: Should have hit memory limit by now (allocated ~10MB)")
end
end
-- Should never reach here
error("FAIL: Memory limit not enforced")

View File

@@ -0,0 +1,33 @@
-- Test that metatables are protected from manipulation
-- Test 1: String metatable should return protection value, not actual metatable
local mt = getmetatable("")
if mt ~= "string" then
error("FAIL: string metatable should return 'string', got " .. tostring(mt))
end
-- Test 2: Cannot add new globals
local ok, err = pcall(function()
_G.my_new_global = "test"
end)
if ok then
error("FAIL: Should not be able to add new globals")
end
-- Test 3: Cannot modify existing globals
local ok2, err2 = pcall(function()
_G.print = nil
end)
if ok2 then
error("FAIL: Should not be able to modify print")
end
-- Test 4: Cannot replace math table
local ok3, err3 = pcall(function()
_G.math = {}
end)
if ok3 then
error("FAIL: Should not be able to replace math")
end
print("PASS: Metatables protected")

View File

@@ -0,0 +1,158 @@
-- Test that safe/normal Lua operations still work correctly
local function check(cond, msg)
if not cond then
error("FAIL: " .. msg)
end
end
-- ============================================
-- MATH OPERATIONS
-- ============================================
local x = math.sin(1.5) + math.floor(3.7)
check(type(x) == "number", "Math operations failed")
check(math.abs(-5) == 5, "math.abs failed")
check(math.max(1, 2, 3) == 3, "math.max failed")
check(math.min(1, 2, 3) == 1, "math.min failed")
check(math.floor(3.9) == 3, "math.floor failed")
check(math.ceil(3.1) == 4, "math.ceil failed")
-- ============================================
-- STRING OPERATIONS
-- ============================================
local s = string.format("hello %d", 42)
check(s == "hello 42", "string.format failed")
local upper = string.upper("test")
check(upper == "TEST", "string.upper failed")
local lower = string.lower("TEST")
check(lower == "test", "string.lower failed")
local sub = string.sub("hello", 2, 4)
check(sub == "ell", "string.sub failed")
local len = string.len("hello")
check(len == 5, "string.len failed")
local rep = string.rep("ab", 3)
check(rep == "ababab", "string.rep failed")
local rev = string.reverse("hello")
check(rev == "olleh", "string.reverse failed")
-- ============================================
-- TABLE OPERATIONS
-- ============================================
local t = {1, 2, 3}
table.insert(t, 4)
check(#t == 4, "table.insert failed")
check(t[4] == 4, "table.insert value failed")
local removed = table.remove(t)
check(removed == 4, "table.remove failed")
check(#t == 3, "table.remove length failed")
local t2 = {3, 1, 2}
table.sort(t2)
check(t2[1] == 1 and t2[2] == 2 and t2[3] == 3, "table.sort failed")
local concat = table.concat({"a", "b", "c"}, ",")
check(concat == "a,b,c", "table.concat failed")
-- ============================================
-- ITERATION
-- ============================================
local count = 0
for i, v in ipairs({1, 2, 3, 4}) do
count = count + 1
end
check(count == 4, "ipairs iteration failed")
count = 0
for k, v in pairs({a=1, b=2, c=3}) do
count = count + 1
end
check(count == 3, "pairs iteration failed")
-- next function
local t3 = {a=1, b=2}
local k, v = next(t3)
check(k ~= nil and v ~= nil, "next function failed")
-- ============================================
-- ERROR HANDLING
-- ============================================
local ok, err = pcall(function()
error("test error")
end)
check(not ok, "pcall should return false for error")
check(err:find("test error"), "Error message should contain 'test error'")
local ok2, result = pcall(function()
return 42
end)
check(ok2 and result == 42, "pcall should return success value")
-- xpcall with traceback
local ok3, err3 = xpcall(function()
error("xpcall test")
end, function(e)
return "caught: " .. tostring(e)
end)
check(not ok3, "xpcall should return false for error")
check(err3:find("caught"), "xpcall error handler should run")
-- ============================================
-- TYPE CHECKS
-- ============================================
check(type({}) == "table", "type table failed")
check(type("") == "string", "type string failed")
check(type(123) == "number", "type number failed")
check(type(true) == "boolean", "type boolean failed")
check(type(nil) == "nil", "type nil failed")
check(type(function() end) == "function", "type function failed")
-- ============================================
-- CONVERSION
-- ============================================
check(tonumber("42") == 42, "tonumber string failed")
check(tonumber("3.14") == 3.14, "tonumber float failed")
check(tonumber("abc") == nil, "tonumber invalid failed")
check(tonumber(42) == 42, "tonumber number failed")
check(tostring(42) == "42", "tostring number failed")
check(tostring(true) == "true", "tostring boolean failed")
check(type(tostring({})) == "string", "tostring table failed")
-- ============================================
-- SELECT
-- ============================================
local a, b = select(2, 1, 2, 3)
check(a == 2 and b == 3, "select failed")
check(select("#", 1, 2, 3, 4) == 4, "select # failed")
-- ============================================
-- ASSERT
-- ============================================
local ok4, err4 = pcall(function()
assert(true, "should not fail")
end)
check(ok4, "assert true failed")
local ok5, err5 = pcall(function()
assert(false, "intentional fail")
end)
check(not ok5, "assert false should fail")
check(err5:find("intentional fail"), "assert message wrong")
-- ============================================
-- UTF8 (if available)
-- ============================================
if utf8 then
local len = utf8.len("hello")
check(len == 5, "utf8.len failed")
end
print("PASS: All safe operations work correctly")

View File

@@ -0,0 +1,18 @@
-- Test that string.dump is removed
-- string.dump can be used to create bytecode from functions,
-- which could be used to bypass sandbox restrictions
if string.dump ~= nil then
error("FAIL: string.dump should be nil but exists")
end
-- Also verify string table exists and other functions work
if string.upper == nil then
error("FAIL: string.upper should exist")
end
if string.format == nil then
error("FAIL: string.format should exist")
end
print("PASS: string.dump removed, other string functions intact")

984
sandbox-test/src/main.cpp Normal file
View File

@@ -0,0 +1,984 @@
#include "test_harness.h"
#include "lua_sandbox.h"
#include "permission_gate.h"
#include <lua.hpp>
#include <iostream>
#include "audit_log.h"
#include "rate_limiter.h"
#include "path_sandbox.h"
#include "timer_manager.h"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <thread>
#include <chrono>
// Get path to scripts directory
std::string GetScriptsDir() {
// Scripts are copied to build directory by CMake
return "scripts";
}
// Helper to create test context
SandboxContext TestContext() {
return SandboxContext{
.app_id = "test.app",
.app_path = ".",
.permissions = {},
.is_system_app = false
};
}
// Helper to read file contents
std::string ReadFile(const std::string& path) {
std::ifstream f(path);
if (!f) return "";
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
// Helper to setup a _test table in real _G for timer tests
// This allows test scripts to store state without triggering the proxy's __newindex
void SetupTestTable(lua_State* L) {
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
lua_newtable(L); // Create _test table
lua_setfield(L, -2, "_test");
lua_pop(L, 3); // pop real _G, metatable, proxy
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, use directly
lua_newtable(L);
lua_setfield(L, -2, "_test");
lua_pop(L, 1); // pop _G
}
//=============================================================================
// TEST DEFINITIONS
//=============================================================================
bool Test_DangerousGlobalsRemoved(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
std::string script = ReadFile(GetScriptsDir() + "/test_globals_removed.lua");
EXPECT_FALSE(script.empty());
EXPECT_TRUE(sandbox.LoadString(script, "test_globals_removed.lua"));
return true;
}
bool Test_BytecodeRejected(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
// Lua 5.4 bytecode signature
std::string bytecode = "\x1bLua\x54\x00\x19\x93\r\n\x1a\n";
EXPECT_FALSE(sandbox.LoadString(bytecode, "bytecode_test"));
// Error should mention binary/bytecode
std::string err = sandbox.GetLastError();
bool mentions_binary = (err.find("binary") != std::string::npos ||
err.find("attempt to load") != std::string::npos ||
err.find("text") != std::string::npos);
EXPECT_TRUE(mentions_binary);
return true;
}
bool Test_MemoryLimitEnforced(std::string& error_msg) {
SandboxLimits limits;
limits.memory_bytes = 512 * 1024; // 512 KB - very small
LuaSandbox sandbox(TestContext(), limits);
std::string script = ReadFile(GetScriptsDir() + "/test_memory_limit.lua");
EXPECT_FALSE(script.empty());
// Should fail due to memory exhaustion
EXPECT_FALSE(sandbox.LoadString(script, "test_memory_limit.lua"));
return true;
}
bool Test_CPULimitEnforced(std::string& error_msg) {
SandboxLimits limits;
limits.instructions_per_call = 10000; // Very low
LuaSandbox sandbox(TestContext(), limits);
std::string script = ReadFile(GetScriptsDir() + "/test_cpu_limit.lua");
EXPECT_FALSE(script.empty());
// Should fail due to instruction limit
EXPECT_FALSE(sandbox.LoadString(script, "test_cpu_limit.lua"));
// Error should mention instructions
std::string err = sandbox.GetLastError();
EXPECT_CONTAINS(err, "instruction");
return true;
}
bool Test_MetatableProtected(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
std::string script = ReadFile(GetScriptsDir() + "/test_metatable_protected.lua");
EXPECT_FALSE(script.empty());
if (!sandbox.LoadString(script, "test_metatable_protected.lua")) {
error_msg = "Script failed: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_SafeOperationsWork(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
std::string script = ReadFile(GetScriptsDir() + "/test_safe_operations.lua");
EXPECT_FALSE(script.empty());
EXPECT_TRUE(sandbox.LoadString(script, "test_safe_operations.lua"));
return true;
}
bool Test_StringDumpRemoved(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
std::string script = ReadFile(GetScriptsDir() + "/test_string_dump_removed.lua");
EXPECT_FALSE(script.empty());
EXPECT_TRUE(sandbox.LoadString(script, "test_string_dump_removed.lua"));
return true;
}
bool Test_MemoryTracking(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
// Initially should have some baseline memory
size_t initial = sandbox.GetMemoryUsed();
EXPECT_TRUE(initial > 0);
// Allocate some data
sandbox.LoadString("local t = {}; for i=1,1000 do t[i] = string.rep('x', 100) end", "alloc");
// Memory should have increased
size_t after = sandbox.GetMemoryUsed();
EXPECT_TRUE(after > initial);
return true;
}
bool Test_InstructionCounting(std::string& error_msg) {
SandboxLimits limits;
limits.instructions_per_call = 1000000; // 1M instructions
LuaSandbox sandbox(TestContext(), limits);
// Run some code
sandbox.LoadString("for i=1,10000 do local x = i * 2 end", "counting");
// Should have used some instructions
EXPECT_TRUE(sandbox.GetInstructionsUsed() > 0);
return true;
}
bool Test_MultipleLoads(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
// Should be able to load multiple scripts
EXPECT_TRUE(sandbox.LoadString("local a = 1", "script1"));
EXPECT_TRUE(sandbox.LoadString("local b = 2", "script2"));
EXPECT_TRUE(sandbox.LoadString("local c = 3", "script3"));
return true;
}
bool Test_ErrorRecovery(std::string& error_msg) {
LuaSandbox sandbox(TestContext());
// Script with error
EXPECT_FALSE(sandbox.LoadString("error('test error')", "error_script"));
// Should still be able to run more code after error
EXPECT_TRUE(sandbox.LoadString("local x = 1", "after_error"));
return true;
}
//=============================================================================
// PERMISSION SYSTEM TESTS (Milestone 2)
//=============================================================================
bool Test_NormalPermissionAutoGranted(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {"internet", "vibrate"}; // Declare normal permissions
mosis::PermissionGate gate(ctx);
// Normal permissions should be auto-granted when declared
EXPECT_TRUE(gate.HasPermission("internet"));
EXPECT_TRUE(gate.HasPermission("vibrate"));
return true;
}
bool Test_DangerousPermissionRequiresGrant(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {"camera"}; // Declare dangerous permission
mosis::PermissionGate gate(ctx);
// Not granted yet (regular app)
EXPECT_FALSE(gate.HasPermission("camera"));
// Grant at runtime
gate.GrantPermission("camera");
// Now should have it
EXPECT_TRUE(gate.HasPermission("camera"));
// Revoke
gate.RevokePermission("camera");
EXPECT_FALSE(gate.HasPermission("camera"));
return true;
}
bool Test_SignaturePermissionSystemOnly(std::string& error_msg) {
// Non-system app
SandboxContext ctx = TestContext();
ctx.permissions = {"system.settings"};
ctx.is_system_app = false;
mosis::PermissionGate gate(ctx);
EXPECT_FALSE(gate.HasPermission("system.settings"));
// System app
SandboxContext sys_ctx = TestContext();
sys_ctx.permissions = {"system.settings"};
sys_ctx.is_system_app = true;
mosis::PermissionGate sys_gate(sys_ctx);
EXPECT_TRUE(sys_gate.HasPermission("system.settings"));
return true;
}
bool Test_UserGestureTracking(std::string& error_msg) {
SandboxContext ctx = TestContext();
mosis::PermissionGate gate(ctx);
// No recent gesture
EXPECT_FALSE(gate.HasRecentUserGesture(5000));
// Record gesture
gate.RecordUserGesture();
// Should have recent gesture
EXPECT_TRUE(gate.HasRecentUserGesture(5000));
// Wait for gesture to expire (use short window)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
EXPECT_FALSE(gate.HasRecentUserGesture(50)); // 50ms window, we waited 100ms
return true;
}
bool Test_UndeclaredPermissionDenied(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {}; // No permissions declared
mosis::PermissionGate gate(ctx);
// Even normal permissions need to be declared
EXPECT_FALSE(gate.HasPermission("internet"));
// Dangerous permissions also denied
EXPECT_FALSE(gate.HasPermission("camera"));
return true;
}
bool Test_SystemAppGetsDangerousAuto(std::string& error_msg) {
// System apps get dangerous permissions automatically (no runtime grant needed)
SandboxContext ctx = TestContext();
ctx.permissions = {"camera", "microphone"};
ctx.is_system_app = true;
mosis::PermissionGate gate(ctx);
// System app should have dangerous perms without explicit grant
EXPECT_TRUE(gate.HasPermission("camera"));
EXPECT_TRUE(gate.HasPermission("microphone"));
return true;
}
bool Test_PermissionCategoryCheck(std::string& error_msg) {
// Check that permission categories are correct
EXPECT_TRUE(mosis::PermissionGate::GetCategory("internet") == mosis::PermissionCategory::Normal);
EXPECT_TRUE(mosis::PermissionGate::GetCategory("camera") == mosis::PermissionCategory::Dangerous);
EXPECT_TRUE(mosis::PermissionGate::GetCategory("system.settings") == mosis::PermissionCategory::Signature);
// Unknown permissions default to Dangerous
EXPECT_TRUE(mosis::PermissionGate::GetCategory("unknown.perm") == mosis::PermissionCategory::Dangerous);
return true;
}
//=============================================================================
// AUDIT LOG TESTS (Milestone 3)
//=============================================================================
bool Test_AuditLogBasic(std::string& error_msg) {
mosis::AuditLog log(1000);
log.Log(mosis::AuditEvent::AppStart, "test.app", "App started");
log.Log(mosis::AuditEvent::PermissionCheck, "test.app", "camera", true);
log.Log(mosis::AuditEvent::PermissionDenied, "test.app", "microphone", false);
auto entries = log.GetEntries(10);
EXPECT_TRUE(entries.size() == 3);
auto app_entries = log.GetEntriesForApp("test.app", 10);
EXPECT_TRUE(app_entries.size() == 3);
// Check event filtering
auto denied_entries = log.GetEntriesByEvent(mosis::AuditEvent::PermissionDenied, 10);
EXPECT_TRUE(denied_entries.size() == 1);
return true;
}
bool Test_AuditLogRingBuffer(std::string& error_msg) {
mosis::AuditLog log(100); // Small buffer
// Log more than capacity
for (int i = 0; i < 200; i++) {
log.Log(mosis::AuditEvent::Custom, "test.app", std::to_string(i));
}
// Should only have latest 100 stored
auto entries = log.GetEntries(200);
EXPECT_TRUE(entries.size() == 100);
// Total logged should be 200
EXPECT_TRUE(log.GetTotalEntries() == 200);
// Most recent should be "199"
EXPECT_TRUE(entries[0].details == "199");
return true;
}
bool Test_AuditLogThreadSafe(std::string& error_msg) {
mosis::AuditLog log(10000);
// Spawn multiple threads logging concurrently
std::vector<std::thread> threads;
for (int t = 0; t < 4; t++) {
threads.emplace_back([&log, t]() {
for (int i = 0; i < 1000; i++) {
log.Log(mosis::AuditEvent::Custom, "app" + std::to_string(t), std::to_string(i));
}
});
}
for (auto& thread : threads) {
thread.join();
}
// Should have logged 4000 entries
EXPECT_TRUE(log.GetTotalEntries() == 4000);
return true;
}
//=============================================================================
// RATE LIMITER TESTS (Milestone 3)
//=============================================================================
bool Test_RateLimiterBasic(std::string& error_msg) {
mosis::RateLimiter limiter;
// Should succeed initially (has tokens)
EXPECT_TRUE(limiter.Check("test.app", "network.request"));
return true;
}
bool Test_RateLimiterExhaustion(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {0.0, 5.0}); // 5 tokens, no refill
// Use all tokens
for (int i = 0; i < 5; i++) {
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
}
// Should be denied now
EXPECT_FALSE(limiter.Check("test.app", "test.op"));
return true;
}
bool Test_RateLimiterRefill(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {1000.0, 1.0}); // 1000/sec, max 1 token
// Use the token
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
EXPECT_FALSE(limiter.Check("test.app", "test.op"));
// Wait a bit for refill (2ms = ~2 tokens at 1000/sec, but max is 1)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// Should have token again
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
return true;
}
bool Test_RateLimiterAppIsolation(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {0.0, 1.0}); // 1 token, no refill
// App 1 uses its token
EXPECT_TRUE(limiter.Check("app1", "test.op"));
EXPECT_FALSE(limiter.Check("app1", "test.op"));
// App 2 should still have its token
EXPECT_TRUE(limiter.Check("app2", "test.op"));
return true;
}
bool Test_RateLimiterReset(std::string& error_msg) {
mosis::RateLimiter limiter;
limiter.SetLimit("test.op", {0.0, 2.0}); // 2 tokens, no refill
// Use all tokens
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
EXPECT_FALSE(limiter.Check("test.app", "test.op"));
// Reset the app
limiter.ResetApp("test.app");
// Should have tokens again
EXPECT_TRUE(limiter.Check("test.app", "test.op"));
return true;
}
bool Test_RateLimiterNoConfig(std::string& error_msg) {
mosis::RateLimiter limiter;
// Operation with no config should always succeed
for (int i = 0; i < 100; i++) {
EXPECT_TRUE(limiter.Check("test.app", "unconfigured.operation"));
}
return true;
}
//=============================================================================
// PATH SANDBOX TESTS (Milestone 4)
//=============================================================================
bool Test_PathRejectsTraversal(std::string& error_msg) {
mosis::PathSandbox sandbox("D:/test/app");
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("../etc/passwd"));
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("foo/../../../bar"));
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..\\windows\\system32"));
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("data/.."));
EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal(".."));
// Should not match ".." in filenames
EXPECT_FALSE(mosis::PathSandbox::ContainsTraversal("file..txt"));
EXPECT_FALSE(mosis::PathSandbox::ContainsTraversal("test...name"));
std::string canonical;
EXPECT_FALSE(sandbox.ValidatePath("../etc/passwd", canonical));
EXPECT_FALSE(sandbox.ValidatePath("data/../../../etc/passwd", canonical));
return true;
}
bool Test_PathRejectsAbsolute(std::string& error_msg) {
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("/etc/passwd"));
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("C:\\Windows\\System32"));
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("D:/test/file.txt"));
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("\\\\server\\share"));
EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("//server/share"));
EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("scripts/utils.lua"));
EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("./data/file.txt"));
EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("data/config.json"));
mosis::PathSandbox sandbox("D:/test/app");
std::string canonical;
EXPECT_FALSE(sandbox.ValidatePath("/etc/passwd", canonical));
EXPECT_FALSE(sandbox.ValidatePath("C:\\Windows\\System32\\file.dll", canonical));
return true;
}
bool Test_PathAcceptsValid(std::string& error_msg) {
mosis::PathSandbox sandbox(GetScriptsDir());
std::string canonical;
EXPECT_TRUE(sandbox.ValidatePath("test_globals_removed.lua", canonical));
EXPECT_TRUE(sandbox.ValidatePath("./test_memory_limit.lua", canonical));
return true;
}
bool Test_ModuleNameValidation(std::string& error_msg) {
// Valid names
EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("utils"));
EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("my_module"));
EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("ui.button"));
EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("a.b.c"));
EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("Module123"));
// Invalid names
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName(""));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName(".utils"));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("utils."));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("ui..button"));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("../evil"));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("/etc/passwd"));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo;bar"));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo/bar"));
EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo\\bar"));
return true;
}
bool Test_ModuleToPath(std::string& error_msg) {
EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("utils") == "scripts/utils.lua");
EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("ui.button") == "scripts/ui/button.lua");
EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("a.b.c") == "scripts/a/b/c.lua");
return true;
}
bool Test_SafeRequireLoads(std::string& error_msg) {
// Create sandbox with scripts directory as app path
// The test_module.lua is in scripts/scripts/ so after ModuleToPath
// it becomes scripts/scripts/test_module.lua
// Safe require is auto-registered by LuaSandbox when app_path is set
SandboxContext ctx = TestContext();
ctx.app_path = GetScriptsDir(); // "scripts"
LuaSandbox sandbox(ctx);
// Should be able to require a test module
std::string script =
"local m = require('test_module')\n"
"if m.value ~= 42 then\n"
" error('module value mismatch')\n"
"end\n"
"return true\n";
if (!sandbox.LoadString(script, "require_test")) {
error_msg = "Failed to load module: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_SafeRequireCaches(std::string& error_msg) {
// Safe require is auto-registered by LuaSandbox when app_path is set
SandboxContext ctx = TestContext();
ctx.app_path = GetScriptsDir();
LuaSandbox sandbox(ctx);
std::string script =
"local m1 = require('test_module')\n"
"local m2 = require('test_module')\n"
"if m1 ~= m2 then\n"
" error('modules should be same (cached)')\n"
"end\n"
"return true\n";
if (!sandbox.LoadString(script, "cache_test")) {
error_msg = "Cache test failed: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_SafeRequireRejectsInvalid(std::string& error_msg) {
// Safe require is auto-registered by LuaSandbox when app_path is set
SandboxContext ctx = TestContext();
ctx.app_path = GetScriptsDir();
LuaSandbox sandbox(ctx);
// Should reject path traversal in module name
EXPECT_FALSE(sandbox.LoadString("require('../evil')", "evil_require"));
// Should reject absolute paths
EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')", "abs_require"));
// Should reject special characters
EXPECT_FALSE(sandbox.LoadString("require('foo;bar')", "special_require"));
// Should reject empty
EXPECT_FALSE(sandbox.LoadString("require('')", "empty_require"));
return true;
}
//=============================================================================
// TIMER MANAGER TESTS (Milestone 5)
//=============================================================================
bool Test_SetTimeoutFires(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
// Setup _test table for storing state (bypasses proxy __newindex)
SetupTestTable(sandbox.GetState());
// Register timer API
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Set a timeout that modifies _test table
std::string script =
"_test.fired = false\n"
"setTimeout(function() _test.fired = true end, 50)\n";
if (!sandbox.LoadString(script, "timeout_test")) {
error_msg = "Failed to set timeout: " + sandbox.GetLastError();
return false;
}
// Process timers after delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
manager.ProcessTimers();
// Check if callback fired
if (!sandbox.LoadString("assert(_test.fired == true, 'callback did not fire')", "check")) {
error_msg = "Timeout callback did not fire: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_SetIntervalFires(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
// Setup _test table for storing state
SetupTestTable(sandbox.GetState());
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
std::string script =
"_test.count = 0\n"
"setInterval(function() _test.count = _test.count + 1 end, 30)\n";
if (!sandbox.LoadString(script, "interval_test")) {
error_msg = "Failed to set interval: " + sandbox.GetLastError();
return false;
}
// Process multiple times
for (int i = 0; i < 5; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(40));
manager.ProcessTimers();
}
// Should have fired multiple times
if (!sandbox.LoadString("assert(_test.count >= 3, 'interval fired only ' .. _test.count .. ' times')", "check")) {
error_msg = "Interval did not fire enough times: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_ClearTimeoutCancels(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
// Setup _test table for storing state
SetupTestTable(sandbox.GetState());
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
std::string script =
"_test.fired = false\n"
"local id = setTimeout(function() _test.fired = true end, 100)\n"
"clearTimeout(id)\n";
if (!sandbox.LoadString(script, "clear_test")) {
error_msg = "Failed to clear timeout: " + sandbox.GetLastError();
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(150));
manager.ProcessTimers();
// Should NOT have fired
if (!sandbox.LoadString("assert(_test.fired == false, 'callback should not have fired')", "check")) {
error_msg = "Cancelled timeout still fired: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_ClearIntervalCancels(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
// Setup _test table for storing state
SetupTestTable(sandbox.GetState());
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Store both count and interval ID in _test table so they persist across LoadString calls
std::string script =
"_test.count = 0\n"
"_test.id = setInterval(function() _test.count = _test.count + 1 end, 30)\n";
if (!sandbox.LoadString(script, "interval_setup")) {
error_msg = "Failed to set interval: " + sandbox.GetLastError();
return false;
}
// Let it fire once
std::this_thread::sleep_for(std::chrono::milliseconds(40));
manager.ProcessTimers();
// Now cancel it
sandbox.LoadString("clearInterval(_test.id)", "cancel");
// Wait and process more
std::this_thread::sleep_for(std::chrono::milliseconds(100));
manager.ProcessTimers();
// Should have fired only once (or maybe twice due to timing)
if (!sandbox.LoadString("assert(_test.count <= 2, 'interval fired too many times: ' .. _test.count)", "check")) {
error_msg = "Interval kept firing after cancel: " + sandbox.GetLastError();
return false;
}
return true;
}
bool Test_TimerLimitEnforced(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Try to create more than MAX_TIMERS_PER_APP (100) timers
std::string script =
"created = 0\n"
"for i = 1, 150 do\n"
" local ok, err = pcall(function()\n"
" setTimeout(function() end, 1000000)\n"
" end)\n"
" if ok then created = created + 1 end\n"
"end\n";
sandbox.LoadString(script, "limit_test");
// Should be capped at MAX_TIMERS_PER_APP
size_t count = manager.GetTimerCount(ctx.app_id);
if (count > 100) {
error_msg = "Timer limit not enforced: " + std::to_string(count) + " timers created";
return false;
}
return true;
}
bool Test_ClearAppTimersCleanup(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
std::string script =
"for i = 1, 10 do\n"
" setTimeout(function() end, 1000000)\n"
"end\n";
sandbox.LoadString(script, "cleanup_test");
size_t before = manager.GetTimerCount(ctx.app_id);
EXPECT_TRUE(before == 10);
// Clear all timers for app (simulating app stop)
manager.ClearAppTimers(ctx.app_id);
size_t after = manager.GetTimerCount(ctx.app_id);
EXPECT_TRUE(after == 0);
return true;
}
bool Test_MinIntervalEnforced(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
// Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox
mosis::TimerManager manager;
// Setup _test table for storing state
SetupTestTable(sandbox.GetState());
mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);
// Try to set interval less than minimum (10ms)
std::string script =
"_test.count = 0\n"
"setInterval(function() _test.count = _test.count + 1 end, 1)\n"; // 1ms, should be clamped to 10ms
sandbox.LoadString(script, "min_interval_test");
// With 1ms interval, in 50ms we'd get 50 callbacks
// With 10ms minimum, we should get ~5
std::this_thread::sleep_for(std::chrono::milliseconds(55));
for (int i = 0; i < 10; i++) {
manager.ProcessTimers();
}
if (!sandbox.LoadString("assert(_test.count <= 10, 'interval fired too often: ' .. _test.count)", "check")) {
error_msg = "Minimum interval not enforced: " + sandbox.GetLastError();
return false;
}
return true;
}
//=============================================================================
// MAIN
//=============================================================================
int main(int argc, char* argv[]) {
std::string filter;
std::string output_file = "test_results.json";
// Parse args
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--test" && i + 1 < argc) {
filter = argv[++i];
} else if (arg == "--output" && i + 1 < argc) {
output_file = argv[++i];
} else if (arg == "--help") {
std::cout << "Usage: sandbox-test [options]\n";
std::cout << "Options:\n";
std::cout << " --test <name> Run only tests containing <name>\n";
std::cout << " --output <file> Write JSON report to <file>\n";
std::cout << " --help Show this help\n";
return 0;
}
}
std::cout << "========================================\n";
std::cout << " LUA SANDBOX SECURITY TESTS\n";
std::cout << "========================================\n\n";
// Check scripts directory exists
if (!std::filesystem::exists(GetScriptsDir())) {
std::cerr << "ERROR: Scripts directory not found: " << GetScriptsDir() << "\n";
std::cerr << "Make sure to run from the build directory.\n";
return 1;
}
// Register tests
TestHarness harness;
// Milestone 1: Core Sandbox
harness.AddTest("DangerousGlobalsRemoved", Test_DangerousGlobalsRemoved);
harness.AddTest("BytecodeRejected", Test_BytecodeRejected);
harness.AddTest("MemoryLimitEnforced", Test_MemoryLimitEnforced);
harness.AddTest("CPULimitEnforced", Test_CPULimitEnforced);
harness.AddTest("MetatableProtected", Test_MetatableProtected);
harness.AddTest("SafeOperationsWork", Test_SafeOperationsWork);
harness.AddTest("StringDumpRemoved", Test_StringDumpRemoved);
harness.AddTest("MemoryTracking", Test_MemoryTracking);
harness.AddTest("InstructionCounting", Test_InstructionCounting);
harness.AddTest("MultipleLoads", Test_MultipleLoads);
harness.AddTest("ErrorRecovery", Test_ErrorRecovery);
// Milestone 2: Permission System
harness.AddTest("NormalPermissionAutoGranted", Test_NormalPermissionAutoGranted);
harness.AddTest("DangerousPermissionRequiresGrant", Test_DangerousPermissionRequiresGrant);
harness.AddTest("SignaturePermissionSystemOnly", Test_SignaturePermissionSystemOnly);
harness.AddTest("UserGestureTracking", Test_UserGestureTracking);
harness.AddTest("UndeclaredPermissionDenied", Test_UndeclaredPermissionDenied);
harness.AddTest("SystemAppGetsDangerousAuto", Test_SystemAppGetsDangerousAuto);
harness.AddTest("PermissionCategoryCheck", Test_PermissionCategoryCheck);
// Milestone 3: Audit Logging & Rate Limiting
harness.AddTest("AuditLogBasic", Test_AuditLogBasic);
harness.AddTest("AuditLogRingBuffer", Test_AuditLogRingBuffer);
harness.AddTest("AuditLogThreadSafe", Test_AuditLogThreadSafe);
harness.AddTest("RateLimiterBasic", Test_RateLimiterBasic);
harness.AddTest("RateLimiterExhaustion", Test_RateLimiterExhaustion);
harness.AddTest("RateLimiterRefill", Test_RateLimiterRefill);
harness.AddTest("RateLimiterAppIsolation", Test_RateLimiterAppIsolation);
harness.AddTest("RateLimiterReset", Test_RateLimiterReset);
harness.AddTest("RateLimiterNoConfig", Test_RateLimiterNoConfig);
// Milestone 4: Safe Path & Require
harness.AddTest("PathRejectsTraversal", Test_PathRejectsTraversal);
harness.AddTest("PathRejectsAbsolute", Test_PathRejectsAbsolute);
harness.AddTest("PathAcceptsValid", Test_PathAcceptsValid);
harness.AddTest("ModuleNameValidation", Test_ModuleNameValidation);
harness.AddTest("ModuleToPath", Test_ModuleToPath);
harness.AddTest("SafeRequireLoads", Test_SafeRequireLoads);
harness.AddTest("SafeRequireCaches", Test_SafeRequireCaches);
harness.AddTest("SafeRequireRejectsInvalid", Test_SafeRequireRejectsInvalid);
// Milestone 5: Timer & Callback System
harness.AddTest("SetTimeoutFires", Test_SetTimeoutFires);
harness.AddTest("SetIntervalFires", Test_SetIntervalFires);
harness.AddTest("ClearTimeoutCancels", Test_ClearTimeoutCancels);
harness.AddTest("ClearIntervalCancels", Test_ClearIntervalCancels);
harness.AddTest("TimerLimitEnforced", Test_TimerLimitEnforced);
harness.AddTest("ClearAppTimersCleanup", Test_ClearAppTimersCleanup);
harness.AddTest("MinIntervalEnforced", Test_MinIntervalEnforced);
// Run tests
auto results = harness.Run(filter);
// Output
harness.PrintResults(results);
harness.WriteJsonReport(results, output_file);
std::cout << "\nJSON report written to: " << output_file << "\n";
// Return non-zero if any tests failed
int failed = 0;
for (const auto& r : results) {
if (!r.passed) failed++;
}
return failed > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,128 @@
#include "test_harness.h"
#include <nlohmann/json.hpp>
#include <fstream>
#include <iomanip>
#include <ctime>
void TestHarness::AddTest(const std::string& name, std::function<bool(std::string&)> func) {
m_tests.push_back({name, func});
}
std::vector<TestResult> TestHarness::Run(const std::string& filter) {
std::vector<TestResult> results;
for (const auto& test : m_tests) {
// Filter check
if (!filter.empty() && test.name.find(filter) == std::string::npos) {
continue;
}
TestResult result;
result.name = test.name;
std::cout << "Running: " << test.name << "... " << std::flush;
auto start = std::chrono::steady_clock::now();
try {
std::string error;
result.passed = test.func(error);
result.error_message = error;
} catch (const std::exception& e) {
result.passed = false;
result.error_message = std::string("Exception: ") + e.what();
} catch (...) {
result.passed = false;
result.error_message = "Unknown exception";
}
auto end = std::chrono::steady_clock::now();
result.duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
if (result.passed) {
std::cout << "PASSED (" << result.duration_ms << "ms)\n";
} else {
std::cout << "FAILED\n";
std::cout << " Error: " << result.error_message << "\n";
}
results.push_back(result);
}
return results;
}
void TestHarness::WriteJsonReport(const std::vector<TestResult>& results, const std::string& path) {
nlohmann::json report;
// Get timestamp
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::gmtime(&time), "%Y-%m-%dT%H:%M:%SZ");
report["name"] = "Lua Sandbox Security Tests";
report["timestamp"] = ss.str();
int passed = 0, failed = 0;
for (const auto& r : results) {
if (r.passed) passed++;
else failed++;
}
report["summary"]["passed"] = passed;
report["summary"]["failed"] = failed;
report["summary"]["total"] = static_cast<int>(results.size());
nlohmann::json tests = nlohmann::json::array();
for (const auto& r : results) {
nlohmann::json t;
t["name"] = r.name;
t["status"] = r.passed ? "passed" : "failed";
t["duration_ms"] = r.duration_ms;
if (!r.passed && !r.error_message.empty()) {
t["error"] = r.error_message;
}
tests.push_back(t);
}
report["tests"] = tests;
std::ofstream f(path);
f << report.dump(2);
}
void TestHarness::PrintResults(const std::vector<TestResult>& results) {
std::cout << "\n";
std::cout << "========================================\n";
std::cout << " TEST RESULTS\n";
std::cout << "========================================\n\n";
int passed = 0, failed = 0;
for (const auto& r : results) {
if (r.passed) passed++;
else failed++;
}
std::cout << "Total: " << results.size() << "\n";
std::cout << "Passed: " << passed << "\n";
std::cout << "Failed: " << failed << "\n\n";
if (failed > 0) {
std::cout << "FAILED TESTS:\n";
for (const auto& r : results) {
if (!r.passed) {
std::cout << " - " << r.name << "\n";
std::cout << " " << r.error_message << "\n";
}
}
std::cout << "\n";
}
if (failed == 0) {
std::cout << "ALL TESTS PASSED!\n";
} else {
std::cout << "SOME TESTS FAILED!\n";
}
std::cout << "========================================\n";
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include <string>
#include <vector>
#include <functional>
#include <chrono>
#include <iostream>
// Simple test result
struct TestResult {
std::string name;
bool passed;
std::string error_message;
int64_t duration_ms;
};
// Test case definition
struct TestCase {
std::string name;
std::function<bool(std::string&)> func; // Returns true if passed, error in string
};
// Test runner
class TestHarness {
public:
void AddTest(const std::string& name, std::function<bool(std::string&)> func);
// Run all tests or filter by name
std::vector<TestResult> Run(const std::string& filter = "");
// Output results as JSON
void WriteJsonReport(const std::vector<TestResult>& results, const std::string& path);
// Print results to console
void PrintResults(const std::vector<TestResult>& results);
private:
std::vector<TestCase> m_tests;
};
// Assertion macros
#define EXPECT_TRUE(cond) \
do { \
if (!(cond)) { \
error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \
": EXPECT_TRUE(" #cond ") failed"; \
return false; \
} \
} while(0)
#define EXPECT_FALSE(cond) \
do { \
if (cond) { \
error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \
": EXPECT_FALSE(" #cond ") failed"; \
return false; \
} \
} while(0)
#define EXPECT_EQ(a, b) \
do { \
if ((a) != (b)) { \
error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \
": EXPECT_EQ failed: " + std::to_string(a) + " != " + std::to_string(b); \
return false; \
} \
} while(0)
#define EXPECT_NE(a, b) \
do { \
if ((a) == (b)) { \
error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \
": EXPECT_NE failed: values are equal"; \
return false; \
} \
} while(0)
#define EXPECT_CONTAINS(haystack, needle) \
do { \
if ((haystack).find(needle) == std::string::npos) { \
error_msg = std::string(__FILE__) + ":" + std::to_string(__LINE__) + \
": EXPECT_CONTAINS failed: '" + (haystack) + "' does not contain '" + (needle) + "'"; \
return false; \
} \
} while(0)

View File

@@ -0,0 +1,221 @@
{
"name": "Lua Sandbox Security Tests",
"summary": {
"failed": 0,
"passed": 42,
"total": 42
},
"tests": [
{
"duration_ms": 0,
"name": "DangerousGlobalsRemoved",
"status": "passed"
},
{
"duration_ms": 0,
"name": "BytecodeRejected",
"status": "passed"
},
{
"duration_ms": 2,
"name": "MemoryLimitEnforced",
"status": "passed"
},
{
"duration_ms": 0,
"name": "CPULimitEnforced",
"status": "passed"
},
{
"duration_ms": 0,
"name": "MetatableProtected",
"status": "passed"
},
{
"duration_ms": 0,
"name": "SafeOperationsWork",
"status": "passed"
},
{
"duration_ms": 0,
"name": "StringDumpRemoved",
"status": "passed"
},
{
"duration_ms": 0,
"name": "MemoryTracking",
"status": "passed"
},
{
"duration_ms": 0,
"name": "InstructionCounting",
"status": "passed"
},
{
"duration_ms": 0,
"name": "MultipleLoads",
"status": "passed"
},
{
"duration_ms": 0,
"name": "ErrorRecovery",
"status": "passed"
},
{
"duration_ms": 0,
"name": "NormalPermissionAutoGranted",
"status": "passed"
},
{
"duration_ms": 0,
"name": "DangerousPermissionRequiresGrant",
"status": "passed"
},
{
"duration_ms": 0,
"name": "SignaturePermissionSystemOnly",
"status": "passed"
},
{
"duration_ms": 111,
"name": "UserGestureTracking",
"status": "passed"
},
{
"duration_ms": 0,
"name": "UndeclaredPermissionDenied",
"status": "passed"
},
{
"duration_ms": 0,
"name": "SystemAppGetsDangerousAuto",
"status": "passed"
},
{
"duration_ms": 0,
"name": "PermissionCategoryCheck",
"status": "passed"
},
{
"duration_ms": 0,
"name": "AuditLogBasic",
"status": "passed"
},
{
"duration_ms": 0,
"name": "AuditLogRingBuffer",
"status": "passed"
},
{
"duration_ms": 14,
"name": "AuditLogThreadSafe",
"status": "passed"
},
{
"duration_ms": 0,
"name": "RateLimiterBasic",
"status": "passed"
},
{
"duration_ms": 0,
"name": "RateLimiterExhaustion",
"status": "passed"
},
{
"duration_ms": 16,
"name": "RateLimiterRefill",
"status": "passed"
},
{
"duration_ms": 0,
"name": "RateLimiterAppIsolation",
"status": "passed"
},
{
"duration_ms": 0,
"name": "RateLimiterReset",
"status": "passed"
},
{
"duration_ms": 0,
"name": "RateLimiterNoConfig",
"status": "passed"
},
{
"duration_ms": 0,
"name": "PathRejectsTraversal",
"status": "passed"
},
{
"duration_ms": 0,
"name": "PathRejectsAbsolute",
"status": "passed"
},
{
"duration_ms": 0,
"name": "PathAcceptsValid",
"status": "passed"
},
{
"duration_ms": 0,
"name": "ModuleNameValidation",
"status": "passed"
},
{
"duration_ms": 0,
"name": "ModuleToPath",
"status": "passed"
},
{
"duration_ms": 0,
"name": "SafeRequireLoads",
"status": "passed"
},
{
"duration_ms": 0,
"name": "SafeRequireCaches",
"status": "passed"
},
{
"duration_ms": 0,
"name": "SafeRequireRejectsInvalid",
"status": "passed"
},
{
"duration_ms": 107,
"name": "SetTimeoutFires",
"status": "passed"
},
{
"duration_ms": 237,
"name": "SetIntervalFires",
"status": "passed"
},
{
"duration_ms": 155,
"name": "ClearTimeoutCancels",
"status": "passed"
},
{
"duration_ms": 158,
"name": "ClearIntervalCancels",
"status": "passed"
},
{
"duration_ms": 0,
"name": "TimerLimitEnforced",
"status": "passed"
},
{
"duration_ms": 0,
"name": "ClearAppTimersCleanup",
"status": "passed"
},
{
"duration_ms": 62,
"name": "MinIntervalEnforced",
"status": "passed"
}
],
"timestamp": "2026-01-18T13:19:38Z"
}

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