Created
March 20, 2026 19:40
-
-
Save float64co/8ae3017a8d79ce1aefb82e6673a7cd43 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT-Modern-Variant | |
| /* | |
| * fugue.c — Unified Security Runtime for Security-Sensitive Kernel Modules | |
| * | |
| * Condenses ten protective subsystems into one coherent LKM: | |
| * | |
| * A. Load-Gate: Ed25519 signature verification on every .ko before init runs | |
| * B. Build Provenance: Signed ELF note carries compiler/header/git hash; reject | |
| * modules built outside an approved environment | |
| * C. Symbol Firewall: Per-module declared symbol manifest enforced via kprobes; | |
| * undeclared kernel API calls are blocked at the call site | |
| * D. Text Integrity: .text pages marked RO after load; periodic SHA-256 | |
| * re-verification; mismatch → module kill or panic | |
| * E. Hook Registry: HMAC-signed ledger of every netfilter/LSM/kprobe/ftrace | |
| * hook; any unledgered hook change is an immediate alert | |
| * F. Audit Log: Append-only HMAC-chained ring in reserved physical RAM; | |
| * sealed on suspicious unload; exposed via /dev/fugue_log | |
| * G. Unload Guard: delete_module requires a TPM2-sealed time-limited token | |
| * delivered via ioctl; root alone is not sufficient | |
| * H. Call Isolator: Cross-module calls traverse a trampoline that checks | |
| * caller module identity against an allowed-callers manifest | |
| * I. Developer Sandbox: Optional load-mode flag: I/O trapped, network namespaced | |
| * away, kmalloc red-zoned, quota-limited; graduates to | |
| * production only with a dual-signed clearance token | |
| * J. Canary Injector: kretprobe every exported function for per-module 64-bit | |
| * stack canaries; kmalloc/kfree wrapped with guard pages | |
| * | |
| * Build: | |
| * make -C /lib/modules/$(uname -r)/build M=$(pwd) modules | |
| * | |
| * Kernel version target: 6.x | |
| * | |
| * Policy blob at /etc/fugue/policy.bin (signed, described below). | |
| * TPM token delivery via ioctl(fd, FUGUE_IOC_UNLOAD_TOKEN, &tok). | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/kernel.h> | |
| #include <linux/init.h> | |
| #include <linux/slab.h> | |
| #include <linux/spinlock.h> | |
| #include <linux/rwsem.h> | |
| #include <linux/rbtree.h> | |
| #include <linux/list.h> | |
| #include <linux/kprobes.h> | |
| #include <linux/ftrace.h> | |
| #include <linux/netfilter.h> | |
| #include <linux/lsm_hooks.h> | |
| #include <linux/set_memory.h> | |
| #include <linux/vmalloc.h> | |
| #include <linux/mm.h> | |
| #include <linux/highmem.h> | |
| #include <linux/crypto.h> | |
| #include <linux/scatterlist.h> | |
| #include <linux/err.h> | |
| #include <linux/uaccess.h> | |
| #include <linux/fs.h> | |
| #include <linux/miscdevice.h> | |
| #include <linux/ioctl.h> | |
| #include <linux/mutex.h> | |
| #include <linux/percpu.h> | |
| #include <linux/timer.h> | |
| #include <linux/jiffies.h> | |
| #include <linux/workqueue.h> | |
| #include <linux/memblock.h> | |
| #include <linux/elf.h> | |
| #include <linux/kallsyms.h> | |
| #include <linux/nsproxy.h> | |
| #include <linux/net.h> | |
| #include <linux/tpm.h> | |
| #include <crypto/hash.h> | |
| #include <crypto/sha2.h> | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Float64"); | |
| MODULE_DESCRIPTION("fugue: unified LKM security runtime (A-J)"); | |
| MODULE_VERSION("0.0.1"); | |
| /* ========================================================================= | |
| * Constants and tunables | |
| * ========================================================================= */ | |
| #define FUGUE_MAX_MODULES 64 | |
| #define FUGUE_MAX_SYMBOLS 256 /* per-module symbol manifest */ | |
| #define FUGUE_MAX_HOOKS 512 /* total tracked hooks across all modules */ | |
| #define FUGUE_MAX_CALLERS 32 /* per-module allowed-callers list */ | |
| #define FUGUE_LOG_ENTRIES 4096 /* ring log depth */ | |
| #define FUGUE_CANARY_MAGIC 0xC17ADE1C17ADE1CULL | |
| #define FUGUE_TEXT_CHECK_HZ (60 * HZ) /* re-verify .text every 60 s */ | |
| #define FUGUE_TOKEN_TTL_SEC 30 /* unload token validity window */ | |
| #define FUGUE_HMAC_LEN 32 /* SHA-256 HMAC output */ | |
| #define FUGUE_SIG_LEN 64 /* Ed25519 signature */ | |
| #define FUGUE_HASH_LEN 32 /* SHA-256 digest */ | |
| /* /dev node name */ | |
| #define FUGUE_DEV_NAME "fugue_log" | |
| /* ========================================================================= | |
| * ioctl interface (userspace: #include this header or copy the defines) | |
| * ========================================================================= */ | |
| #define FUGUE_IOC_MAGIC 'C' | |
| /* Submit a TPM-sealed unload token for module <name> */ | |
| struct fugue_unload_token { | |
| char mod_name[MODULE_NAME_LEN]; | |
| u8 hmac[FUGUE_HMAC_LEN]; /* HMAC-SHA256(mod_name || timestamp || nonce) */ | |
| u64 timestamp_sec; | |
| u8 nonce[16]; | |
| u8 tpm_quote[256]; /* TPM2_Quote blob covering the above */ | |
| }; | |
| /* Graduate a sandboxed module to production mode */ | |
| struct fugue_graduate_token { | |
| char mod_name[MODULE_NAME_LEN]; | |
| u8 sig_dev1[FUGUE_SIG_LEN]; /* Ed25519 sig from developer key 1 */ | |
| u8 sig_dev2[FUGUE_SIG_LEN]; /* Ed25519 sig from developer key 2 */ | |
| }; | |
| #define FUGUE_IOC_UNLOAD_TOKEN _IOW(FUGUE_IOC_MAGIC, 1, struct fugue_unload_token) | |
| #define FUGUE_IOC_GRADUATE _IOW(FUGUE_IOC_MAGIC, 2, struct fugue_graduate_token) | |
| #define FUGUE_IOC_DUMP_HOOKS _IOR(FUGUE_IOC_MAGIC, 3, unsigned int) | |
| #define FUGUE_IOC_SEAL_LOG _IO (FUGUE_IOC_MAGIC, 4) | |
| /* ========================================================================= | |
| * § A/B Policy database (loaded from signed /etc/fugue/policy.bin) | |
| * ========================================================================= | |
| * | |
| * Binary layout of policy.bin: | |
| * [4B magic "CTDL"] [4B version] [4B entry_count] | |
| * entry_count × fugue_policy_entry | |
| * [64B Ed25519 signature over everything above] | |
| * | |
| * Kernel-resident public key is compiled in below (replace with real key). | |
| */ | |
| /* Compiled-in Ed25519 public key (32 bytes). Replace with real key material. */ | |
| static const u8 fugue_pubkey[32] = { | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| }; | |
| /* Second developer key for dual-sign graduation (subsystem I). */ | |
| static const u8 fugue_pubkey2[32] = { | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| }; | |
| /* HMAC key for audit log chaining (subsystem F). Generated at boot. */ | |
| static u8 fugue_log_hmac_key[32]; | |
| struct fugue_symbol_entry { | |
| char name[64]; | |
| unsigned long addr; /* resolved at policy-load time */ | |
| }; | |
| struct fugue_caller_entry { | |
| char mod_name[MODULE_NAME_LEN]; | |
| }; | |
| struct fugue_policy_entry { | |
| char mod_name[MODULE_NAME_LEN]; | |
| /* A: expected Ed25519 signature of the raw .ko ELF image */ | |
| u8 ko_sig[FUGUE_SIG_LEN]; | |
| /* B: SHA-256 of (compiler_path || kernel_header_tree || cflags || git_sha) */ | |
| u8 build_env_hash[FUGUE_HASH_LEN]; | |
| /* C: symbol manifest */ | |
| u32 sym_count; | |
| struct fugue_symbol_entry syms[FUGUE_MAX_SYMBOLS]; | |
| /* G: must this module require a TPM token to unload? */ | |
| bool unload_guarded; | |
| /* H: which modules may call into this one? */ | |
| u32 caller_count; | |
| struct fugue_caller_entry callers[FUGUE_MAX_CALLERS]; | |
| /* I: start in sandbox mode? */ | |
| bool sandbox_mode; | |
| bool graduated; /* set true after dual-sign graduation */ | |
| /* D: stored .text SHA-256 baseline (filled after first load) */ | |
| u8 text_hash[FUGUE_HASH_LEN]; | |
| unsigned long text_start; | |
| unsigned long text_len; | |
| }; | |
| static struct fugue_policy_entry policy_db[FUGUE_MAX_MODULES]; | |
| static int policy_count; | |
| static DECLARE_RWSEM(policy_rwsem); | |
| /* ========================================================================= | |
| * § F Audit log (append-only HMAC-chained ring in reserved physical RAM) | |
| * ========================================================================= | |
| * | |
| * Reserve physical pages at boot with kernel parameter: | |
| * memmap=4M$0x3f000000 | |
| * Then pass the same address as module parameter fugue_log_phys. | |
| * | |
| * If no reserved region is given, we fall back to a vmalloc'd ring. | |
| * The fallback is less tamper-resistant but still functional. | |
| */ | |
| static ulong fugue_log_phys; | |
| module_param(fugue_log_phys, ulong, 0400); | |
| MODULE_PARM_DESC(fugue_log_phys, "Physical base of reserved audit-log RAM"); | |
| static ulong fugue_log_size = 4 * 1024 * 1024; | |
| module_param(fugue_log_size, ulong, 0400); | |
| MODULE_PARM_DESC(fugue_log_size, "Size of reserved audit-log region (bytes)"); | |
| #define FUGUE_LOG_RECORD_MAGIC 0xC10671C1UL | |
| struct fugue_log_record { | |
| u32 magic; | |
| u32 seq; | |
| u64 ktime_ns; | |
| char mod_name[MODULE_NAME_LEN]; | |
| char event[128]; | |
| u8 prev_hmac[FUGUE_HMAC_LEN]; /* HMAC of previous record */ | |
| u8 this_hmac[FUGUE_HMAC_LEN]; /* HMAC of this record (excluding this field) */ | |
| }; | |
| struct fugue_log_header { | |
| u32 magic; /* 0xC10671DE */ | |
| u32 version; | |
| u32 head; /* index of oldest record */ | |
| u32 tail; /* index of next write slot */ | |
| u32 count; | |
| u32 capacity; | |
| bool sealed; | |
| u8 seal_hmac[FUGUE_HMAC_LEN]; | |
| /* records follow immediately */ | |
| }; | |
| static struct fugue_log_header *fugue_log; | |
| static struct fugue_log_record *fugue_log_records; | |
| static spinlock_t fugue_log_lock; | |
| static u32 fugue_log_seq; | |
| static u8 fugue_log_prev_hmac[FUGUE_HMAC_LEN]; | |
| /* ========================================================================= | |
| * § E Hook Registry Ledger | |
| * ========================================================================= | |
| * | |
| * Every hook registration made by a fugue-managed module must go through | |
| * fugue_hook_register(). We record it here and HMAC the entire table. | |
| * A background work item re-verifies the table HMAC periodically. | |
| */ | |
| enum fugue_hook_type { | |
| HOOK_NETFILTER, | |
| HOOK_LSM, | |
| HOOK_KPROBE, | |
| HOOK_FTRACE, | |
| HOOK_TRACEPOINT, | |
| }; | |
| struct fugue_hook_record { | |
| bool active; | |
| enum fugue_hook_type type; | |
| char mod_name[MODULE_NAME_LEN]; | |
| char hook_name[64]; | |
| unsigned long handler_addr; | |
| u64 registered_ktime; | |
| }; | |
| static struct fugue_hook_record hook_ledger[FUGUE_MAX_HOOKS]; | |
| static int hook_ledger_count; | |
| static DEFINE_SPINLOCK(hook_ledger_lock); | |
| static u8 hook_ledger_hmac[FUGUE_HMAC_LEN]; | |
| /* ========================================================================= | |
| * § J Canary table (per-module canary values and kretprobe list) | |
| * ========================================================================= */ | |
| struct fugue_canary_entry { | |
| char mod_name[MODULE_NAME_LEN]; | |
| u64 canary; | |
| struct list_head probe_list; /* list of struct fugue_krp_node */ | |
| }; | |
| struct fugue_krp_node { | |
| struct kretprobe krp; | |
| struct list_head list; | |
| }; | |
| struct fugue_krp_instance_data { | |
| u64 saved_canary; | |
| }; | |
| static struct fugue_canary_entry canary_table[FUGUE_MAX_MODULES]; | |
| static int canary_table_count; | |
| static DEFINE_SPINLOCK(canary_lock); | |
| /* ========================================================================= | |
| * § D Text integrity verification work item | |
| * ========================================================================= */ | |
| static struct delayed_work fugue_text_check_work; | |
| /* ========================================================================= | |
| * § G Pending unload tokens | |
| * ========================================================================= */ | |
| struct fugue_pending_token { | |
| char mod_name[MODULE_NAME_LEN]; | |
| u64 issued_ktime_sec; | |
| u8 nonce[16]; | |
| bool valid; | |
| }; | |
| #define FUGUE_MAX_PENDING_TOKENS 16 | |
| static struct fugue_pending_token pending_tokens[FUGUE_MAX_PENDING_TOKENS]; | |
| static DEFINE_SPINLOCK(token_lock); | |
| /* ========================================================================= | |
| * § I Sandbox slab pool per module | |
| * ========================================================================= */ | |
| struct fugue_sandbox_pool { | |
| char mod_name[MODULE_NAME_LEN]; | |
| struct kmem_cache *cache; | |
| atomic_t alloc_count; | |
| u32 quota; /* max simultaneous allocations */ | |
| }; | |
| static struct fugue_sandbox_pool sandbox_pools[FUGUE_MAX_MODULES]; | |
| static int sandbox_pool_count; | |
| static DEFINE_SPINLOCK(sandbox_lock); | |
| /* ========================================================================= | |
| * Forward declarations | |
| * ========================================================================= */ | |
| static int fugue_log_init(void); | |
| static void fugue_log_write(const char *mod_name, const char *event); | |
| static void fugue_log_seal(void); | |
| static int fugue_policy_load(void); | |
| static struct fugue_policy_entry *fugue_find_policy(const char *name); | |
| static int fugue_verify_ko_signature(const void *ko_image, size_t ko_size, | |
| const u8 *expected_sig); | |
| static int fugue_verify_build_provenance(const void *ko_image, size_t ko_size, | |
| const u8 *expected_env_hash); | |
| static int fugue_hash_text_segment(unsigned long start, unsigned long len, | |
| u8 *out_hash); | |
| static void fugue_enforce_text_ro(unsigned long start, unsigned long len); | |
| static int fugue_hook_ledger_add(enum fugue_hook_type type, | |
| const char *mod_name, | |
| const char *hook_name, | |
| unsigned long handler_addr); | |
| static void fugue_hook_ledger_remove(unsigned long handler_addr); | |
| static void fugue_hook_ledger_recompute_hmac(void); | |
| static bool fugue_hook_ledger_verify_hmac(void); | |
| static int fugue_canary_install(struct fugue_policy_entry *pol, | |
| struct module *mod); | |
| static void fugue_canary_remove(const char *mod_name); | |
| static int fugue_sandbox_create(struct fugue_policy_entry *pol); | |
| static void fugue_sandbox_destroy(const char *mod_name); | |
| static int fugue_token_consume(const struct fugue_unload_token *tok); | |
| static int fugue_call_check(const char *callee_mod, unsigned long caller_addr); | |
| /* ========================================================================= | |
| * § F Audit log implementation | |
| * ========================================================================= */ | |
| static int fugue_compute_hmac(const void *data, size_t len, | |
| const u8 *key, size_t key_len, u8 *out) | |
| { | |
| struct crypto_shash *tfm; | |
| struct shash_desc *desc; | |
| int ret; | |
| tfm = crypto_alloc_shash("hmac(sha256)", 0, 0); | |
| if (IS_ERR(tfm)) | |
| return PTR_ERR(tfm); | |
| ret = crypto_shash_setkey(tfm, key, key_len); | |
| if (ret) | |
| goto out_free_tfm; | |
| desc = kzalloc(sizeof(*desc) + crypto_shash_descsize(tfm), GFP_ATOMIC); | |
| if (!desc) { | |
| ret = -ENOMEM; | |
| goto out_free_tfm; | |
| } | |
| desc->tfm = tfm; | |
| ret = crypto_shash_digest(desc, data, len, out); | |
| kfree(desc); | |
| out_free_tfm: | |
| crypto_free_shash(tfm); | |
| return ret; | |
| } | |
| static int fugue_log_init(void) | |
| { | |
| size_t header_size = sizeof(struct fugue_log_header); | |
| u32 capacity; | |
| spin_lock_init(&fugue_log_lock); | |
| fugue_log_seq = 0; | |
| memset(fugue_log_prev_hmac, 0, FUGUE_HMAC_LEN); | |
| /* Generate log HMAC key from kernel PRNG */ | |
| get_random_bytes(fugue_log_hmac_key, sizeof(fugue_log_hmac_key)); | |
| if (fugue_log_phys) { | |
| /* Map the reserved physical region */ | |
| fugue_log = (struct fugue_log_header *) | |
| ioremap_cache(fugue_log_phys, fugue_log_size); | |
| if (!fugue_log) { | |
| pr_err("fugue: failed to map reserved log region at 0x%lx\n", | |
| fugue_log_phys); | |
| goto fallback; | |
| } | |
| capacity = (fugue_log_size - header_size) | |
| / sizeof(struct fugue_log_record); | |
| } else { | |
| fallback: | |
| pr_warn("fugue: no reserved RAM; using vmalloc fallback for audit log\n"); | |
| fugue_log = vmalloc(sizeof(struct fugue_log_header) + | |
| FUGUE_LOG_ENTRIES * | |
| sizeof(struct fugue_log_record)); | |
| if (!fugue_log) | |
| return -ENOMEM; | |
| capacity = FUGUE_LOG_ENTRIES; | |
| } | |
| fugue_log->magic = 0xC10671DEUL; | |
| fugue_log->version = 1; | |
| fugue_log->head = 0; | |
| fugue_log->tail = 0; | |
| fugue_log->count = 0; | |
| fugue_log->capacity = capacity; | |
| fugue_log->sealed = false; | |
| fugue_log_records = (struct fugue_log_record *)(fugue_log + 1); | |
| return 0; | |
| } | |
| static void fugue_log_write(const char *mod_name, const char *event) | |
| { | |
| struct fugue_log_record *rec; | |
| unsigned long flags; | |
| u32 slot; | |
| if (!fugue_log || fugue_log->sealed) | |
| return; | |
| spin_lock_irqsave(&fugue_log_lock, flags); | |
| slot = fugue_log->tail % fugue_log->capacity; | |
| rec = &fugue_log_records[slot]; | |
| rec->magic = FUGUE_LOG_RECORD_MAGIC; | |
| rec->seq = fugue_log_seq++; | |
| rec->ktime_ns = ktime_get_ns(); | |
| strscpy(rec->mod_name, mod_name ? mod_name : "<fugue>", | |
| MODULE_NAME_LEN); | |
| strscpy(rec->event, event, sizeof(rec->event)); | |
| memcpy(rec->prev_hmac, fugue_log_prev_hmac, FUGUE_HMAC_LEN); | |
| /* HMAC of everything except this_hmac field */ | |
| fugue_compute_hmac(rec, | |
| offsetof(struct fugue_log_record, this_hmac), | |
| fugue_log_hmac_key, sizeof(fugue_log_hmac_key), | |
| rec->this_hmac); | |
| memcpy(fugue_log_prev_hmac, rec->this_hmac, FUGUE_HMAC_LEN); | |
| fugue_log->tail = (fugue_log->tail + 1) % fugue_log->capacity; | |
| if (fugue_log->count < fugue_log->capacity) | |
| fugue_log->count++; | |
| else | |
| fugue_log->head = (fugue_log->head + 1) % fugue_log->capacity; | |
| spin_unlock_irqrestore(&fugue_log_lock, flags); | |
| } | |
| static void fugue_log_seal(void) | |
| { | |
| unsigned long flags; | |
| if (!fugue_log) | |
| return; | |
| spin_lock_irqsave(&fugue_log_lock, flags); | |
| fugue_log->sealed = true; | |
| memcpy(fugue_log->seal_hmac, fugue_log_prev_hmac, FUGUE_HMAC_LEN); | |
| spin_unlock_irqrestore(&fugue_log_lock, flags); | |
| pr_info("fugue: audit log sealed\n"); | |
| } | |
| /* ========================================================================= | |
| * § A .ko signature verification stub | |
| * ========================================================================= | |
| * | |
| * Full Ed25519 verification requires linking against the kernel's asymmetric | |
| * key subsystem (CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE). The stub below | |
| * shows the call structure; replace the body with the real crypto call once | |
| * the kernel crypto API akcipher path for Ed25519 is confirmed for your | |
| * kernel version. | |
| */ | |
| static int fugue_verify_ko_signature(const void *ko_image, size_t ko_size, | |
| const u8 *expected_sig) | |
| { | |
| /* | |
| * Production implementation: | |
| * 1. SHA-512 hash the raw ko_image bytes. | |
| * 2. Call crypto_akcipher_verify() with fugue_pubkey and expected_sig. | |
| * 3. Return 0 on success, -EKEYREJECTED on failure. | |
| * | |
| * For kernels with CONFIG_MODULE_SIG the in-tree module signature check | |
| * already provides this; fugue supplements it by also verifying against | |
| * its own out-of-band key so a compromised distro key cannot bypass us. | |
| */ | |
| if (!ko_image || !expected_sig) | |
| return -EINVAL; | |
| /* Stub: always pass in this skeleton — replace with real verify */ | |
| pr_debug("fugue: [A] signature check (stub) for %zu-byte image\n", | |
| ko_size); | |
| return 0; | |
| } | |
| /* ========================================================================= | |
| * § B Build provenance verification | |
| * ========================================================================= | |
| * | |
| * The .ko ELF must contain a note section named "FUGUE_PROV" holding: | |
| * SHA-256(compiler_abspath || '\0' || kernel_header_tree_hash || | |
| * cflags_string || '\0' || git_commit_sha40) | |
| * signed with the same Ed25519 key as the module. | |
| * | |
| * This stub locates the ELF note and compares its hash to the policy entry. | |
| */ | |
| static int fugue_verify_build_provenance(const void *ko_image, | |
| size_t ko_size, | |
| const u8 *expected_env_hash) | |
| { | |
| const Elf64_Ehdr *ehdr = ko_image; | |
| const Elf64_Shdr *shdr; | |
| const char *shstrtab; | |
| int i; | |
| if (ko_size < sizeof(*ehdr) || | |
| memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) | |
| return -ENOEXEC; | |
| if (ehdr->e_shoff == 0 || ehdr->e_shstrndx == SHN_UNDEF) | |
| return -EINVAL; | |
| shdr = (const Elf64_Shdr *)((const u8 *)ko_image + ehdr->e_shoff); | |
| shstrtab = (const char *)ko_image + | |
| shdr[ehdr->e_shstrndx].sh_offset; | |
| for (i = 0; i < ehdr->e_shnum; i++) { | |
| if (shdr[i].sh_type != SHT_NOTE) | |
| continue; | |
| if (strcmp(shstrtab + shdr[i].sh_name, ".note.fugue_prov") != 0) | |
| continue; | |
| /* | |
| * Found the provenance note. Its desc field is a 32-byte SHA-256 | |
| * of the build environment. Compare to policy. | |
| * | |
| * Real implementation: parse the Elf64_Nhdr + name + desc, then | |
| * memcmp(desc, expected_env_hash, FUGUE_HASH_LEN). | |
| */ | |
| pr_debug("fugue: [B] provenance note found at section %d\n", i); | |
| /* Stub: assume match */ | |
| return 0; | |
| } | |
| pr_warn("fugue: [B] no .note.fugue_prov section found — rejecting\n"); | |
| return -ENOKEY; | |
| } | |
| /* ========================================================================= | |
| * § D Text segment hashing and RO enforcement | |
| * ========================================================================= */ | |
| static int fugue_hash_text_segment(unsigned long start, unsigned long len, | |
| u8 *out_hash) | |
| { | |
| struct crypto_shash *tfm; | |
| struct shash_desc *desc; | |
| int ret; | |
| tfm = crypto_alloc_shash("sha256", 0, 0); | |
| if (IS_ERR(tfm)) | |
| return PTR_ERR(tfm); | |
| desc = kzalloc(sizeof(*desc) + crypto_shash_descsize(tfm), GFP_KERNEL); | |
| if (!desc) { | |
| crypto_free_shash(tfm); | |
| return -ENOMEM; | |
| } | |
| desc->tfm = tfm; | |
| ret = crypto_shash_init(desc); | |
| if (ret) | |
| goto out; | |
| /* | |
| * Hash in PAGE_SIZE chunks to avoid holding RCU or disabling preemption | |
| * for the entire duration. | |
| */ | |
| while (len > 0) { | |
| size_t chunk = min_t(size_t, len, PAGE_SIZE); | |
| ret = crypto_shash_update(desc, (const u8 *)start, chunk); | |
| if (ret) | |
| goto out; | |
| start += chunk; | |
| len -= chunk; | |
| } | |
| ret = crypto_shash_final(desc, out_hash); | |
| out: | |
| kfree(desc); | |
| crypto_free_shash(tfm); | |
| return ret; | |
| } | |
| static void fugue_enforce_text_ro(unsigned long start, unsigned long len) | |
| { | |
| unsigned long addr = round_down(start, PAGE_SIZE); | |
| unsigned long end = round_up(start + len, PAGE_SIZE); | |
| unsigned long npages = (end - addr) / PAGE_SIZE; | |
| if (set_memory_ro(addr, npages) != 0) | |
| pr_warn("fugue: [D] failed to mark .text RO at %lx+%lu\n", | |
| addr, len); | |
| else | |
| pr_info("fugue: [D] .text marked RO: %lx + %lu pages\n", | |
| addr, npages); | |
| } | |
| /* Periodic text re-verification */ | |
| static void fugue_text_check_fn(struct work_struct *w) | |
| { | |
| int i; | |
| u8 current_hash[FUGUE_HASH_LEN]; | |
| char event[128]; | |
| down_read(&policy_rwsem); | |
| for (i = 0; i < policy_count; i++) { | |
| struct fugue_policy_entry *pol = &policy_db[i]; | |
| if (!pol->text_start || !pol->text_len) | |
| continue; | |
| if (fugue_hash_text_segment(pol->text_start, pol->text_len, | |
| current_hash) != 0) | |
| continue; | |
| if (memcmp(current_hash, pol->text_hash, FUGUE_HASH_LEN) != 0) { | |
| snprintf(event, sizeof(event), | |
| "TEXT INTEGRITY VIOLATION: .text hash mismatch"); | |
| fugue_log_write(pol->mod_name, event); | |
| pr_crit("fugue: [D] .text hash mismatch for module %s! " | |
| "Possible runtime patch.\n", pol->mod_name); | |
| /* | |
| * Policy choice: panic() for high-assurance deployments, | |
| * or just log and alert. Uncomment the next line for panic mode. | |
| */ | |
| /* panic("fugue: LKM text integrity violated"); */ | |
| } | |
| } | |
| up_read(&policy_rwsem); | |
| schedule_delayed_work(&fugue_text_check_work, FUGUE_TEXT_CHECK_HZ); | |
| } | |
| /* ========================================================================= | |
| * § E Hook Registry Ledger | |
| * ========================================================================= */ | |
| static void fugue_hook_ledger_recompute_hmac(void) | |
| { | |
| /* HMAC over the active portion of the ledger */ | |
| fugue_compute_hmac(hook_ledger, | |
| hook_ledger_count * sizeof(struct fugue_hook_record), | |
| fugue_log_hmac_key, sizeof(fugue_log_hmac_key), | |
| hook_ledger_hmac); | |
| } | |
| static bool fugue_hook_ledger_verify_hmac(void) | |
| { | |
| u8 current[FUGUE_HMAC_LEN]; | |
| fugue_compute_hmac(hook_ledger, | |
| hook_ledger_count * sizeof(struct fugue_hook_record), | |
| fugue_log_hmac_key, sizeof(fugue_log_hmac_key), | |
| current); | |
| return memcmp(current, hook_ledger_hmac, FUGUE_HMAC_LEN) == 0; | |
| } | |
| int fugue_hook_register(enum fugue_hook_type type, | |
| const char *mod_name, | |
| const char *hook_name, | |
| unsigned long handler_addr) | |
| { | |
| unsigned long flags; | |
| int slot = -1, i; | |
| char event[128]; | |
| spin_lock_irqsave(&hook_ledger_lock, flags); | |
| if (!fugue_hook_ledger_verify_hmac()) { | |
| pr_crit("fugue: [E] hook ledger HMAC tampered before registration!\n"); | |
| fugue_log_write(mod_name, "HOOK LEDGER HMAC TAMPERED"); | |
| spin_unlock_irqrestore(&hook_ledger_lock, flags); | |
| return -EACCES; | |
| } | |
| /* Find a free slot */ | |
| for (i = 0; i < FUGUE_MAX_HOOKS; i++) { | |
| if (!hook_ledger[i].active) { | |
| slot = i; | |
| break; | |
| } | |
| } | |
| if (slot < 0) { | |
| spin_unlock_irqrestore(&hook_ledger_lock, flags); | |
| return -ENOMEM; | |
| } | |
| hook_ledger[slot].active = true; | |
| hook_ledger[slot].type = type; | |
| hook_ledger[slot].handler_addr = handler_addr; | |
| hook_ledger[slot].registered_ktime = ktime_get_ns(); | |
| strscpy(hook_ledger[slot].mod_name, mod_name, MODULE_NAME_LEN); | |
| strscpy(hook_ledger[slot].hook_name, hook_name, 64); | |
| if (slot >= hook_ledger_count) | |
| hook_ledger_count = slot + 1; | |
| fugue_hook_ledger_recompute_hmac(); | |
| spin_unlock_irqrestore(&hook_ledger_lock, flags); | |
| snprintf(event, sizeof(event), "HOOK REGISTERED type=%d name=%s addr=%lx", | |
| type, hook_name, handler_addr); | |
| fugue_log_write(mod_name, event); | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(fugue_hook_register); | |
| void fugue_hook_deregister(unsigned long handler_addr) | |
| { | |
| unsigned long flags; | |
| int i; | |
| char mod_name[MODULE_NAME_LEN] = "<unknown>"; | |
| char hook_name[64] = "<unknown>"; | |
| char event[128]; | |
| spin_lock_irqsave(&hook_ledger_lock, flags); | |
| if (!fugue_hook_ledger_verify_hmac()) { | |
| pr_crit("fugue: [E] hook ledger HMAC tampered before deregistration!\n"); | |
| fugue_log_write(NULL, "HOOK LEDGER HMAC TAMPERED"); | |
| } | |
| for (i = 0; i < hook_ledger_count; i++) { | |
| if (hook_ledger[i].active && | |
| hook_ledger[i].handler_addr == handler_addr) { | |
| strscpy(mod_name, hook_ledger[i].mod_name, MODULE_NAME_LEN); | |
| strscpy(hook_name, hook_ledger[i].hook_name, 64); | |
| hook_ledger[i].active = false; | |
| break; | |
| } | |
| } | |
| fugue_hook_ledger_recompute_hmac(); | |
| spin_unlock_irqrestore(&hook_ledger_lock, flags); | |
| snprintf(event, sizeof(event), "HOOK DEREGISTERED name=%s addr=%lx", | |
| hook_name, handler_addr); | |
| fugue_log_write(mod_name, event); | |
| } | |
| EXPORT_SYMBOL_GPL(fugue_hook_deregister); | |
| /* kprobe on register_nf_hook / register_kprobe etc. to catch hooks that | |
| * bypass the ledger API. Handler addresses not in our ledger get logged. */ | |
| static int fugue_nf_hook_kprobe_pre(struct kprobe *kp, struct pt_regs *regs) | |
| { | |
| unsigned long flags; | |
| unsigned long handler; | |
| bool found = false; | |
| int i; | |
| /* | |
| * rdi = first argument on x86-64 = pointer to nf_hook_ops. | |
| * Dereference .hook to get the handler address. | |
| * Adjust for other architectures as needed. | |
| */ | |
| #ifdef CONFIG_X86_64 | |
| struct nf_hook_ops *ops = (struct nf_hook_ops *)regs->di; | |
| if (!ops || IS_ERR_OR_NULL(ops)) | |
| return 0; | |
| handler = (unsigned long)ops->hook; | |
| #else | |
| return 0; /* add arch-specific arg extraction */ | |
| #endif | |
| spin_lock_irqsave(&hook_ledger_lock, flags); | |
| for (i = 0; i < hook_ledger_count; i++) { | |
| if (hook_ledger[i].active && | |
| hook_ledger[i].handler_addr == handler) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&hook_ledger_lock, flags); | |
| if (!found) { | |
| pr_warn("fugue: [E] UNLEDGERED netfilter hook at %lx — possible rogue module\n", | |
| handler); | |
| fugue_log_write(NULL, "UNLEDGERED NF HOOK DETECTED"); | |
| } | |
| return 0; | |
| } | |
| static struct kprobe fugue_nf_hook_kp = { | |
| .symbol_name = "nf_register_net_hook", | |
| .pre_handler = fugue_nf_hook_kprobe_pre, | |
| }; | |
| /* ========================================================================= | |
| * § C Symbol Firewall | |
| * ========================================================================= | |
| * | |
| * For each protected module, we install kprobes on every kernel symbol | |
| * NOT in its manifest. The pre_handler checks the call's return address | |
| * to determine if the caller is the protected module; if so, block it. | |
| * | |
| * This is a best-effort implementation. The full implementation would | |
| * instrument every kernel export (kallsyms_on_each_symbol) and filter | |
| * by module-of-caller using __module_address(). | |
| */ | |
| static int fugue_sym_firewall_pre(struct kprobe *kp, struct pt_regs *regs) | |
| { | |
| unsigned long caller; | |
| struct module *caller_mod; | |
| #ifdef CONFIG_X86_64 | |
| caller = regs->ip - 5; /* approximate: back up past the call instruction */ | |
| #elif defined(CONFIG_ARM64) | |
| caller = regs->pc - 4; | |
| #else | |
| return 0; | |
| #endif | |
| caller_mod = __module_address(caller); | |
| if (!caller_mod) | |
| return 0; /* caller is core kernel — not our concern */ | |
| down_read(&policy_rwsem); | |
| { | |
| struct fugue_policy_entry *pol = | |
| fugue_find_policy(caller_mod->name); | |
| if (pol) { | |
| /* | |
| * The caller is a fugue-managed module. Check if this | |
| * symbol is in its manifest. kp->symbol_name is the symbol. | |
| */ | |
| bool permitted = false; | |
| u32 i; | |
| for (i = 0; i < pol->sym_count; i++) { | |
| if (strcmp(pol->syms[i].name, kp->symbol_name) == 0) { | |
| permitted = true; | |
| break; | |
| } | |
| } | |
| if (!permitted) { | |
| char event[128]; | |
| snprintf(event, sizeof(event), | |
| "SYMBOL FIREWALL BLOCK: %s called undeclared %s", | |
| caller_mod->name, kp->symbol_name); | |
| fugue_log_write(caller_mod->name, event); | |
| pr_warn("fugue: [C] %s\n", event); | |
| /* | |
| * To actually block the call, set the instruction pointer | |
| * to a safe return stub. Uncomment and implement as needed: | |
| * regs->ip = (unsigned long)fugue_blocked_call_stub; | |
| */ | |
| } | |
| } | |
| } | |
| up_read(&policy_rwsem); | |
| return 0; | |
| } | |
| /* ========================================================================= | |
| * § G Unload Guard | |
| * ========================================================================= | |
| * | |
| * We kprobe delete_module (the kernel side of rmmod). If the target module | |
| * is guarded, we check for a valid pending token. | |
| */ | |
| static int fugue_delete_module_pre(struct kprobe *kp, struct pt_regs *regs) | |
| { | |
| const char __user *uname_ptr; | |
| char mod_name[MODULE_NAME_LEN]; | |
| struct fugue_policy_entry *pol; | |
| #ifdef CONFIG_X86_64 | |
| uname_ptr = (const char __user *)regs->di; | |
| #elif defined(CONFIG_ARM64) | |
| uname_ptr = (const char __user *)regs->regs[0]; | |
| #else | |
| return 0; | |
| #endif | |
| if (strncpy_from_user(mod_name, uname_ptr, MODULE_NAME_LEN) < 0) | |
| return 0; | |
| mod_name[MODULE_NAME_LEN - 1] = '\0'; | |
| down_read(&policy_rwsem); | |
| pol = fugue_find_policy(mod_name); | |
| if (pol && pol->unload_guarded) { | |
| unsigned long flags; | |
| bool token_ok = false; | |
| int i; | |
| spin_lock_irqsave(&token_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_PENDING_TOKENS; i++) { | |
| if (pending_tokens[i].valid && | |
| strcmp(pending_tokens[i].mod_name, mod_name) == 0) { | |
| u64 now = ktime_get_seconds(); | |
| if (now - pending_tokens[i].issued_ktime_sec | |
| <= FUGUE_TOKEN_TTL_SEC) { | |
| token_ok = true; | |
| pending_tokens[i].valid = false; | |
| } | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&token_lock, flags); | |
| if (!token_ok) { | |
| pr_warn("fugue: [G] rmmod of guarded module %s rejected — " | |
| "no valid TPM token\n", mod_name); | |
| fugue_log_write(mod_name, "UNLOAD REJECTED: no valid token"); | |
| up_read(&policy_rwsem); | |
| /* | |
| * Prevent the delete_module syscall from proceeding by | |
| * overwriting its first argument to an empty string so the | |
| * kernel returns -ENOENT. Cleaner alternatives: return a | |
| * non-zero value from the pre_handler to skip the original | |
| * function (supported in some kernel versions), or use | |
| * LSM security_module_free hook. | |
| */ | |
| #ifdef CONFIG_X86_64 | |
| regs->di = (unsigned long)""; | |
| #endif | |
| return 0; | |
| } | |
| fugue_log_write(mod_name, "UNLOAD PERMITTED: valid token consumed"); | |
| } | |
| up_read(&policy_rwsem); | |
| return 0; | |
| } | |
| static struct kprobe fugue_delete_module_kp = { | |
| .symbol_name = "do_init_module", /* hook delete_module path */ | |
| .pre_handler = fugue_delete_module_pre, | |
| }; | |
| /* ========================================================================= | |
| * § H Inter-Module Call Isolator | |
| * ========================================================================= | |
| * | |
| * fugue_call_check() is called from trampolines installed at each | |
| * exported symbol of a fugue-managed module. It verifies that the | |
| * physical caller's module is on the callee's allowed-callers list. | |
| */ | |
| int fugue_call_check(const char *callee_mod, unsigned long caller_addr) | |
| { | |
| struct module *caller_mod; | |
| struct fugue_policy_entry *pol; | |
| bool permitted = false; | |
| u32 i; | |
| caller_mod = __module_address(caller_addr); | |
| if (!caller_mod) | |
| return 0; /* core kernel — always permit */ | |
| if (strcmp(caller_mod->name, callee_mod) == 0) | |
| return 0; /* self-call — permit */ | |
| down_read(&policy_rwsem); | |
| pol = fugue_find_policy(callee_mod); | |
| if (!pol) { | |
| up_read(&policy_rwsem); | |
| return 0; /* callee not managed — permit */ | |
| } | |
| for (i = 0; i < pol->caller_count; i++) { | |
| if (strcmp(pol->callers[i].mod_name, caller_mod->name) == 0) { | |
| permitted = true; | |
| break; | |
| } | |
| } | |
| up_read(&policy_rwsem); | |
| if (!permitted) { | |
| char event[128]; | |
| snprintf(event, sizeof(event), | |
| "CALL ISOLATION BLOCK: %s → %s denied", | |
| caller_mod->name, callee_mod); | |
| fugue_log_write(callee_mod, event); | |
| pr_warn("fugue: [H] %s\n", event); | |
| return -EACCES; | |
| } | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(fugue_call_check); | |
| /* ========================================================================= | |
| * § I Developer Sandbox | |
| * ========================================================================= | |
| * | |
| * When sandbox_mode is true, the module's kmalloc calls are routed through | |
| * a quota-limited, red-zoned slab cache created here. Network access is | |
| * handled by moving the module's worker threads into a private netns | |
| * (requires the module to cooperate by using fugue_sandbox_sock_create() | |
| * instead of sock_create() directly). | |
| * | |
| * Graduation to production mode requires two Ed25519 signatures (stub). | |
| */ | |
| static int fugue_sandbox_create(struct fugue_policy_entry *pol) | |
| { | |
| unsigned long flags; | |
| int i; | |
| char cache_name[64]; | |
| struct fugue_sandbox_pool *pool = NULL; | |
| spin_lock_irqsave(&sandbox_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (!sandbox_pools[i].cache) { | |
| pool = &sandbox_pools[i]; | |
| break; | |
| } | |
| } | |
| if (!pool) { | |
| spin_unlock_irqrestore(&sandbox_lock, flags); | |
| return -ENOMEM; | |
| } | |
| strscpy(pool->mod_name, pol->mod_name, MODULE_NAME_LEN); | |
| pool->quota = 1024; | |
| atomic_set(&pool->alloc_count, 0); | |
| spin_unlock_irqrestore(&sandbox_lock, flags); | |
| snprintf(cache_name, sizeof(cache_name), "fugue_sb_%s", pol->mod_name); | |
| /* | |
| * SLAB_POISON fills freed objects with 0x6b; SLAB_RED_ZONE adds | |
| * guard bytes around each allocation (CONFIG_SLUB_DEBUG required). | |
| */ | |
| pool->cache = kmem_cache_create(cache_name, 256, 0, | |
| SLAB_POISON | SLAB_CONSISTENCY_CHECKS, | |
| NULL); | |
| if (!pool->cache) | |
| return -ENOMEM; | |
| sandbox_pool_count++; | |
| fugue_log_write(pol->mod_name, "SANDBOX CREATED"); | |
| pr_info("fugue: [I] sandbox slab pool created for %s\n", pol->mod_name); | |
| return 0; | |
| } | |
| static void fugue_sandbox_destroy(const char *mod_name) | |
| { | |
| unsigned long flags; | |
| int i; | |
| spin_lock_irqsave(&sandbox_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (sandbox_pools[i].cache && | |
| strcmp(sandbox_pools[i].mod_name, mod_name) == 0) { | |
| kmem_cache_destroy(sandbox_pools[i].cache); | |
| sandbox_pools[i].cache = NULL; | |
| sandbox_pool_count--; | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&sandbox_lock, flags); | |
| fugue_log_write(mod_name, "SANDBOX DESTROYED"); | |
| } | |
| /* Public API: sandboxed modules call this instead of kmalloc */ | |
| void *fugue_sandbox_alloc(const char *mod_name, size_t size) | |
| { | |
| unsigned long flags; | |
| int i; | |
| struct fugue_sandbox_pool *pool = NULL; | |
| spin_lock_irqsave(&sandbox_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (sandbox_pools[i].cache && | |
| strcmp(sandbox_pools[i].mod_name, mod_name) == 0) { | |
| pool = &sandbox_pools[i]; | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&sandbox_lock, flags); | |
| if (!pool) | |
| return kmalloc(size, GFP_KERNEL); /* not sandboxed — normal alloc */ | |
| if (atomic_inc_return(&pool->alloc_count) > (int)pool->quota) { | |
| atomic_dec(&pool->alloc_count); | |
| pr_warn("fugue: [I] sandbox quota exhausted for %s\n", mod_name); | |
| fugue_log_write(mod_name, "SANDBOX QUOTA EXHAUSTED"); | |
| return NULL; | |
| } | |
| return kmem_cache_alloc(pool->cache, GFP_KERNEL); | |
| } | |
| EXPORT_SYMBOL_GPL(fugue_sandbox_alloc); | |
| void fugue_sandbox_free(const char *mod_name, void *ptr) | |
| { | |
| unsigned long flags; | |
| int i; | |
| struct fugue_sandbox_pool *pool = NULL; | |
| spin_lock_irqsave(&sandbox_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (sandbox_pools[i].cache && | |
| strcmp(sandbox_pools[i].mod_name, mod_name) == 0) { | |
| pool = &sandbox_pools[i]; | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&sandbox_lock, flags); | |
| if (!pool) { | |
| kfree(ptr); | |
| return; | |
| } | |
| atomic_dec(&pool->alloc_count); | |
| kmem_cache_free(pool->cache, ptr); | |
| } | |
| EXPORT_SYMBOL_GPL(fugue_sandbox_free); | |
| /* ========================================================================= | |
| * § J Stack and Heap Canary Injector | |
| * ========================================================================= */ | |
| static int fugue_canary_entry_handler(struct kretprobe_instance *ri, | |
| struct pt_regs *regs) | |
| { | |
| struct fugue_krp_instance_data *data = | |
| (struct fugue_krp_instance_data *)ri->data; | |
| struct fugue_canary_entry *ce = (struct fugue_canary_entry *) | |
| container_of(ri->rp, struct fugue_krp_node, krp)->list.next; /* placeholder */ | |
| (void)ce; | |
| data->saved_canary = FUGUE_CANARY_MAGIC ^ (u64)(uintptr_t)ri; | |
| return 0; | |
| } | |
| static int fugue_canary_ret_handler(struct kretprobe_instance *ri, | |
| struct pt_regs *regs) | |
| { | |
| struct fugue_krp_instance_data *data = | |
| (struct fugue_krp_instance_data *)ri->data; | |
| u64 expected = FUGUE_CANARY_MAGIC ^ (u64)(uintptr_t)ri; | |
| if (data->saved_canary != expected) { | |
| pr_crit("fugue: [J] STACK CANARY VIOLATION in LKM function!\n"); | |
| fugue_log_write(NULL, "STACK CANARY VIOLATION"); | |
| /* panic() in high-assurance mode */ | |
| } | |
| return 0; | |
| } | |
| static int fugue_canary_install(struct fugue_policy_entry *pol, | |
| struct module *mod) | |
| { | |
| /* | |
| * Iterate the module's exported symbols (mod->syms) and install a | |
| * kretprobe on each one. We keep the probes in the per-module | |
| * fugue_canary_entry's probe_list. | |
| * | |
| * For brevity we show the structure; real iteration uses | |
| * mod->syms and mod->num_syms. | |
| */ | |
| struct fugue_canary_entry *ce = NULL; | |
| unsigned long flags; | |
| int i; | |
| spin_lock_irqsave(&canary_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (!canary_table[i].canary) { | |
| ce = &canary_table[i]; | |
| break; | |
| } | |
| } | |
| if (!ce) { | |
| spin_unlock_irqrestore(&canary_lock, flags); | |
| return -ENOMEM; | |
| } | |
| strscpy(ce->mod_name, pol->mod_name, MODULE_NAME_LEN); | |
| get_random_bytes(&ce->canary, sizeof(ce->canary)); | |
| INIT_LIST_HEAD(&ce->probe_list); | |
| canary_table_count++; | |
| spin_unlock_irqrestore(&canary_lock, flags); | |
| pr_info("fugue: [J] canary 0x%llx installed for %s\n", | |
| ce->canary, pol->mod_name); | |
| fugue_log_write(pol->mod_name, "CANARY INSTALLED"); | |
| return 0; | |
| } | |
| static void fugue_canary_remove(const char *mod_name) | |
| { | |
| unsigned long flags; | |
| int i; | |
| spin_lock_irqsave(&canary_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (canary_table[i].canary && | |
| strcmp(canary_table[i].mod_name, mod_name) == 0) { | |
| /* | |
| * Unregister kretprobes in probe_list here in a real | |
| * implementation. | |
| */ | |
| struct fugue_krp_node *node, *tmp; | |
| list_for_each_entry_safe(node, tmp, &canary_table[i].probe_list, | |
| list) { | |
| unregister_kretprobe(&node->krp); | |
| list_del(&node->list); | |
| kfree(node); | |
| } | |
| canary_table[i].canary = 0; | |
| canary_table_count--; | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&canary_lock, flags); | |
| } | |
| /* ========================================================================= | |
| * § Policy helpers | |
| * ========================================================================= */ | |
| static struct fugue_policy_entry *fugue_find_policy(const char *name) | |
| { | |
| int i; | |
| /* Caller must hold policy_rwsem */ | |
| for (i = 0; i < policy_count; i++) { | |
| if (strcmp(policy_db[i].mod_name, name) == 0) | |
| return &policy_db[i]; | |
| } | |
| return NULL; | |
| } | |
| static int fugue_policy_load(void) | |
| { | |
| /* | |
| * In production: open /etc/fugue/policy.bin, verify its Ed25519 | |
| * signature, parse entries into policy_db[], resolve symbol addresses | |
| * via kallsyms_lookup_name() for each sym manifest entry. | |
| * | |
| * Here we demonstrate the structure with one built-in example entry | |
| * so the module compiles and runs for testing. | |
| */ | |
| down_write(&policy_rwsem); | |
| policy_count = 0; | |
| /* Example: protect a hypothetical "sec_dns_interceptor" module */ | |
| strscpy(policy_db[0].mod_name, "sec_dns_interceptor", MODULE_NAME_LEN); | |
| policy_db[0].unload_guarded = true; | |
| policy_db[0].sandbox_mode = false; | |
| policy_db[0].graduated = true; | |
| policy_db[0].sym_count = 2; | |
| strscpy(policy_db[0].syms[0].name, "kmalloc", 64); | |
| strscpy(policy_db[0].syms[1].name, "kfree", 64); | |
| policy_db[0].caller_count = 0; | |
| policy_count = 1; | |
| up_write(&policy_rwsem); | |
| pr_info("fugue: policy loaded (%d entries)\n", policy_count); | |
| return 0; | |
| } | |
| /* ========================================================================= | |
| * § G Token consumption | |
| * ========================================================================= */ | |
| static int fugue_token_consume(const struct fugue_unload_token *tok) | |
| { | |
| unsigned long flags; | |
| u64 now = ktime_get_seconds(); | |
| int i, free_slot = -1; | |
| if (now - tok->timestamp_sec > FUGUE_TOKEN_TTL_SEC) | |
| return -ETIMEDOUT; | |
| /* | |
| * Production: verify tok->tpm_quote against the TPM's AIK and verify | |
| * tok->hmac = HMAC-SHA256(mod_name||timestamp||nonce) with a pre-shared | |
| * key sealed to a known PCR set. | |
| * | |
| * Stub: accept any token with a sane timestamp. | |
| */ | |
| spin_lock_irqsave(&token_lock, flags); | |
| for (i = 0; i < FUGUE_MAX_PENDING_TOKENS; i++) { | |
| if (pending_tokens[i].valid && | |
| strcmp(pending_tokens[i].mod_name, tok->mod_name) == 0) { | |
| /* Replace existing */ | |
| free_slot = i; | |
| break; | |
| } | |
| if (!pending_tokens[i].valid && free_slot < 0) | |
| free_slot = i; | |
| } | |
| if (free_slot < 0) { | |
| spin_unlock_irqrestore(&token_lock, flags); | |
| return -ENOMEM; | |
| } | |
| strscpy(pending_tokens[free_slot].mod_name, tok->mod_name, MODULE_NAME_LEN); | |
| pending_tokens[free_slot].issued_ktime_sec = tok->timestamp_sec; | |
| memcpy(pending_tokens[free_slot].nonce, tok->nonce, 16); | |
| pending_tokens[free_slot].valid = true; | |
| spin_unlock_irqrestore(&token_lock, flags); | |
| fugue_log_write(tok->mod_name, "UNLOAD TOKEN ACCEPTED"); | |
| return 0; | |
| } | |
| /* ========================================================================= | |
| * § /dev/fugue_log character device for log export and ioctl | |
| * ========================================================================= */ | |
| static ssize_t fugue_dev_read(struct file *f, char __user *buf, | |
| size_t count, loff_t *ppos) | |
| { | |
| /* | |
| * Expose the raw log region starting at *ppos. In production, seal | |
| * the log first and only permit reads after sealing. | |
| */ | |
| size_t total = sizeof(struct fugue_log_header) + | |
| fugue_log->capacity * sizeof(struct fugue_log_record); | |
| size_t remaining; | |
| size_t to_copy; | |
| if (*ppos >= total) | |
| return 0; | |
| remaining = total - *ppos; | |
| to_copy = min(count, remaining); | |
| if (copy_to_user(buf, (const char *)fugue_log + *ppos, to_copy)) | |
| return -EFAULT; | |
| *ppos += to_copy; | |
| return to_copy; | |
| } | |
| static long fugue_dev_ioctl(struct file *f, unsigned int cmd, | |
| unsigned long arg) | |
| { | |
| switch (cmd) { | |
| case FUGUE_IOC_UNLOAD_TOKEN: { | |
| struct fugue_unload_token tok; | |
| if (copy_from_user(&tok, (void __user *)arg, sizeof(tok))) | |
| return -EFAULT; | |
| return fugue_token_consume(&tok); | |
| } | |
| case FUGUE_IOC_GRADUATE: { | |
| struct fugue_graduate_token gtok; | |
| struct fugue_policy_entry *pol; | |
| int ret = 0; | |
| if (copy_from_user(>ok, (void __user *)arg, sizeof(gtok))) | |
| return -EFAULT; | |
| /* | |
| * Verify gtok.sig_dev1 with fugue_pubkey and | |
| * gtok.sig_dev2 with fugue_pubkey2. | |
| * Both must be valid Ed25519 signatures over mod_name. | |
| * Stub: assume valid. | |
| */ | |
| (void)fugue_pubkey; | |
| (void)fugue_pubkey2; | |
| down_write(&policy_rwsem); | |
| pol = fugue_find_policy(gtok.mod_name); | |
| if (!pol) { | |
| ret = -ENOENT; | |
| } else if (!pol->sandbox_mode) { | |
| ret = -EINVAL; /* not in sandbox */ | |
| } else { | |
| pol->sandbox_mode = false; | |
| pol->graduated = true; | |
| fugue_sandbox_destroy(gtok.mod_name); | |
| fugue_log_write(gtok.mod_name, "GRADUATED TO PRODUCTION"); | |
| pr_info("fugue: [I] %s graduated to production mode\n", | |
| gtok.mod_name); | |
| } | |
| up_write(&policy_rwsem); | |
| return ret; | |
| } | |
| case FUGUE_IOC_DUMP_HOOKS: { | |
| unsigned int active = 0; | |
| unsigned long flags; | |
| int i; | |
| spin_lock_irqsave(&hook_ledger_lock, flags); | |
| for (i = 0; i < hook_ledger_count; i++) | |
| if (hook_ledger[i].active) | |
| active++; | |
| spin_unlock_irqrestore(&hook_ledger_lock, flags); | |
| return put_user(active, (unsigned int __user *)arg) ? -EFAULT : 0; | |
| } | |
| case FUGUE_IOC_SEAL_LOG: | |
| if (!capable(CAP_SYS_ADMIN)) | |
| return -EPERM; | |
| fugue_log_seal(); | |
| return 0; | |
| default: | |
| return -ENOTTY; | |
| } | |
| } | |
| static const struct file_operations fugue_fops = { | |
| .owner = THIS_MODULE, | |
| .read = fugue_dev_read, | |
| .unlocked_ioctl = fugue_dev_ioctl, | |
| .llseek = generic_file_llseek, | |
| }; | |
| static struct miscdevice fugue_misc = { | |
| .minor = MISC_DYNAMIC_MINOR, | |
| .name = FUGUE_DEV_NAME, | |
| .fops = &fugue_fops, | |
| .mode = 0600, | |
| }; | |
| /* ========================================================================= | |
| * Module load / unload | |
| * ========================================================================= */ | |
| static int __init fugue_init(void) | |
| { | |
| int ret; | |
| int i; | |
| pr_info("fugue: initializing unified LKM security runtime\n"); | |
| /* F: audit log */ | |
| ret = fugue_log_init(); | |
| if (ret) { | |
| pr_err("fugue: audit log init failed: %d\n", ret); | |
| return ret; | |
| } | |
| fugue_log_write(NULL, "FUGUE INIT BEGIN"); | |
| /* Policy */ | |
| ret = fugue_policy_load(); | |
| if (ret) { | |
| pr_err("fugue: policy load failed: %d\n", ret); | |
| goto err_log; | |
| } | |
| /* E: install kprobe to catch unledgered netfilter hooks */ | |
| ret = register_kprobe(&fugue_nf_hook_kp); | |
| if (ret) { | |
| pr_warn("fugue: [E] failed to install nf_register_net_hook kprobe: %d\n", | |
| ret); | |
| /* Non-fatal: continue */ | |
| } | |
| /* G: install kprobe on delete_module path */ | |
| /* Note: the real target should be "do_syscall_64" path for | |
| * delete_module or the syscall entry; adjust symbol_name per kernel. */ | |
| fugue_delete_module_kp.symbol_name = "__x64_sys_delete_module"; | |
| ret = register_kprobe(&fugue_delete_module_kp); | |
| if (ret) { | |
| pr_warn("fugue: [G] failed to install delete_module kprobe: %d\n", | |
| ret); | |
| } | |
| /* D: schedule periodic text integrity checks */ | |
| INIT_DELAYED_WORK(&fugue_text_check_work, fugue_text_check_fn); | |
| schedule_delayed_work(&fugue_text_check_work, FUGUE_TEXT_CHECK_HZ); | |
| /* I: create sandbox pools for modules that need it */ | |
| down_read(&policy_rwsem); | |
| for (i = 0; i < policy_count; i++) { | |
| if (policy_db[i].sandbox_mode) | |
| fugue_sandbox_create(&policy_db[i]); | |
| } | |
| up_read(&policy_rwsem); | |
| /* /dev node */ | |
| ret = misc_register(&fugue_misc); | |
| if (ret) { | |
| pr_err("fugue: misc_register failed: %d\n", ret); | |
| goto err_kprobes; | |
| } | |
| fugue_log_write(NULL, "FUGUE INIT COMPLETE"); | |
| pr_info("fugue: ready. /dev/%s available.\n", FUGUE_DEV_NAME); | |
| return 0; | |
| err_kprobes: | |
| unregister_kprobe(&fugue_nf_hook_kp); | |
| unregister_kprobe(&fugue_delete_module_kp); | |
| cancel_delayed_work_sync(&fugue_text_check_work); | |
| err_log: | |
| if (fugue_log) { | |
| if (fugue_log_phys) | |
| iounmap(fugue_log); | |
| else | |
| vfree(fugue_log); | |
| } | |
| return ret; | |
| } | |
| static void __exit fugue_exit(void) | |
| { | |
| int i; | |
| pr_info("fugue: shutting down\n"); | |
| fugue_log_write(NULL, "FUGUE EXIT BEGIN"); | |
| cancel_delayed_work_sync(&fugue_text_check_work); | |
| misc_deregister(&fugue_misc); | |
| unregister_kprobe(&fugue_nf_hook_kp); | |
| unregister_kprobe(&fugue_delete_module_kp); | |
| /* Remove all canaries */ | |
| down_read(&policy_rwsem); | |
| for (i = 0; i < policy_count; i++) | |
| fugue_canary_remove(policy_db[i].mod_name); | |
| up_read(&policy_rwsem); | |
| /* Destroy sandbox pools */ | |
| for (i = 0; i < FUGUE_MAX_MODULES; i++) { | |
| if (sandbox_pools[i].cache) { | |
| kmem_cache_destroy(sandbox_pools[i].cache); | |
| sandbox_pools[i].cache = NULL; | |
| } | |
| } | |
| fugue_log_write(NULL, "FUGUE EXIT COMPLETE"); | |
| fugue_log_seal(); | |
| if (fugue_log) { | |
| if (fugue_log_phys) | |
| iounmap(fugue_log); | |
| else | |
| vfree(fugue_log); | |
| } | |
| } | |
| module_init(fugue_init); | |
| module_exit(fugue_exit); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment