The Injection Toolkit follows a minimal injection, maximal external processing philosophy:
- Inject only what's necessary - Code running inside the target application should be minimal
- Process externally - Heavy lifting happens in the daemon and overlay
- Communicate via IPC - Clean separation between components
- Fail gracefully - Components should handle failures without crashing the target
What it does:
- Connects to daemon via IPC
- Hooks strategic functions in the target
- Extracts minimal state data
- Sends state updates to daemon
What it doesn't do:
- Complex processing
- Rendering
- Network communication (beyond local IPC)
- Blocking operations
What it does:
- Receives state from injector
- Aggregates and caches state
- Serves queries from clients (overlay, MCP)
- Optionally handles multiplayer sync
What it doesn't do:
- Inject code
- Render anything
- Interact directly with target application
What it does:
- Renders content on top of target application
- Handles click-through mode
- Provides interactive UI when enabled
- Reads frame data from shared memory (for video)
What it doesn't do:
- Inject code
- Process video/audio
- Heavy computation
What it does:
- Transfer large data (video frames) between daemon and overlay
- Lock-free triple-buffered design
- Seqlock for consistency
What it doesn't do:
- Small message passing (use IPC)
- Cross-machine communication
Target App Function
│
▼
[Hook triggers]
│
▼
Injector extracts state
│
▼
[IPC message]
│
▼
Daemon receives & caches
│
▼
[IPC query]
│
▼
Client receives state
Video Source (daemon)
│
▼
[Decode frame]
│
▼
Write to shared memory
(seqlock protected)
│
▼
Overlay reads frame
│
▼
[GPU texture upload]
│
▼
Render to screen
All messages use a common header:
┌─────────┬─────────┬──────────┬─────────────┬─────────┬───────────┐
│ Magic │ Version │ MsgType │ PayloadLen │ CRC32 │ Payload │
│ 4 bytes │ 4 bytes │ 4 bytes │ 4 bytes │ 4 bytes │ N bytes │
└─────────┴─────────┴──────────┴─────────────┴─────────┴───────────┘
- Magic:
"ITKP"- identifies ITK protocol - Version: Protocol version for compatibility
- MsgType: Enum identifying message type
- PayloadLen: Size of payload (max 1MB)
- CRC32: Checksum for validation
- Payload: bincode-serialized data
| Type | Direction | Purpose |
|---|---|---|
| Ping/Pong | Any | Keepalive, latency measurement |
| ScreenRect | Injector → Daemon | Overlay positioning |
| WindowState | Injector → Daemon | Target window properties |
| StateSnapshot | Injector → Daemon | Full state dump |
| StateEvent | Injector → Daemon | Incremental state change |
| StateQuery | Client → Daemon | Request state |
| StateResponse | Daemon → Client | State data |
| Platform | Implementation |
|---|---|
| Windows | Named Pipes (\\.\pipe\itk_*) |
| Linux | Unix Domain Sockets (/tmp/itk_*.sock) |
| Platform | Implementation |
|---|---|
| Windows | CreateFileMappingW + MapViewOfFile |
| Linux | shm_open + mmap |
| Platform | Click-Through | Always-on-Top |
|---|---|---|
| Windows | WS_EX_TRANSPARENT |
HWND_TOPMOST |
| Linux/X11 | SHAPE extension | _NET_WM_STATE_ABOVE |
| Linux/Wayland | Layer-shell* | Layer-shell* |
*Requires compositor support and additional dependencies.
Used for lock-free shared memory access:
// Writer
seq.fetch_add(1, Acquire); // Odd = writing, prevents data writes from floating up
// ... write data (Relaxed is fine here) ...
seq.fetch_add(1, Release); // Even = done, makes writes visible
// Reader
loop {
let s1 = seq.load(Acquire); // Synchronizes with writer's Release
if s1 & 1 != 0 { continue; } // Write in progress
// ... read data (Relaxed, bounded by fence below) ...
fence(Acquire); // Prevents data reads from sinking past seq2 check
let s2 = seq.load(Relaxed); // Fence provides ordering
if s1 == s2 { break; } // Consistent read
}The seqlock uses carefully chosen orderings for ARM compatibility:
Writer:
- begin_write:
fetch_add(1, Acquire)- Prevents subsequent data writes from being reordered before the odd-increment. Without this, readers could see "even" sequence but read partially-written data. - end_write:
fetch_add(1, Release)- Ensures all data writes are visible before the even sequence number.
Reader:
- First seq load:
load(Acquire)- Synchronizes with writer's Release, ensuring we see data that was written before the sequence we observe. - Data reads:
Relaxed- Safe because bounded by the fence below. - Fence:
fence(Acquire)before second seq check - Critical: Prevents data loads from being reordered past the validation. Without this fence, the CPU could check seq2, find it valid, then execute data reads that see new/torn data from a concurrent write. - Second seq load:
Relaxed- The fence provides the necessary ordering.
This approach:
- ARM compatible: Correctly handles weak memory ordering
- Performant: Uses minimal barriers (no SeqCst)
- Verified: Tested with Loom concurrency checker
CRITICAL: The seqlock implementation assumes a single writer. Multiple concurrent writers will corrupt the sequence counter and cause undefined behavior. This is an intentional design choice for our use case:
- Daemon: Single process, single write path for frame updates
- Injector: Single process, single write path for state updates
If you need multiple writers in the future:
- Wrap the
Seqlock::write()call with an externalMutexorRwLock - Or use a different synchronization primitive (e.g., a channel-based approach)
// SAFE: External mutex protects multi-threaded writer access
let writer_lock = Mutex::new(());
{
let _guard = writer_lock.lock().unwrap();
seqlock.write(|state| {
// ... update state ...
});
}
// UNSAFE: Multiple threads calling write() without synchronization
// This WILL corrupt data - do not do this!
std::thread::spawn(|| seqlock.write(|s| s.pts_ms = 100)); // Thread 1
std::thread::spawn(|| seqlock.write(|s| s.pts_ms = 200)); // Thread 2 - DATA RACE!The seqlock is designed for one writer, many readers - this is the common pattern for frame buffer synchronization where one producer (decoder) writes frames and multiple consumers (overlay, MCP clients) read them.
| Failure | Detection | Behavior |
|---|---|---|
| Daemon unreachable | IPC error | Injector continues without state export |
| Injector disconnects | IPC timeout | Daemon serves stale state |
| Overlay crash | Process exit | Target unaffected |
| Target crash | Process exit | All components survive |
- IPC channels automatically reconnect with exponential backoff
- Shared memory handles are validated before each access
- Missing state returns explicit errors, not crashes
The Injection Toolkit operates in a hostile environment where the injected code runs inside an untrusted process. The daemon and overlay must treat ALL data from the injector as potentially malicious.
| Component | Trust Level | Threat |
|---|---|---|
| Injector | UNTRUSTED | Compromised target, malicious mods, memory corruption |
| Daemon | Trusted | Local process with validated inputs |
| Overlay | Trusted | Local process with validated inputs |
| Shared Memory | Untrusted data | Injector can write arbitrary bytes |
The daemon validates all incoming data before use:
// String length limits
const MAX_STRING_LEN: usize = 256;
const MAX_DATA_SIZE: usize = 64 * 1024; // 64 KB
// Numeric bounds checking
const MAX_SCREEN_DIM: f32 = 16384.0;
// Float validation (reject NaN/Inf)
if !value.is_finite() {
bail!("Non-finite value rejected");
}
// Dimension validation
if width < 0.0 || height < 0.0 {
bail!("Negative dimensions rejected");
}Named pipes should use appropriate security descriptors:
- Default: Local user access only (inherited from process token)
- Custom: Use
SECURITY_ATTRIBUTESto restrict access further - Never expose pipes to network without explicit intent
// Recommended: Restrict to current user
let mut sa = SECURITY_ATTRIBUTES::default();
// Set up DACL allowing only current user...Unix domain sockets use filesystem permissions:
- Socket created with
0600permissions (owner only) - Located in
/tmpwith sticky bit protection - Consider
SO_PASSCREDfor peer authentication
// Socket path: /tmp/itk_{name}.sock
// Permissions: -rw------- (0600)- Memory regions are created with restrictive permissions
- Size is fixed at creation to prevent overflow
- Triple-buffering prevents reader/writer corruption
- Seqlock provides consistency, not access control
- Protocol validation: Magic bytes, version, CRC32
- Size limits: Payload bounded to 1MB max
- Type validation: All fields checked before use
- Fail-safe: Invalid data logged and rejected, never crashes
- Isolation: Components run in separate processes
The toolkit does not protect against:
- Malicious overlay/daemon (these are trusted)
- Kernel-level attacks
- Physical access attacks
- Side-channel attacks
These are out of scope for a userspace injection framework.
| Component | Budget | Notes |
|---|---|---|
| Injector | < 5 MB | Minimal footprint |
| Daemon | < 30 MB | State caching |
| Overlay | < 20 MB | GPU resources |
| Shmem | ~10 MB | Triple-buffered 720p |
| Operation | Target | Notes |
|---|---|---|
| State update (IPC) | < 1 ms | Local only |
| Frame copy (shmem) | < 1 ms | ~3.5 MB @ 720p |
| Overlay render | < 5 ms | Simple quad |
- Create new crate in
injectors/ - Implement IPC client connection
- Implement platform-specific initialization
- Export state using
itk-protocolmessages
- Add variant to
MessageTypeenum initk-protocol - Define payload struct with serde derives
- Update daemon message handlers
- Update clients as needed
- Add platform module in
itk-shmemanditk-ipc - Implement platform traits
- Update
cfg_if!blocks - Add platform module in overlay if needed