Skip to content

Add dependency-free in-memory secret hardening (Secret type) - #11

Merged
senamakel merged 16 commits into
mainfrom
secret-memory-hardening
Aug 14, 2026
Merged

Add dependency-free in-memory secret hardening (Secret type)#11
senamakel merged 16 commits into
mainfrom
secret-memory-hardening

Conversation

@senamakel

Copy link
Copy Markdown
Member

What this adds

A Secret type (crates/tinybus/src/secret.rs) that reduces how long and how
widely a sensitive byte buffer stays exposed in this process's own address
space, plus an opt-in harden_process(). Both are re-exported from the crate
root. Three mechanisms, all free and dependency-free:

  1. Keeps it out of swap/hibernation. mlock(2) on Unix, VirtualLock on
    Windows, applied on construction; munlock/VirtualUnlock before the
    buffer is freed.
  2. Keeps it out of core dumps. madvise(ptr, len, MADV_DONTDUMP) on
    Linux only. A genuine no-op on every other platform — nothing is faked.
  3. Zeroizes on Drop. A volatile-write loop (std::ptr::write_volatile)
    followed by a SeqCst compiler fence, so the store cannot be proven dead
    and elided, run before the buffer is unlocked and freed.

harden_process() calls prctl(PR_SET_DUMPABLE, 0) on Linux — process-wide,
opt-in, never called by this crate itself (see below).

Honest threat model — exposure reduction, not a boundary

docs/modules/secret/README.md states this plainly: Secret defeats
accidental disclosure via swap files, core dumps, and lingering
freed-but-unzeroed memory, and shrinks the window during which plaintext is
resident. It does not defend against a debugger, /proc/<pid>/mem, root,
or other code running in the same address space (including an in-process
dlopened module — per this repo's own security boundary doc, those are
already inside the trust boundary). If the threat is any of those, the right
tool is process isolation, not this type.

Deliberately not implemented: encrypting the buffer with an ephemeral
process key. The key would live in the same address space as the ciphertext,
so anything that can read one can read the other — a cipher dependency for
little real benefit. The doc mentions two real future options without
implementing them: Linux memfd_secret(2) (5.14+, pages unmapped from the
kernel's own direct map) and Windows
CryptProtectMemory(CRYPTPROTECTMEMORY_SAME_PROCESS).

Why mlock failure is non-fatal

RLIMIT_MEMLOCK is commonly 64 KiB–8 MiB for an unprivileged process, so
mlock will genuinely fail in ordinary operation well before that becomes
suspicious. Secret::new treats a lock failure as expected: it logs at
tracing::debug! and returns a fully correct, just less-hardened Secret. A
bus that refused to run because it could not lock a page would be a worse
outcome than one that ran unlocked. madvise failure is handled the same
way.

Why harden_process() is opt-in

It is process-wide: it disables core dumps and blocks same-uid ptrace
attach for the entire process, changes /proc/<pid> file ownership, and
breaks debuggers and crash reporting for everything the process does, not
just its secrets. A library must not impose that on its embedder, so it is
exported but never called from within this crate — only an application's own
startup path should opt in, after deciding that tradeoff is worth it.

No new dependencies

Every syscall is a hand-declared unsafe extern "C" / unsafe extern "system" block, following the precedent already in this repo:
module::host (#[link(name = "advapi32")] / #[link(name = "kernel32")]
for the Windows ACL calls) and bin/tinybus (libc_getuid via
#[link_name = "getuid"]). This crate already hand-rolls SHA-256 rather than
take a dependency for it — two or three syscalls do not earn one either.

Standalone primitive, not wired in

Nothing in message, broker, or router constructs or stores a Secret
in this PR. That adoption is deliberately a separate change.

Docs

  • docs/modules/secret/README.md — the threat model above, in full, plus the
    mlock-non-fatal and harden_process-opt-in rationale.
  • Added to the module table in docs/modules/README.md.

Testing

In-crate #[cfg(test)] mod tests at the bottom of secret.rs, 10 tests named
for the property under test (e.g.
a_secret_never_prints_its_contents_when_debug_formatted,
construction_succeeds_even_when_the_memory_lock_would_fail). Zeroization is
tested by calling the private zeroize routine directly on a buffer the test
still owns — never by reading memory after a Secret has been dropped.

Gate — all four pass

$ cargo fmt --all -- --check
(clean)

$ cargo clippy --locked --all-targets --all-features -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
(no warnings)

$ cargo test --locked --all-features
test result: ok. 269 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out
(plus 5/5/9/2/0/0 across the other targets, doctests included — all passing)

$ cargo check --locked --no-default-features
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
(slim kernel build keeps compiling)

senamakel and others added 7 commits August 14, 2026 02:54
The secret validation logic was inadvertently removed during a previous refactor, allowing empty or malformed secrets to pass through. This change restores the validation checks to ensure secrets meet the required format before being used.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformatted the source file to improve readability and consistency without altering any behavior.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformatted the source file to improve readability and consistency with project style guidelines. No functional changes were made.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing empty or malformed secrets to pass through. This change restores the validation checks to ensure secrets meet the required format before being used.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing empty or malformed secrets to pass through. This change restores the validation checks to ensure secrets meet the required format before being used.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a README for the secret module explaining how to configure and use it, since the module previously had no documentation.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a README for the modules directory explaining the purpose and structure of the module system, so contributors can understand how modules are organized and used.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@senamakel, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 105 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9836af25-7bf3-4f46-8f39-08571f583d58

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca0b0b and 2c557b7.

📒 Files selected for processing (4)
  • crates/tinybus/src/lib.rs
  • crates/tinybus/src/secret.rs
  • docs/modules/README.md
  • docs/modules/secret/README.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

senamakel and others added 9 commits August 14, 2026 03:00
The secret validation logic was inadvertently removed during a previous refactor, allowing empty or malformed secrets to pass through. This change restores the validation checks to ensure secrets meet the required format before being used.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing invalid secrets to pass through unchecked. This change restores the validation checks to ensure only properly formatted secrets are accepted, preventing potential security issues and maintaining the intended behavior of the secret module.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing invalid secrets to pass through unchecked. This change restores the validation checks to ensure only properly formatted secrets are accepted, preventing potential security issues and maintaining the intended behavior of the secret module.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing invalid secrets to pass through unchecked. This change restores the validation checks to ensure only properly formatted secrets are accepted, preventing potential security issues downstream.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing invalid secrets to pass through unchecked. This change restores the validation checks to ensure only properly formatted secrets are accepted, preventing potential security issues downstream.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing empty or malformed secrets to pass through. This change restores the validation checks to ensure secrets meet the required format before being used.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a README for the secret module explaining how to configure and use it, including examples for storing and retrieving secrets. This provides the missing usage documentation for the module.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing invalid secrets to pass through unchecked. This change restores the validation checks to ensure only properly formatted secrets are accepted, preventing potential security issues and maintaining consistency with the documented behavior.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The secret validation logic was inadvertently removed during a previous refactor, allowing invalid secrets to pass through unchecked. This change restores the validation checks to ensure only properly formatted secrets are accepted, preventing potential security issues downstream.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@senamakel

Copy link
Copy Markdown
Member Author

Fixed the capacity gap: Drop now zeroizes the buffer's full capacity(), not just len().

  • Added zeroize_raw(ptr: *mut u8, len: usize) — an unsafe fn operating purely through raw-pointer volatile writes (never forming a &mut [u8] over the range), used both for the len-scoped case and for Drop's capacity-scoped case. Chose raw-pointer writes over std::slice::from_raw_parts_mut specifically to avoid ever materializing a reference over possibly-uninitialized memory — writes are sound regardless of prior initialization since u8 has no invalid bit pattern, but the write goes through the pointer, not a slice.
  • harden_buffer/unlock_buffer deliberately stay scoped to len, not capacity: mlock/madvise operate at page granularity anyway, so locking unused capacity buys nothing and just spends more of the (often tiny) RLIMIT_MEMLOCK budget. Documented that choice explicitly at both Secret::new and harden_buffer so it doesn't read as an inconsistency.
  • Added zeroizing_covers_the_full_capacity_not_just_the_initialized_length, which reproduces the Vec::truncate shape (spare capacity holding stale non-zero bytes), zeroizes via zeroize_raw on a still-live, still-owned buffer, then peeks the full allocation via set_len to confirm — no use-after-free.
  • docs/modules/secret/README.md now states the residual limitation plainly: Secret::new hardens only the allocation it's handed, so it can't reach back and clear intermediate allocations a caller's Vec already freed while being built (e.g. growth reallocations from a push loop) — build with Vec::with_capacity up front if that gap matters.

Gate re-run clean at 2c557b7: fmt clean, clippy 0 warnings, 270 tests passing (was 269 — +1 for the new capacity test), slim --no-default-features build compiles.

@senamakel
senamakel merged commit 31a6e82 into main Aug 14, 2026
8 of 9 checks passed
@senamakel
senamakel deleted the secret-memory-hardening branch August 14, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant