Languages: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
-fdyncode originally covered only ring-3 payloads (PEB walk, svc/syscall stubs, libSystem bridging). Ring-0 payloads (Windows drivers, Linux/Android kernel modules, macOS kexts) cannot simply reuse the ring-3 ABI: TEB/PEB does not exist, syscall instructions are user-to-kernel traps (drivers should not issue them), and x86_64 additionally requires a different code model and disabling the red zone.
- User mode (default): maintains existing PEB / syscall stub pipeline.
- Kernel mode:
- Disables
SyscallStubPass(svc/syscallare meaningless in ring-0). - Disables
WinPEBImportPass(PEB lives in user TEB, unreachable from ring-0). - Windows kernel
TargetDescclears TCB/PEB read templates and syscall register descriptions. - Injects platform-specific driver flags (see Section 3).
- Enables
KernelImportPassfor automatic resolver-backed callsite rewriting (Section 4). - Injects
-D__NEVERC_DYNCODE_KERNEL__=1so user-mode shim headers (<windows.h>/<unistd.h>/ etc.) issue#error, preventing accidental inclusion.
- Disables
-mdyncode-context= is a strict enum: only user or kernel. Invalid values must fail at the driver stage to prevent typos from generating bytecode at the wrong privilege level.
Execution level is an additional dimension of describeTriple():
TargetDesc::Level:UserorKernel.TargetDesc::KernelImport:WindowsKernelResolverShim/LinuxKallsymsShim/DarwinXNUKextShim/None. Only meaningful whenLevel == Kernel.TargetDesc::KernelInjectFlags: kernel-mode-only driver flag static array.
Adding kernel-mode support for a new OS/arch is still "add one table row".
| Dimension | x86_64 kernel | AArch64 kernel |
|---|---|---|
| Red zone | -mno-red-zone |
Naturally absent |
| Code model | -mcmodel=kernel |
Reuses existing -mcmodel=small |
| Implicit SIMD | -mno-sse -mno-sse2 -mno-mmx |
-mgeneral-regs-only |
| Stack probe | Inherits existing -mno-stack-arg-probe |
Same |
These flags stack as "user-mode baseline + kernel increment": cc1 processes later -m... flags as overrides.
Ring-0 symbol resolution varies significantly across platforms:
- Windows drivers:
PsLoadedModuleList+RtlFindExportedRoutineByName, orMmGetSystemRoutineAddress. - Linux/Android:
kallsyms_lookup_name(5.7+ requires kprobe workaround), or ksymtab. - macOS:
OSKextLookupKextWithIdentifier+ Mach-O symbol table.
KernelImportPass automatically rewrites unresolved extern direct calls to resolver-backed indirect calls. Users write normal C; the pass handles the rewriting.
When the module contains extern direct calls requiring a resolver, user code:
void dyncode_entry(void) {
printk("hello %d\n", 7);
}is transformed to the equivalent of:
void dyncode_entry(void *__resolver, void *__cookie) {
void *fn = __resolver(hash("printk"), __cookie);
((int(*)(const char*, ...))fn)("hello %d\n", 7);
}The user does not need to manually write resolver/cookie parameters — KernelImportPass injects them at the entry front. The loader passes them when calling the dyncode. Pure computation payloads or payloads that explicitly accept neverc_kern_resolve_t do not trigger this automatic prepend.
Each direct extern callsite is replaced in-place:
- Load resolver and cookie from internal globals
- Call
resolver(FNV1a_hash(bare_name), cookie) - Cast returned
void*to the correct function pointer type - Forward all original arguments; return the result
Callsite rewriting (rather than generic wrappers) is chosen to support variadic helpers like printk("x=%d", v). Generic wrappers in LLVM IR cannot reliably forward anonymous variadic arguments.
Address-taken externs are not automatically wrapped; diagnostics direct the user to call helpers directly or have the loader pass pre-resolved function pointers.
FNV-1a 64-bit, identical to neverc_kern_hash() in <neverc/dyncode/kernel.h>:
uint64_t h = 0xcbf29ce484222325ull;
while (*s) { h ^= (unsigned char)*s++; h *= 0x100000001b3ull; }The pass strips leading underscores (Mach-O _ prefix) before hashing for cross-platform consistency.
typedef int (*Entry)(void *resolver, void *cookie /*, user params... */);
Entry e = (Entry)dyncode_memory;
e(my_resolver, my_cookie);Under automatic resolver rewriting, the first two parameters are always (resolver, cookie). If the user explicitly declares dyncode_entry(neverc_kern_resolve_t resolver, void *cookie, ...) and calls the resolver themselves, the entry signature is preserved as-is.
If KernelImportPass cannot complete rewriting (e.g., cannot find entry function, inline asm directly references symbols, third-party MIR pass re-inserts externs):
- IR layer: pass outputs diagnostics or skips address-taken extern scenarios
- MIR layer:
DynCodeMIRPrepPass::auditExternalReferencesre-audits - Extractor: rejects all unresolved relocations
Users always see clear diagnostics; incorrect .bin files are never silently generated.
Tables/KernelHelperNames.def lists common ring-0 helpers for each OS (~120 Linux, ~120 Windows, ~60 Darwin). Both diagnostic paths query this table:
- User mode: when driver code is accidentally used in ring-3, the extractor emits
"'<name>' is a <os> kernel-only helper ... pass -mdyncode-context=kernel". - Kernel mode: when inline asm bypasses
KernelImportPass, the table identifies "you meant to resolve a kernel helper" and points to the appropriateKernelImportbranch.
The table is user-extensible via CMake variable NEVERC_EXTRA_KernelHelperNames.
- Android ring-3: bionic + Linux syscall ABI (
svc #0, x8=nr). Reuses Linux arm64 table entries. - Android ring-0: pure Linux kernel (GKI/KMI). Reuses Linux kernel's
LinuxKallsymsShim+KernelInjectFlags. The triple is kept separate for future GKI/KMI constraints.
| Mode | Allowed shim headers | Rejected shim headers |
|---|---|---|
User-mode -fdyncode |
<windows.h> / <unistd.h> / <fcntl.h> / <sys/stat.h> / <sys/mman.h> / <string.h> / <stdlib.h> |
<neverc/dyncode/kernel.h> |
Kernel-mode -mdyncode-context=kernel |
<neverc/dyncode/kernel.h> / <string.h> / <stdlib.h> / <stddef.h> / <stdint.h> (pure type headers) |
<windows.h> / <unistd.h> / <fcntl.h> / <sys/stat.h> / <sys/mman.h> |
<neverc/dyncode/kernel.h> exposes:
neverc_kern_resolve_ttype aliasneverc_kern_hash(): inline FNV-1a 64-bit hash, identical to the pass's internal algorithmNEVERC_KERNEL_ENTRY: kernel entry marker macro (equivalent to__attribute__((used)))
The shim layer intentionally does not emulate any OS kernel SDK — kernel ABI differences across platforms are too large for a pseudo-universal interface.
#include <neverc/dyncode/kernel.h>
NEVERC_KERNEL_ENTRY
int dyncode_entry(int a, int b) {
int s = a * 13 + b * 7;
for (int i = 0; i < 4; ++i) s ^= (s << 3) + i;
return s;
}neverc -fdyncode -mdyncode-context=kernel \
-target aarch64-linux-gnu dyncode.c -o sc.bin#include <neverc/dyncode/kernel.h>
typedef void (*PrintkFn)(const char *fmt, int a, int b);
typedef void *(*AllocFn)(unsigned int kind, unsigned long size);
NEVERC_KERNEL_ENTRY
int dyncode_entry(neverc_kern_resolve_t resolver, void *cookie,
int a, int b) {
PrintkFn pf = (PrintkFn)resolver(neverc_kern_hash("printk"), cookie);
AllocFn af = (AllocFn)resolver(neverc_kern_hash("kmalloc"), cookie);
if (pf) pf("neverc: %d %d\n", a, b);
void *blk = af ? af(0, 64) : (void *)0;
(void)blk;
return a * 13 + b * 7;
}The first two parameters are (neverc_kern_resolve_t resolver, void *cookie), prepared by the hosting driver / kext / LKM. The resolver implementation is hosting-side (Linux LKM wraps kallsyms_lookup_name; Windows driver wraps MmGetSystemRoutineAddress; XNU kext uses OSKextLookup*).
struct sym_table { const char *name; void *addr; };
static struct sym_table st[] = {
{ "printk", (void*)printk },
{ "kmalloc", (void*)kmalloc },
{ NULL, NULL }
};
static uint64_t fnv1a(const char *s) {
uint64_t h = 0xcbf29ce484222325ull;
while (*s) { h ^= (unsigned char)*s++; h *= 0x100000001b3ull; }
return h;
}
static void *my_resolver(uint64_t hash, void *cookie) {
struct sym_table *tbl = cookie;
for (int i = 0; tbl[i].name; ++i)
if (fnv1a(tbl[i].name) == hash) return tbl[i].addr;
return NULL;
}
typedef int (*Entry)(void *(*)(uint64_t, void*), void*, int, int);
Entry e = (Entry)kmem;
int result = e(my_resolver, st, 1, 2);This is the hosting driver's code, compiled separately from the dyncode.
| Phase | Status | Content |
|---|---|---|
| Kernel context switch + platform flags | Done | -mdyncode-context=kernel, KernelInjectFlags, pass gates |
| Resolver rewrite + diagnostic fallback | Done | KernelImportPass automatic callsite rewriting; MIR / extractor fallback |
| Ring-0 pure computation payload | Done | 8 triple coverage |
| Resolver-based payload | Done | <neverc/dyncode/kernel.h> + stress test coverage |
| Automatic extern → resolver rewrite | Done | KernelImportPass with implicit parameter injection |
Kernel SDK header subsets (<linux/kernel.h> / <wdm.h>) |
Planned | To be added based on real driver payload needs |