Skip to content

store: make the database, its WAL and lease files owner-only - #9

Merged
ualtinok merged 1 commit into
masterfrom
plex/store-owner-only-permissions
Jul 31, 2026
Merged

store: make the database, its WAL and lease files owner-only#9
ualtinok merged 1 commit into
masterfrom
plex/store-owner-only-permissions

Conversation

@ualtinok

@ualtinok ualtinok commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

SQLite sets no mode of its own, so open_sqlite left the file mode to the caller's umask and the shipped default was world-readable 0644. Measured on a real deployment: 11 of 11 module stores at 0644, including cortexkit-credentials.

A crate that already decides WAL mode, busy timeout and foreign keys has taken responsibility for how the file behaves on disk. Leaving permissions to callers means the decision is made by the ambient umask, which is to say not made at all. A group-readable store is not a configuration this crate supports anyway: it hands out an exclusive single-writer lease, so an out-of-band reader is already outside the contract.

The exposure, stated honestly. A single-account host has no other human to read these files. What 0644 does expose them to is every process running as this user — every module, every worker, every tool — and anything that copies the tree: a backup, a restore, an install into a shared location, a container bind-mount.

The WAL is the half that gets missed. Recently committed rows live there until a checkpoint, so protecting only the database leaves the newest data readable while the database file itself reads as correct. On the measured host the WALs are routinely larger than their databases — alfonso-core's is 23MB.

Lease files too, hardened in cortexkit-lease where they are created. Their exposure is integrity rather than privacy: the lease carries the persisted epoch that is the single-writer fence token, so a writable lease file lets a stale writer's fence be forged.

Two deliberate choices:

  • Applied on open, not only at creation. Every store already deployed was created at 0644, so a creation-time-only fix protects exactly the installations with no history.
  • A path that is not a regular file is refused, not adjusted. Following a symlink would chmod a file the caller never named — a privilege-escalation primitive wearing a hardening step's clothes. The test asserts the symlink target's mode is unchanged, not merely that an error came back.

Verification

Each assertion is mutation-proved separately: dropping "" from the protected suffixes fails the database assertion, dropping "-wal" fails the WAL assertion, and removing the protect_file call from acquire fails the lease test.

Worth recording that the first version of the WAL test could not fail. It used a first open, where SQLite creates the WAL inheriting the database's already-corrected mode, so dropping -wal from the protected suffixes passed it. The test now reopens a store with a leftover permissive WAL — the state an unclean shutdown leaves behind, and the only one in which a permissive WAL can be waiting at open time.

Full workspace: cargo fmt, cargo clippy --workspace --all-targets -D warnings, and cargo test --workspace all clean. Verified downstream in ck-plexus, which consumes this crate by path: its gate and all 19 conformance fixtures pass, and its own store-permission test fails when the protect_file loop is removed here — a cross-repo mutation proof that the rule reaches consumers.

Found while auditing ck-plexus, whose trigger_events table now retains vendor response bodies at rest. SUBC ruled the rule belongs here rather than in each module; the plexus-local implementation has been deleted in favour of this one.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Force owner-only (0600) permissions for SQLite database, WAL/SHM, and lease files to prevent unintended access by other processes. Applied on open so existing deployments are fixed immediately.

  • Bug Fixes
    • Enforce 0600 on {db, -wal, -shm} after enabling WAL in cortexkit-store via protect_file from cortexkit-lease.
    • Enforce 0600 on lease files to protect the single-writer epoch token.
    • Apply on reopen and creation; ignore missing files; refuse non-regular paths (do not follow symlinks).
    • Added tests covering reopened permissive stores and symlink refusal.

Written for commit 3418285. Summary will update on new commits.

Review in cubic

SQLite sets no mode of its own, so open_sqlite left the file mode to the
caller's umask and the shipped default was world-readable 0644. Measured
on a real deployment: 11 of 11 module stores at 0644, including
cortexkit-credentials.

A crate that already decides WAL mode, busy timeout and foreign keys has
taken responsibility for how the file behaves on disk. Leaving permissions
to callers means the decision is made by the ambient umask, which is to
say not made at all. A group-readable store is not a configuration this
crate supports anyway: it hands out an exclusive single-writer lease, so
an out-of-band reader is already outside the contract.

