add Lua sandbox with timer system (milestones 1-5 complete)
This commit is contained in:
23
CLAUDE.md
23
CLAUDE.md
@@ -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
3569
SANDBOX.md
Normal file
File diff suppressed because it is too large
Load Diff
964
SANDBOX_MILESTONES.md
Normal file
964
SANDBOX_MILESTONES.md
Normal 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
595
SANDBOX_MILESTONE_1.md
Normal 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
488
SANDBOX_MILESTONE_2.md
Normal 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
357
SANDBOX_MILESTONE_3.md
Normal 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
241
SANDBOX_MILESTONE_4.md
Normal 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
333
SANDBOX_MILESTONE_5.md
Normal 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
|
||||
54
sandbox-test/CMakeLists.txt
Normal file
54
sandbox-test/CMakeLists.txt
Normal 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
132
sandbox-test/README.md
Normal 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
|
||||
```
|
||||
59
sandbox-test/run_tests.bat
Normal file
59
sandbox-test/run_tests.bat
Normal 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%
|
||||
67
sandbox-test/run_tests.ps1
Normal file
67
sandbox-test/run_tests.ps1
Normal 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
|
||||
}
|
||||
11
sandbox-test/scripts/scripts/test_module.lua
Normal file
11
sandbox-test/scripts/scripts/test_module.lua
Normal 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
|
||||
5
sandbox-test/scripts/test_bytecode_rejected.lua
Normal file
5
sandbox-test/scripts/test_bytecode_rejected.lua
Normal 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")
|
||||
12
sandbox-test/scripts/test_cpu_limit.lua
Normal file
12
sandbox-test/scripts/test_cpu_limit.lua
Normal 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")
|
||||
26
sandbox-test/scripts/test_globals_removed.lua
Normal file
26
sandbox-test/scripts/test_globals_removed.lua
Normal 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")
|
||||
20
sandbox-test/scripts/test_memory_limit.lua
Normal file
20
sandbox-test/scripts/test_memory_limit.lua
Normal 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")
|
||||
33
sandbox-test/scripts/test_metatable_protected.lua
Normal file
33
sandbox-test/scripts/test_metatable_protected.lua
Normal 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")
|
||||
158
sandbox-test/scripts/test_safe_operations.lua
Normal file
158
sandbox-test/scripts/test_safe_operations.lua
Normal 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")
|
||||
18
sandbox-test/scripts/test_string_dump_removed.lua
Normal file
18
sandbox-test/scripts/test_string_dump_removed.lua
Normal 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
984
sandbox-test/src/main.cpp
Normal 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;
|
||||
}
|
||||
128
sandbox-test/src/test_harness.cpp
Normal file
128
sandbox-test/src/test_harness.cpp
Normal 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";
|
||||
}
|
||||
85
sandbox-test/src/test_harness.h
Normal file
85
sandbox-test/src/test_harness.h
Normal 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)
|
||||
221
sandbox-test/test_results.json
Normal file
221
sandbox-test/test_results.json
Normal 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"
|
||||
}
|
||||
188
src/main/cpp/sandbox/audit_log.cpp
Normal file
188
src/main/cpp/sandbox/audit_log.cpp
Normal 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
|
||||
94
src/main/cpp/sandbox/audit_log.h
Normal file
94
src/main/cpp/sandbox/audit_log.h
Normal 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;
|
||||
448
src/main/cpp/sandbox/lua_sandbox.cpp
Normal file
448
src/main/cpp/sandbox/lua_sandbox.cpp
Normal 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
|
||||
101
src/main/cpp/sandbox/lua_sandbox.h
Normal file
101
src/main/cpp/sandbox/lua_sandbox.h
Normal 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;
|
||||
344
src/main/cpp/sandbox/path_sandbox.cpp
Normal file
344
src/main/cpp/sandbox/path_sandbox.cpp
Normal 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
|
||||
52
src/main/cpp/sandbox/path_sandbox.h
Normal file
52
src/main/cpp/sandbox/path_sandbox.h
Normal 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
|
||||
197
src/main/cpp/sandbox/permission_gate.cpp
Normal file
197
src/main/cpp/sandbox/permission_gate.cpp
Normal 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
|
||||
73
src/main/cpp/sandbox/permission_gate.h
Normal file
73
src/main/cpp/sandbox/permission_gate.h
Normal 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;
|
||||
209
src/main/cpp/sandbox/rate_limiter.cpp
Normal file
209
src/main/cpp/sandbox/rate_limiter.cpp
Normal 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
|
||||
68
src/main/cpp/sandbox/rate_limiter.h
Normal file
68
src/main/cpp/sandbox/rate_limiter.h
Normal 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;
|
||||
440
src/main/cpp/sandbox/timer_manager.cpp
Normal file
440
src/main/cpp/sandbox/timer_manager.cpp
Normal 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
|
||||
87
src/main/cpp/sandbox/timer_manager.h
Normal file
87
src/main/cpp/sandbox/timer_manager.h
Normal 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
|
||||
Reference in New Issue
Block a user