598 docs/web/hardening.html Hardening & Exploit Mitigations · MayteraOS Docs

Home / Docs / Hardening & Mitigations

Hardening and exploit mitigations

Measured against golden build 1713 (commit 6c0e140), 2026-08-06. Individual figures on this page (binary counts, syscall counts, canary sites) will drift as the kernel and app fleet grow; the methodology and the honest ENFORCED / PARTIAL / PLANNED status of each mitigation is the part that should not.

This page is written to be falsifiable. Every claim below carries the evidence that supports it and a status of ENFORCED, PARTIAL or PLANNED. Where a mitigation is missing, this page says it is missing rather than leaving it out. If you download an image you can re-run most of the checks yourself; the commands are in the last section.

The reason for that framing is specific. This project has repeatedly found its own documentation describing code that did not run: a filesystem module with dozens of declared functions and none implemented, a driver that linked but had no caller, a pointer-validation routine with zero call sites, and a build script named after a job it did not do. A security feature with no caller provides no security. So the standard used here is not "the flag is in the makefile" but "the symbol is linked, something calls it, and it was observed doing work in a running system".

145 / 145shipped app and game binaries are position-independent
0of those have a writable and executable segment
132 / 132pointer-taking syscalls validated at one chokepoint
1,317stack-canary check sites compiled into the kernel

How each claim was checked

Source flags were treated as the weakest form of evidence and never as proof on their own. The order of preference was:

  1. Observed in a running system. The current image was booted twice, on two different virtual CPU models, and the serial console captured. Runtime counters and per-boot random values come from those captures.
  2. Measured on the shipped artifact. The image was mounted read-only and every ELF binary inspected with readelf. Counts on this page are from that scan, not from a source listing.
  3. Proven reachable in the linked binary. Where the question was "does anything call this", the answer came from the call graph of the linked kernel, not from the source.
  4. Negative controls. A check that has never been observed failing is not evidence. Where a build gate or self-test is cited, it is cited because it has been watched going red on a deliberately broken input as well as green on a good one.

Status at a glance

MitigationStatusEvidence, and the gap where there is one
Position-independent executablesENFORCED All 145 ELF binaries shipped under the application and game directories are ET_DYN. A build gate derives the launched set from the launcher sources and fails the build on a non-PIE launched binary.
User image base randomisationPARTIAL Every process load base is drawn per spawn from the system DRBG. Two boots produced 14 launches and 14 distinct 2 MB-aligned bases. Ceiling is about 9 bits.
Stack, heap and mapping randomisationPLANNED Not implemented. The kernel prints this itself at boot rather than leaving it to be inferred.
Kernel base randomisation (KASLR)PLANNED Not implemented. The kernel adopts the firmware identity map, so this is structural work rather than a flag.
W^X for user processesENFORCED The loader re-protects every page after relocation. Runtime counters report zero writable-and-executable pages for every live process, and a synthetic hostile image in the same boot reports a non-zero count, so the counter discriminates. A whole-image gate fails the build on any shipped binary with a writable and executable segment.
Kernel stack canariesENFORCED 1,317 check sites across 211 of 338 kernel objects. A fresh random guard is installed early in boot; it differed across the two captured boots.
Userland stack canariesPARTIAL 145 of 146 application build files enable them, and the guard is seeded before main. The shared C library itself is compiled without them, and no build gate enforces the flag on a new application.
NX / no-executeENFORCED Reported live at boot and required for the W^X counters above to mean anything.
SMEPENFORCED where the CPU has it Enabled by a control-register write that is then read back; the feature is only reported enabled if the read-back confirms it. On a CPU without SMEP the system says so instead of claiming it.
SMAPPLANNED (built, deliberately off) Fully implemented and wired, and deliberately not armed. Turning it on today would fault during normal operation. A lint refuses to build with the switch removed until the remaining work is done, so the ordering is enforced rather than remembered.
Ring 0 / Ring 3 separationENFORCED User processes enter at ring 3 through descriptors built with privilege level 3, and return through SYSRET with the interrupt, trap, direction and alignment-check flags masked on entry.
Syscall pointer validationENFORCED Validation runs at a single dispatcher chokepoint before the handler switch, driven by a machine-checked descriptor table. 132 of 132 pointer-taking syscalls are covered out of 275 total. The lint is a link prerequisite, so a new pointer-taking syscall with no descriptor fails the build.
Filesystem permission enforcementPARTIAL Implemented, self-tested at boot, and inert as shipped: see the session model note below.
Non-root desktop sessionPLANNED The shipped desktop auto-logs in as root. First-boot account creation exists; the default has not been flipped.
Password storageENFORCED PBKDF2-HMAC-SHA256, 50,000 iterations, 16-byte salt drawn from the system DRBG, self-describing records so the iteration count is upgradeable, with rate limiting and escalating lockout on the interactive path.
TLS certificate chain validationENFORCED Real signature verification against the issuer key, reachable from both the 1.3 and 1.2 handshakes, proven by the call graph of the linked kernel. Every failure path aborts the handshake; none warn and continue.
TLS version and cipher floorENFORCED Only TLS 1.3 and 1.2 are reachable. On 1.2 exactly two forward-secret AEAD suites are accepted and anything else is refused. The RFC 8446 downgrade sentinel is checked first, before anything else the server said.
Certificate revocationNOT IMPLEMENTED No CRL and no OCSP consumption.
Release secret gateENFORCED Byte-level, decompressing scanner run against the artifact about to be published. Its self-test is 25 of 25 and includes a negative control showing the older text-based gate passing a compressed artifact full of credentials.
Audit loggingPARTIAL A kernel-side event sink exists and records an actor. It is not append-only and not tamper-evident.
AI capability and consent gatingPARTIAL Real, enforced before tool dispatch, with a genuine consent dialog that denies by default on timeout. It runs inside the same unprivileged process it constrains, so it is a policy layer, not yet a trust boundary.
Prompt-injection screeningPLANNED A keyword matcher exists and self-tests, but nothing calls it, so nothing is screened today.
Read-only kernel text after bootPLANNED Not implemented.
Protected immutable security corePLANNED Design only. No code.

Position independence and address-space randomisation

Every application and game binary the system ships is a static position-independent executable. That was measured on the image rather than assumed from build files: 145 ELF files under the application and game directories, 145 of them ET_DYN, none ET_EXEC.

This is held in place by a build gate rather than by convention. The gate derives the set of binaries the system actually launches by scanning the launcher sources at gate time, so adding an application to the desktop automatically puts it under the gate, and it refuses to report success if that scan comes back empty. A separate shape check encodes exactly the rule the kernel loader enforces, and its self-test proves it goes red on the real historical bad shape, a binary linked without the correct linker script.

The randomisation is modest and this page will not call it "full ASLR". The kernel places each position-independent image at a randomly chosen slot inside a 1 GB window at 2 MB granularity. That is at most 512 slots, about 9 bits, and the slot count shrinks as the image grows. Two boots of the current image produced seven process launches each and fourteen distinct load bases with no repeats. The bytes come from the system HMAC-DRBG, which is also what seeds password salts, rather than from a private mixer.

The stack, the heap and the mapping region are not randomised, and neither is the kernel. The system reports this itself on every boot, in the same log line that reports what is randomised. That was a deliberate decision: an earlier version of this subsystem exposed a whole per-process randomisation interface that had never had a single caller, and that interface was deleted rather than left standing as an implied feature.

W^X

No page in a user process is both writable and executable. The loader maps segments writable, copies and relocates, and only then walks every page of the image and applies the protection derived from the segment flags, clearing write or setting no-execute as appropriate. That ordering is the only one that works given the loader writes through the target mapping.

Three independent forms of evidence agree:

  • The binaries have the right shape. Applications link with a script that emits a read-plus-execute segment and a separate read-write segment, instead of the single read-write-execute segment they historically used.
  • The running system counts it. Each launch reports the number of pages made read-only, the number made no-execute, and the number left writable and executable. The last number is zero for every process observed. In the same boot, a synthetic deliberately hostile image reports a non-zero count and is flagged, which is what makes the zero meaningful.
  • A build gate enforces it. Every shipped ELF under the application and game directories is inspected and the build fails if any has a writable and executable segment.

Known gap, stated because the gate's scope is narrower than "the image". The gate scans the application and game directories. One stale binary elsewhere on the image, left behind by an older build and never launched by anything, is still a non-PIE binary with a writable and executable segment. It is dead weight rather than an active component, but "no writable and executable binary anywhere on the image" would be an overstatement, so it is not made here.