The exposure, stated honestly rather than as an implied multi-user threat:
a single-account host has no other human to read these files. What 0644
does expose them to is every process running as this user — every module,
every worker, every tool — and anything that copies the tree: a backup, a
restore, an install into a shared location, a container bind-mount.

THE WAL IS THE HALF THAT GETS MISSED. Recently committed rows live there
until a checkpoint, so protecting only the database leaves the newest data
readable while the database file itself reads as correct. On the measured
host the WALs are routinely larger than their databases — alfonso-core's
is 23MB.

Lease files are hardened too, in cortexkit-lease where they are created.
Their exposure is integrity rather than privacy: the lease carries the
persisted epoch that is the single-writer fence token, so a writable lease
file lets a stale writer's fence be forged.

Applied on OPEN rather than only at creation, because every store already
deployed was created at 0644 — a creation-time-only fix protects exactly
the installations with no history. A path that is not a regular file is
refused rather than adjusted: following a symlink would chmod a file the
caller never named, which is a privilege-escalation primitive wearing a
hardening step's clothes.

Each assertion is mutation-proved separately. Worth recording that the
first version of the WAL test could not fail: it used a FIRST open, where
SQLite creates the WAL inheriting the database's already-corrected mode.
Dropping '-wal' from the protected suffixes passed it. The test now
reopens a store with a leftover permissive WAL — the state an unclean
shutdown leaves behind, and the only one where a permissive WAL can be
waiting at open time.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/cortexkit-lease/src/lib.rs">

<violation number="1" location="crates/cortexkit-lease/src/lib.rs:79">
P1: A malicious lease-path symlink can still cause an unintended target to be created or chmodded: the callers open the path before this helper, and the helper stats then chmods it in separate syscalls. Open with no-follow and apply permissions to the same validated file handle before any create side effect.</violation>

<violation number="2" location="crates/cortexkit-lease/src/lib.rs:83">
P1: On Windows, `protect_file` always returns `Ok(())` without changing permissions or rejecting symlinks, so the owner-only database, WAL, and lease guarantee is absent on a supported build target. Implement Windows ACL/handle protection or explicitly scope this contract to Unix.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

));
}
if metadata.permissions().mode() & 0o777 != 0o600 {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A malicious lease-path symlink can still cause an unintended target to be created or chmodded: the callers open the path before this helper, and the helper stats then chmods it in separate syscalls. Open with no-follow and apply permissions to the same validated file handle before any create side effect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/cortexkit-lease/src/lib.rs, line 79:

<comment>A malicious lease-path symlink can still cause an unintended target to be created or chmodded: the callers open the path before this helper, and the helper stats then chmods it in separate syscalls. Open with no-follow and apply permissions to the same validated file handle before any create side effect.</comment>

<file context>
@@ -35,6 +35,55 @@ use std::{
+            ));
+        }
+        if metadata.permissions().mode() & 0o777 != 0o600 {
+            std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
+        }
+    }
</file context>

}
}
#[cfg(not(unix))]
let _ = path;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: On Windows, protect_file always returns Ok(()) without changing permissions or rejecting symlinks, so the owner-only database, WAL, and lease guarantee is absent on a supported build target. Implement Windows ACL/handle protection or explicitly scope this contract to Unix.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/cortexkit-lease/src/lib.rs, line 83:

<comment>On Windows, `protect_file` always returns `Ok(())` without changing permissions or rejecting symlinks, so the owner-only database, WAL, and lease guarantee is absent on a supported build target. Implement Windows ACL/handle protection or explicitly scope this contract to Unix.</comment>

<file context>
@@ -35,6 +35,55 @@ use std::{
+        }
+    }
+    #[cfg(not(unix))]
+    let _ = path;
+    Ok(())
+}
</file context>

@ualtinok
ualtinok merged commit 49bcaa2 into master Jul 31, 2026
8 checks passed
@ualtinok
ualtinok deleted the plex/store-owner-only-permissions branch July 31, 2026 18:25
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