Native API and the SSDT
What is it?
The Native API is the kernel's NtXxx and ZwXxx entry-point contract: every call returns an NTSTATUS, and every call is dispatched through a numbered System Service Descriptor Table (SSDT), the mechanism Windows uses inside ntoskrnl.exe. ssdt_dispatch() takes a service number, splits it into a table and an index, and calls the registered handler.
Two hardware paths reach it: the SYSCALL/SYSRET fast path and the older INT 0x2E software interrupt. The earlier Linux-style SYS_* calls on INT 0x80 still work beside it as a separate number space.
There are two tables. Table 0, the main table, holds kernel NtXxx services from 0x0000 (SSDT_NtClose is 0x0000) and is largely populated. Table 1, the shadow table from 0x1000, is reserved for the Win32k NtGdiXxx and NtUserXxx services and is still all stubs; 08-graphics-ui TODO-15 fills it.
How does it work?
ssdt_init() in ssdt.c fills both 1024-slot tables with a stub that returns STATUS_NOT_IMPLEMENTED and logs SSDT initialized: N main slots (last=0x...), shadow stub ready. Subsystems then call ssdt_register(service_number, handler) to install real handlers; 25 kernel source files do so today.
ssdt_dispatch() is the single point both hardware paths go through. For each call it checks the index against the table's capacity, applies the per-process system-call filter (free when no process has installed one), applies a coarse category check, and then calls the handler, wrapped in audit hooks only if an audit hook is registered. A per-thread previous-mode flag records whether the caller was user mode (NtXxx) or kernel mode (ZwXxx in zw.h, which calls ssdt_dispatch() directly). Probing user buffers with ProbeForRead and ProbeForWrite is each handler's job, not the dispatcher's, and not every handler does it yet.
flowchart LR
U["ring 3: SYSCALL or INT 0x2E"] --> D["ssdt_dispatch()"]
K["kernel: ZwXxx"] --> D
D --> F{"filter, category, audit"}
F --> H["NtXxx handler"]
H --> R["NTSTATUS to caller"]
syscall_init_fast() in syscall_fast.c programs the IA32_STAR, IA32_LSTAR and IA32_FMASK MSRs and sets EFER.SCE, but only after confirming the GDT user segments are in the order SYSRET requires. The entry code in syscall_entry.asm runs swapgs, switches to the per-CPU kernel stack and calls into ssdt_dispatch(). Both paths use the Windows x64 register convention: RAX holds the service number and R10, RDX, R8 and R9 the first four arguments.
What are its interfaces?
| Interface | Purpose |
|---|---|
ssdt_dispatch(), ssdt_register(), ssdt_get_table() |
Dispatch, register and inspect (ssdt.h) |
SSDT_NtXxx constants, SSDT_MAIN_COUNT, SSDT_LAST_MAIN_INDEX |
Service numbers (service_numbers.h) |
NTSTATUS, NT_SUCCESS, NT_ERROR |
Return codes and severity macros (ntstatus.h) |
HANDLE, IO_STATUS_BLOCK, OBJECT_ATTRIBUTES, UNICODE_STRING, CLIENT_ID |
Shared NT types (nt_types.h) |
ZwClose, ZwCreateFile, ZwReadFile and others |
Kernel-mode aliases without user-buffer probes (zw.h) |
ProbeForRead(), ProbeForWrite() |
User-buffer validation |
nt_audit_register(), nt_audit_unregister() |
System-call audit hooks for kernel code (nt_audit.h) |
ProcessSystemCallFilterPolicy on NtSetInformationProcess |
Per-process allow-list of system calls (syscall_filter.h) |
RtlNtStatusToDosError() |
Translate an NTSTATUS to a Win32 error code |
How do I use it?
ssdt_init() runs in Phase 3 before the fast path is enabled; there is no setting.
bash scripts/test.sh SUITE=abi # NTSTATUS, dispatch, ZwXxx, audit and filter tests
Kernel code calls a ZwXxx alias directly, for example ZwClose(handle). User programs do not yet have NtXxx stubs: user/include/syscall.h still wraps the legacy INT 0x80 calls. A test program can load an SSDT_NtXxx number into RAX and execute syscall directly. The tests include test_nt_types.c and test_nt_audit.c.
What is not implemented yet?
- ntdll and user-mode
NtXxxstubs do not exist yet; they belong to 12-user-platform-sdk TODO-04. - Kernel-to-user callbacks (
KeUserModeCallback) have no consumer or continuation mechanism yet (Kernel-to-User-Mode Callback Dispatch). - Write-protecting the SSDT needs a cross-CPU TLB shootdown first (SSDT Integrity Protection).
- The shadow table is all stubs until 08-graphics-ui TODO-15 fills it.
- Registry, token, timer, ALPC, debug and directory handlers are partly done, waiting on their owning roadmap files (Native API roadmap).
- User-buffer validation is incomplete: some handlers write through caller pointers without probing them, for example the
IO_STATUS_BLOCKinNtFlushBuffersFile(nt_file.c); a shared checked write-back helper is planned in section 6. - Calls with more than four arguments: the
SYSCALLentry passes only the four register arguments and sets the fifth and sixth to zero, instead of reading them from the user stack at[RSP+0x28]as Windows does (syscall_entry.asm). A handler that needs more must take a packed structure; reading stack arguments is planned in section 3. - Legacy system call numbers: many roadmaps still reserve fixed
INT 0x80numbers above the live table, and several reserve the same number; one allocation rule and a lint check are planned in LegacySYS_*Number Allocation Across Roadmaps.
How does it compare with Windows 11 and Linux?
Windows has the SSDT and the shadow SSDT; Linux has sys_call_table[]. Impossible OS matches the dispatch model and adds two things neither offers: a low-cost system-call audit hook (Windows relies on ETW, Linux on seccomp and ptrace) and a documented ZwXxx kernel alias contract, which Windows keeps internal. Per-process system-call filtering matches the Windows system-call disable policy and Linux seccomp. Asynchronous IO_STATUS_BLOCK completion, device I/O control and Registry calls are still open, and neither table is write-protected yet.