Stack canaries

The kernel and userland have different postures and this page does not merge them.

Kernel. Built with -fstack-protector-strong and a global guard variable, because the kernel has no thread-local storage block for the compiler's default guard location. The instrumentation is real and was counted in the compiled objects rather than inferred from the flag: 1,317 calls to the failure handler across 211 of the 338 kernel objects. A random 64-bit guard is installed early in boot, before any protected frame that outlives the call exists, and the two captured boots produced different guards. There is a build variant that arms a deliberate 16-byte buffer overflow specifically so the canary can be watched catching it; it is never armed in a shipped image.

Userland. 145 of 146 application build files enable the same combination. The guard is seeded before main runs, on a path that the startup code really takes, from the kernel DRBG mixed with the timestamp counter and the randomised image base, with the low byte forced to zero so that an overflow driven through a string copy cannot rewrite the canary with its own correct value.

Two honest limits. First, -fstack-protector-strong instruments functions with arrays or address-taken locals, not every function; in a representative application translation unit that was 6 of 20 functions. Second, and more important: the shared C library is compiled without the stack protector, so library code linked into every application is not canary-protected. There is also no build gate enforcing the flag on application build files, so this is 145 of 146 by convention rather than by construction. Both are the kind of gap that a reader would otherwise find in five minutes.

CPU protection features

SMEP and no-execute are enabled where the processor provides them. The enable path writes the control register and then reads it back, and the feature is only reported as enabled if the read-back shows the bit took. That detail exists because a hypervisor can silently drop the write, and a system that assumed success would pay the cost of the protection while having none of it. On a virtual CPU without the feature the system reports "not supported by this CPU" rather than claiming it.

SMAP is implemented and deliberately switched off. The control-register write, the read-back, the user-copy brackets, the interrupt-entry clearing and a boot-time pre-flight probe that refuses to arm if the kernel's own memory is user-accessible all exist and are wired together. They are held off by a single compile-time switch, because a substantial number of ring-0 accesses to user memory in the tree still bypass the four user-copy primitives, and they sit on paths taken during normal operation. Turning SMAP on today would not harden the system, it would produce one that faults before it reaches a desktop. A lint refuses to build with the switch removed until that ledger is empty, and a stale entry in the ledger also fails the build, so it cannot rot into a list of things that used to be true.

The syscall boundary

This is the strongest single control in the system, and the interesting part is how it was fixed rather than what it does.

An audit found 171 user-supplied pointer arguments spread across the syscall surface and exactly five validated call sites. The response was not to fix 166 handlers. Validation was moved to one chokepoint at the top of the dispatcher, before the handler switch, driven by a descriptor table that says what each syscall's arguments are. Structure sizes in that table are locked to the real structure sizes by compile-time assertions, so a structure cannot grow past its validated length without the build failing.

Measured today: 275 syscalls, 132 of which take a user pointer, and 132 of 132 are covered. Four are validated inside their handler with a written justification each; the rest are covered by the table. The debt ledger for known gaps is empty. The lint that checks this is an order-only prerequisite of the kernel link, so adding a pointer-taking syscall without a descriptor fails the build rather than producing a quiet hole. Rejections are rate-limited and audited rather than silent.

Privilege separation, and an important caveat about it

User processes genuinely run in ring 3. The descriptors are built at privilege level 3, the initial entry pushes those selectors, and the syscall path masks the interrupt, trap, direction and alignment-check flags on entry, the last two because omitting the direction flag had been exploitable.

Per-process user and group identity exists, and it is not decorative: there are roughly forty explicit privilege checks across the syscall layer, plus a filesystem permission layer with 49 call sites whose path canonicalisation is self-tested at boot against directory traversal and alternate spellings of the same path.

And it does not currently gate anything, because the shipped desktop auto-logs in as root. The permission check returns success immediately for the root user, so with every process running as root the layer short-circuits on its first line. This is stated plainly because any permission claim on this page would otherwise be misleading. The mechanism is built and tested; the shipped configuration means it is not yet load-bearing. Interactive first-boot account creation exists, and switching the default is a configuration change rather than new code, but it has not been made and this page will not describe it as shipped.

TLS and certificate validation

The TLS stack is written from scratch in this project. It validates.

Chain signatures are actually verified, not merely parsed. The chain verification path was traced through the call graph of the linked kernel, not read from source: the handshake calls chain verification, which calls hostname matching, validity checking, signature verification and the trust-anchor test, and signature verification dispatches to real RSA PKCS#1, RSA-PSS and ECDSA verify code against the issuer's key. Both the 1.3 and the 1.2 handshake reach it, and the separate proof-of-possession signature, the CertificateVerify in 1.3 and the ServerKeyExchange signature in 1.2, is verified as well.

Failure aborts. Every validation failure path frees the context and returns an error; none warn and continue. A handshake that omits CertificateVerify entirely cannot complete, and a compressed-certificate message is explicitly rejected rather than skipped, which closes a bypass that skipping would have created. There is no runtime switch, config file or command anywhere in the system that turns verification off.

Versions and suites. Only TLS 1.3 and 1.2 are reachable; 1.0, 1.1 and SSLv3 cannot be negotiated. On 1.2 exactly two forward-secret AEAD cipher suites are accepted and everything else, including all static-RSA and CBC suites, is refused. The RFC 8446 downgrade sentinel is checked first, before anything else in the server's hello is acted on.

Trust anchors and the clock. A bundle of public root certificates ships on the image; 150 certificates are present and 125 currently load, the remainder being older SHA-1-signed roots whose signature algorithm the parser does not accept. That fails closed, with fewer anchors rather than more, but it is a real shortfall and is stated as one. Validity dates are checked for every certificate in the chain against the hardware real-time clock. There is no time synchronisation client, so a machine with a badly wrong clock will make correspondingly wrong expiry decisions.

What has been observed going red. A boot-time self-test in every shipped image checks the key schedule against vectors generated by independent tooling, accepts a real signature and rejects a forged one, and detects the downgrade sentinel. Separately, a three-way differential across three distinct kernels against real internet hosts showed that flipping one bit of the server's signature causes the current kernel to abort while a pre-fix kernel completed the fetch. That disagreement is what proves the check is load-bearing rather than decorative. A bad-hostname certificate has also been observed being rejected against a real server.

What has not. There is no test anywhere that a chain signed by an untrusted root is rejected, and none that an expired certificate is rejected. Both properties follow from reading the code and from the call graph, but neither has ever been watched failing, and by this page's own standard that makes them less well established than the ones above.

Not implemented, stated as scope rather than omitted: no certificate revocation checking of any kind, no CRL and no OCSP consumption; no name constraints; no client certificates; no Ed25519 certificates. Certificate policy checking beyond basic constraints, subject alternative names and validity dates is not implemented. Hostname matching covers DNS names and falls back to the common name, which is more permissive than RFC 6125 requires.

Cryptographic primitives

AES, SHA-256, SHA-512, HMAC, ChaCha20, GHASH, MD4, MD5, the RSA and ECDSA verification paths and the DRBG are implemented in this project from their specifications. The Ed25519 verification path is adopted from a public-domain implementation, and is reported as an adoption rather than counted as our own work.

Known-answer tests run at boot in the shipped image, against published vectors, and are reported per algorithm: AES against the FIPS-197 appendix vectors, HMAC against RFC 4231 and RFC 2202, and the hashes and stream cipher against their own published vectors. Each runs as a differential between two independent implementations and a known-answer check, so a fault shared by both implementations is still caught by the vector half. That distinction matters here: a differential where both sides share the same wrong assumption is green and means nothing, which this project has been bitten by before.

The gaps. There is no known-answer test for the ECDSA point arithmetic; it is exercised only implicitly by real handshakes. The DRBG's boot self-test is a liveness check, two samples must differ and neither be all zero, not a NIST SP 800-90A known-answer vector.

Release engineering as a control

Public artifacts are scanned by a byte-level secret gate before publication, and the gate is run against the artifact that is about to be uploaded rather than against a source tree.

The reason it works that way is a real incident. The previous gate was a text search over the source directory. It passed an artifact that contained live credentials, for two independent reasons, either of which alone leaks: the search skipped binary files, so it never read a byte of the disk image, and the artifact was compressed, so a plain-text search finds nothing even if it does read it. There is a third lesson in the same incident: deleting a file from a FAT filesystem does not erase its bytes, so a credential that was deleted before packaging was still recoverable from the artifact. Public images are therefore built on a fresh filesystem and never by removing files from an internal one.

