Syscall Data Model Specification
August 21, 2026 · View on GitHub
Overview
This document specifies the Neo4j data model for storing Linux and Windows syscall information in the OSWatcher system. The design follows the hash-based, git-like architecture where all nodes are content-addressed and immutable.
Motivation
OSWatcher needs to track syscall information from operating system snapshots for:
- Security analysis (identifying available attack surface)
- OS fingerprinting (kernel version detection)
- Change detection (tracking syscall additions/removals across versions)
- Cross-platform comparison (Linux vs Windows syscall differences)
Architecture Context: Windows vs Linux Syscall Handling
Understanding the fundamental architectural differences between Windows and Linux syscall handling is critical to the data model design:
Windows (x64)
The kernel exposes only 64-bit syscall tables:
- NT SSDT (Native API syscalls)
- Win32k SSDT (Graphics/Window management syscalls)
32-bit syscalls do NOT enter the kernel directly:
- All 32-bit calls go through the WoW64 user-mode thunking layer
- WoW64 translates 32-bit syscall numbers → 64-bit equivalents
- WoW64 switches CPU mode
- WoW64 invokes the 64-bit syscall
- The kernel never processes a native 32-bit syscall ABI
→ Windows uses user-mode translation; kernel only runs 64-bit syscalls.
Linux (x86-64)
The kernel contains two real syscall tables:
- 64-bit syscall table (
syscall_64.tbl) - 32-bit compat syscall table (
syscall_32.tbl) whenCONFIG_COMPAT=y
32-bit processes execute real 32-bit syscalls:
- A 32-bit process executes a 32-bit syscall instruction
- This traps directly into the kernel
- The kernel dispatches to:
sys_*functions (64-bit)compat_sys_*functions (32-bit)
- Argument translation (pointers, struct layouts, time types) happens in kernel
→ Linux uses in-kernel compat handling; both ABIs exist concurrently.
Implications for Data Model
Linux: Single kernel blob contains multiple architecture tables
vmlinuz blob → {x64 syscall table, x86 compat syscall table}
Windows: Different kernel blobs contain different syscall sources
ntoskrnl.exe blob → {NT SSDT (x64 only)}
win32k.sys blob → {Win32k SSDT (x64 only)}
Data Model Design
The syscall data model follows the hierarchical pattern established by WinStruct (which has WinStructField children). This pattern:
- Uses intermediate container nodes (
SyscallTableanalogous toWinStruct) - Enables recursive diffing in the existing diff algorithm
- Solves the relationship name uniqueness requirement naturally
Node Hierarchy
Blob (vmlinuz, ntoskrnl.exe, win32k.sys)
└─[HAS_SYSCALL_TABLE]→ SyscallTable (x64, x86, NT, Win32k)
└─[HAS_SYSCALL]→ Syscall (read, write, open, etc.)
Node Definitions
SyscallTable
Represents a syscall table within a kernel binary.
Properties:
hash: String!- SHA256 hash of:arch + sorted(syscall_hashes)arch: String!- Architecture:"X64"or"X86"
Relationships:
- Incoming:
(Blob)-[:HAS_SYSCALL_TABLE {name: String!}]->(SyscallTable)- Relationship
nameproperty: architecture identifier or source name- Linux:
"x64"or"x86" - Windows:
"nt"or"win32k"
- Linux:
- Relationship
- Outgoing:
(SyscallTable)-[:HAS_SYSCALL {name: String!, index: Int!}]->(Syscall)- Relationship
nameproperty: syscall name (e.g.,"read","write") - Relationship
indexproperty: syscall number
- Relationship
Hash Calculation:
import hashlib
import json
def calculate_syscall_table_hash(arch: str, syscall_hashes: list[str]) -> str:
"""Calculate hash for SyscallTable node.
Args:
arch: "X64" or "X86"
syscall_hashes: List of syscall hashes in table order
Returns:
SHA256 hash hex string
"""
content = {
"arch": arch,
"syscalls": sorted(syscall_hashes) # Sort for deterministic hash
}
content_str = json.dumps(content, sort_keys=True)
return hashlib.sha256(content_str.encode()).hexdigest()
Diff Algorithm Integration:
SyscallTablemust be added to the list of recursable types inTreeDiffRecursiveProcedure.java:private boolean isRecursableLabel(String type) { return "Tree".equals(type) || "WinRegKey".equals(type) || "WinStruct".equals(type) || "WinStructField".equals(type) || "SyscallTable".equals(type); // Add this }
Syscall
Represents an individual syscall entry with its signature.
Properties:
hash: String!- SHA256 hash of:index + name + entry_point + parametersindex: Int!- Syscall numbername: String!- Human-readable syscall name (e.g.,"read","write")entry_point: String!- Kernel function name (e.g.,"sys_read","compat_sys_read")parameters: [String!]!- Parameter signatures (e.g.,["int fd", "char *buf", "size_t count"])
Relationships:
- Incoming:
(SyscallTable)-[:HAS_SYSCALL {name: String!, index: Int!}]->(Syscall)
Hash Calculation:
import hashlib
import json
def calculate_syscall_hash(index: int, name: str, entry_point: str,
parameters: list[str]) -> str:
"""Calculate hash for Syscall node.
Args:
index: Syscall number
name: Syscall name (e.g., "read")
entry_point: Kernel function (e.g., "sys_read")
parameters: List of parameter signatures
Returns:
SHA256 hash hex string
"""
content = {
"index": index,
"name": name,
"entry_point": entry_point,
"parameters": parameters
}
content_str = json.dumps(content, sort_keys=True)
return hashlib.sha256(content_str.encode()).hexdigest()
Design Decision: Index on Node vs Relationship
The index property is placed on the node (not the relationship) for pragmatic reasons:
Pros:
- Index is visible in diff UI (node properties are exposed in
old_props/new_props) - Works with existing diff API (no code changes required)
- Simpler querying (index is directly on the node)
Cons:
- Less semantic purity (index is contextual, not inherent)
- Less deduplication (same signature at different index = different nodes)
Rationale:
- Syscall numbers are very stable in Linux (ABI compatibility guarantee)
- Index is critical information users want to see in diffs
- Current diff API doesn't expose relationship properties (limitation documented in architecture.md)
- Deduplication benefit is minimal (same signature at different index is rare)
Future Refactoring Path:
If deduplication becomes important, we can introduce a SyscallSignature node:
Syscall {index} -[:HAS_SIGNATURE]-> SyscallSignature {name, entry_point, parameters}
Relationship Definitions
HAS_SYSCALL_TABLE
Connects a kernel Blob to its syscall tables.
Relationship Properties:
name: String!- Table identifier for HashMap key uniqueness- Linux:
"x64"or"x86"(architecture) - Windows:
"nt"or"win32k"(syscall source)
- Linux:
Cardinality:
- Linux vmlinuz: 1→2 (one blob, two tables: x64 + x86)
- Windows ntoskrnl.exe: 1→1 (one blob, one NT table)
- Windows win32k.sys: 1→1 (one blob, one Win32k table)
Constraint:
- Relationship names must be unique per parent Blob (enforced by diff algorithm HashMap)
HAS_SYSCALL
Connects a SyscallTable to its syscall entries.
Relationship Properties:
name: String!- Syscall name (e.g.,"read") - HashMap keyindex: Int!- Syscall number (for display, not part of HashMap key)
Cardinality:
- One SyscallTable → Many Syscalls (typically 300-400 syscalls per table)
Constraint:
- Relationship names (syscall names) must be unique per SyscallTable
- This is naturally enforced: each syscall has a unique name within a table
Examples
Linux: vmlinuz with Both Architectures
// The vmlinuz blob
CREATE (vmlinuz:Blob {hash: "abc123..."})
// x64 syscall table
CREATE (table_x64:SyscallTable {
hash: "def456...",
arch: "X64"
})
// x86 compat syscall table
CREATE (table_x86:SyscallTable {
hash: "ghi789...",
arch: "X86"
})
// Connect blob to tables
CREATE (vmlinuz)-[:HAS_SYSCALL_TABLE {name: "x64"}]->(table_x64)
CREATE (vmlinuz)-[:HAS_SYSCALL_TABLE {name: "x86"}]->(table_x86)
// x64 syscall entries
CREATE (sc_read_x64:Syscall {
hash: "jkl012...",
index: 0,
name: "read",
entry_point: "sys_read",
parameters: ["unsigned int fd", "char *buf", "size_t count"]
})
CREATE (sc_write_x64:Syscall {
hash: "mno345...",
index: 1,
name: "write",
entry_point: "sys_write",
parameters: ["unsigned int fd", "const char *buf", "size_t count"]
})
// x86 compat syscall entries (different entry points)
CREATE (sc_read_x86:Syscall {
hash: "pqr678...",
index: 3,
name: "read",
entry_point: "compat_sys_read",
parameters: ["unsigned int fd", "char *buf", "size_t count"]
})
// Connect table to syscalls
CREATE (table_x64)-[:HAS_SYSCALL {name: "read", index: 0}]->(sc_read_x64)
CREATE (table_x64)-[:HAS_SYSCALL {name: "write", index: 1}]->(sc_write_x64)
CREATE (table_x86)-[:HAS_SYSCALL {name: "read", index: 3}]->(sc_read_x86)
Windows: NT and Win32k SSDTs
// NT kernel blob
CREATE (ntoskrnl:Blob {hash: "win_abc..."})
// Win32k kernel blob
CREATE (win32k:Blob {hash: "win_def..."})
// NT syscall table (x64 only)
CREATE (table_nt:SyscallTable {
hash: "nt_hash...",
arch: "X64"
})
// Win32k syscall table (x64 only)
CREATE (table_win32k:SyscallTable {
hash: "w32k_hash...",
arch: "X64"
})
// Connect blobs to tables
CREATE (ntoskrnl)-[:HAS_SYSCALL_TABLE {name: "nt"}]->(table_nt)
CREATE (win32k)-[:HAS_SYSCALL_TABLE {name: "win32k"}]->(table_win32k)
// NT syscalls
CREATE (nt_open:Syscall {
hash: "nt_open_hash...",
index: 51,
name: "NtOpenFile",
entry_point: "NtOpenFile",
parameters: ["PHANDLE FileHandle", "ACCESS_MASK DesiredAccess", "..."]
})
// Win32k syscalls
CREATE (w32k_create:Syscall {
hash: "w32k_create_hash...",
index: 4156,
name: "NtUserCreateWindowEx",
entry_point: "NtUserCreateWindowEx",
parameters: ["DWORD dwExStyle", "PUNICODE_STRING lpClassName", "..."]
})
// Connect tables to syscalls
CREATE (table_nt)-[:HAS_SYSCALL {name: "NtOpenFile", index: 51}]->(nt_open)
CREATE (table_win32k)-[:HAS_SYSCALL {name: "NtUserCreateWindowEx", index: 4156}]->(w32k_create)
Diff Algorithm Integration
Relationship Name Uniqueness
The diff algorithm requires unique relationship name properties per parent node for HashMap-based comparison.
SyscallTable level:
- Blob → SyscallTable: Names are unique (
"x64","x86","nt","win32k") - ✅ No collisions possible
Syscall level:
- SyscallTable → Syscall: Names are syscall names (
"read","write", etc.) - ✅ Syscall names are unique within a table (by definition)
Recursive Diffing
When comparing two kernel versions:
- Blob level: Diff finds changed vmlinuz blob
- Recurse into SyscallTable: Diff compares x64 and x86 tables separately
- Recurse into Syscall: Diff identifies added/removed/modified syscalls
Example diff output:
MOD /boot/vmlinuz-5.15.0
MOD x64
NEW read (index: 0)
MOD write (index: 1) [parameters changed]
DEL oldcall (index: 999)
MOD x86
NEW read (index: 3)
Path Display in Frontend
With current diff API:
- Path shows:
/boot/vmlinuz/x64/read - Index is visible in
new_props.properties.index - Frontend can display:
"Syscall #0: read"
Future Enhancements
1. JSON Path Approach (Not Yet Implemented)
Problem: Current diff API only exposes the relationship name property in the path. The index property is lost.
Future Solution: Serialize all relationship properties as JSON:
Current path: /boot/vmlinuz/x64/read
JSON path: /boot/vmlinuz/{"name":"x64"}/{"name":"read","index":0}
Implementation Requirements:
- Modify
TreeDiffRecursiveProcedure.javato serialize relationship properties to JSON - Use JSON string as HashMap key (deterministic serialization with sorted keys)
- Frontend parses JSON segments to extract both name and index
- No GraphQL schema changes needed
Benefits:
- Generic solution for all relationship properties
- Self-contained path segments
- Backward compatible
2. Deduplication Refactoring
If syscall signature reuse becomes important, introduce a SyscallSignature node:
(SyscallTable)-[:HAS_SYSCALL {name: "read", index: 0}]->(Syscall {index: 0})
(Syscall)-[:HAS_SIGNATURE]->(SyscallSignature {
hash: "sig_hash...",
name: "read",
entry_point: "sys_read",
parameters: [...]
})
This enables:
- Same signature at different indices shares
SyscallSignaturenode - Tracks syscall renumbering across kernel versions
- Better deduplication for storage efficiency
GraphQL Schema Integration
The syscall data model will be added to graphql-api/type-defs.graphql in the graphql-api repository:
type SyscallTable implements Hashable {
hash: String! @unique
arch: String!
syscalls: [Syscall!]! @relationship(
type: "HAS_SYSCALL"
direction: OUT
properties: "HasSyscallRel"
)
blob: Blob! @relationship(
type: "HAS_SYSCALL_TABLE"
direction: IN
properties: "HasNameRel"
)
}
type Syscall implements Hashable {
hash: String! @unique
index: Int!
name: String!
entry_point: String!
parameters: [String!]!
table: SyscallTable! @relationship(
type: "HAS_SYSCALL"
direction: IN
properties: "HasSyscallRel"
)
}
type HasSyscallRel @relationshipProperties {
name: String!
index: Int!
}
# Add to Blob type:
type Blob implements Hashable {
# ... existing fields ...
has_syscall_tables: [SyscallTable!]! @relationship(
type: "HAS_SYSCALL_TABLE"
direction: OUT
properties: "HasNameRel"
)
}
References
- Diff algorithm implementation:
oswatcher-proceduresrepo —src/main/java/io/oswatcher/TreeDiffRecursiveProcedure.java - Plugin implementation:
plugins/plugins/syscalls.py