The replacement decompresses, scans bytes rather than text, fails closed if a decompressor is missing, and carries a self-test that is currently 25 of 25 and includes a negative control demonstrating the old gate passing a compressed artifact full of credentials. If the self-test does not pass in full, the gate is treated as broken and nothing is published.

The same philosophy applies more broadly. Several checks are order-only prerequisites of the kernel link, so a violation stops the build rather than producing a warning someone has to notice: the syscall pointer lint, a concurrency lint that fails on new busy-wait loops, the user-copy ledger lint, and the image invariant gate that must pass before a build is allowed to produce an image at all. Where a gate exists, its self-test is expected to demonstrate a red result on a deliberately broken input, because a check that has only ever been green is indistinguishable from a check that cannot fail.

Audit logging and the AI capability layer

A kernel-side security event sink exists, starts at boot, records an actor and a process identifier, and reports its own overruns honestly rather than hiding them. Unprivileged code cannot inject records into the in-kernel ring.

It is not append-only and it is not tamper-evident. The on-disk logs are rewritten whole rather than appended, and there is no hash chain and no message authentication code, so the trail can be rewritten by anything that can write the file. Combined with the root-session note above, that means the audit trail should be read as a diagnostic record, not as evidence. Making it append-only and tamper-evident with an actor identity is planned work, and this page will not describe it as done.

The capability and consent layer for AI tool calls is real code that runs: capability tokens with expiry, use limits and scope constraints, a consent dialog with distinct deny, allow-once, allow-for-session and persist choices, and a timeout that denies by default. Enforcement happens before tool dispatch, and low-risk tools are audited even when they are not gated.

Its structural limit is that it lives in the same unprivileged process it constrains. There is no kernel-side capability check. That makes it a policy layer that stops an AI model from doing something it was not permitted to do, which is the threat it was built for, and not a boundary that would stop a compromised process from lying about its own permissions. Relocating that logic into a protected core is designed and not built.

A keyword matcher for prompt-injection patterns exists and passes its own rule self-test at boot. Nothing calls it, so nothing is currently screened, and this page does not claim prompt-injection protection. Even once it is wired in, keyword matching against a small ruleset is a speed bump against known phrasings and not a defence; the semantic and model-judge layers of the upstream ruleset are not evaluated here.

Not implemented

Listed so that their absence is not mistaken for an oversight:

  • Kernel address-space randomisation, and read-only kernel text after boot.
  • Stack, heap and mapping randomisation for user processes.
  • Stack guard pages. Nothing in the kernel has one.
  • Control-flow integrity, of any kind.
  • Application sandboxing. A user process is confined by ring 3 and the syscall surface, nothing narrower.
  • SMAP, which is built but deliberately not armed.
  • Certificate revocation, name constraints and client certificates.
  • A tamper-evident audit trail.
  • A protected immutable security core.

Check it yourself

Most of the claims on this page are properties of the artifact, which means you can test them on a downloaded image without trusting this page. Mount the root partition read-only and run:

# Every shipped application and game binary should report DYN, not EXEC.
find /mnt/APPS /mnt/GAMES -type f -exec sh -c \
  'head -c4 "$1" | grep -q ELF && readelf -hW "$1" | grep -q "Type:.*DYN" || echo "NOT PIE: $1"' _ {} \;

# No shipped binary should have a segment that is both writable and executable.
find /mnt/APPS /mnt/GAMES -type f -exec sh -c \
  'readelf -lW "$1" 2>/dev/null | awk "/^  LOAD/ && \$7 ~ /W/ && \$7 ~ /E/ {print \"W+X: \" FILENAME}"' _ {} \;

# The stack-smash handler should be linked into application binaries.
strings -a /mnt/APPS/CALC | grep 'stack smashing'

And on a booted system, the serial console reports the posture directly, including the things that are not enabled. The randomisation line names what is randomised and what is not; the SMAP line says whether the brackets are armed or inert; the per-launch protection counters report writable-and-executable page counts. If any of those disagree with this page, the page is wrong and we would like to know.

Scope. This is a hobby operating system. It has not had an external security audit, it carries no formal vulnerability-response commitment beyond the advisories page, and the mitigations described here are the beginning of a hardening effort rather than the end of one. The value of the list above is that it is accurate, not that it is long.