BLOCKER: protect the private Index and snapshots from local filesystem readers #728

Open
opened 2026-10-02 13:06:33 +00:00 by kayg · 11 comments
Owner

Defensive sec-admin-deploy audit, assigned through #663.

Source: origin/dev c4a61e8cf0.
Also present in origin/job/merge-round-7a. Source review only; no production access or exploit execution.

  1. BLOCKER: private Index files do not have a private filesystem boundary.
    crates/calternal-fs/src/root.rs:1029 creates directories with mode 0755.
    mkdir_internal uses that same function, including .system/backups.
    crates/calternal-db/src/db.rs:46 lets SQLite create the Index without an
    explicit private mode. crates/calternal-db/src/snapshot.rs:57 lets SQLite
    create snapshots without an explicit private mode. The deploy script
    creates the host data directories without a private mode (line 38).
    With a normal 0022 umask and traversable host parents, another local OS
    user can read the Index and snapshots. These contain Security state,
    personal data and Instance secrets. This is a local filesystem issue;
    no remote API bypass is claimed. Protect .system with mode 0700 through
    calternal-fs, including existing directories. Protect the Index, WAL,
    journal and snapshot files with mode 0600. Add mode tests under a 0022
    umask and an upgrade test with existing permissive directories.

Fix acceptance: add the tests described above, run touched-crate gates, and verify no secrets appear in reports.

Defensive sec-admin-deploy audit, assigned through #663. Source: origin/dev c4a61e8cf090170f35b1bed3350d9de20c83ecd5. Also present in origin/job/merge-round-7a. Source review only; no production access or exploit execution. 1. **BLOCKER: private Index files do not have a private filesystem boundary.** `crates/calternal-fs/src/root.rs:1029` creates directories with mode 0755. `mkdir_internal` uses that same function, including `.system/backups`. `crates/calternal-db/src/db.rs:46` lets SQLite create the Index without an explicit private mode. `crates/calternal-db/src/snapshot.rs:57` lets SQLite create snapshots without an explicit private mode. The deploy script creates the host data directories without a private mode (line 38). With a normal 0022 umask and traversable host parents, another local OS user can read the Index and snapshots. These contain Security state, personal data and Instance secrets. This is a local filesystem issue; no remote API bypass is claimed. Protect `.system` with mode 0700 through `calternal-fs`, including existing directories. Protect the Index, WAL, journal and snapshot files with mode 0600. Add mode tests under a 0022 umask and an upgrade test with existing permissive directories. Fix acceptance: add the tests described above, run touched-crate gates, and verify no secrets appear in reports.
Author
Owner

Starting work on #728, #733 and #716 on job/hardening-728 at 2f4482ded066d9c5d9c59130377907f7fd2916c9. Source base origin/dev: c4a61e8cf090170f35b1bed3350d9de20c83ecd5.

Starting work on #728, #733 and #716 on `job/hardening-728` at `2f4482ded066d9c5d9c59130377907f7fd2916c9`. Source base `origin/dev`: `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`.
Author
Owner

Finding for #728: mkdirat and SQLite's requested create modes are subject to the process umask. A mask that removes owner bits can leave a new directory or file at mode 000; ordinary openat2(O_RDONLY) also cannot reopen an existing mode-000 entry for repair. The implementation now pins entries with a directory-relative O_PATH open and applies the exact mode with fchmodat2(AT_EMPTY_PATH), then reopens and checks device/inode identity. The regression covers umask 0777 on new Index and cache entries and repair of an existing mode-000 tree. The filesystem test is running; results will follow.

Finding for #728: `mkdirat` and SQLite's requested create modes are subject to the process umask. A mask that removes owner bits can leave a new directory or file at mode 000; ordinary `openat2(O_RDONLY)` also cannot reopen an existing mode-000 entry for repair. The implementation now pins entries with a directory-relative `O_PATH` open and applies the exact mode with `fchmodat2(AT_EMPTY_PATH)`, then reopens and checks device/inode identity. The regression covers umask 0777 on new Index and cache entries and repair of an existing mode-000 tree. The filesystem test is running; results will follow.
Author
Owner

#728 filesystem regression result:

Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 20s
Running tests/private_permissions.rs (/mnt/hdd/targets/jobs/hardening-728/debug/deps/private_permissions-5b88b3582bb8f61d)
running 1 test
test private_system_permissions_ignore_umask_and_repair_upgrades ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.74s
exit_code=0

This verifies 0700/0600 under umask 0777, idempotent count-only repair, mode-000 upgrade repair, and unchanged 0755 Home content directories. Clippy and full crate gates are pending.

#728 filesystem regression result: ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 20s Running tests/private_permissions.rs (/mnt/hdd/targets/jobs/hardening-728/debug/deps/private_permissions-5b88b3582bb8f61d) running 1 test test private_system_permissions_ignore_umask_and_repair_upgrades ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.74s exit_code=0 ``` This verifies 0700/0600 under umask 0777, idempotent count-only repair, mode-000 upgrade repair, and unchanged 0755 Home content directories. Clippy and full crate gates are pending.
Author
Owner

Final report — hardening-728

Stopped at the requested two-hour limit. The code is committed, but final validation and the requested origin merges are incomplete.

Built:

  • #728: private .system directories and files, startup permission repair with count-only logging, private SQLite Index files/sidecars, cache files, and hardened snapshots. Home content directories keep their existing 0755 mode.
  • #733: Mail errors now log fixed classes and only NO/BAD status words. Calendar and DAV logs were audited; raw Calendar User-Agent text and dynamic storage/queue/compression errors are no longer logged.
  • #716: Notes read/write/full scopes are in the admin denial fixtures; existing combined-scope expectations are unchanged.

Commits on job/hardening-728:

  • 0815aabe72afcd25fa870a021a41bf3de6345b66 — add Notes App Password denial fixtures
  • e1a7ae2cb15ec893c44811ff65f0af2f49a73295 — document the #716 fixture
  • 0a4707258606f74b3c2312ac287890ff454faa28 — enforce private system storage modes
  • 85875c93f257468ba647d255b9638fd6e907826c — protect server Index and snapshot files
  • 61154b748ac75027ca36e37c3681a7864b5ea06e — classify private provider failures
  • 9371ec82ae7c7fdc0b1f1eb937cde05220129e65 — initialize permission repair counts directly

Gate output:

cargo test -p calternal-fs --test private_permissions

Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 20s
Running tests/private_permissions.rs (/mnt/hdd/targets/jobs/hardening-728/debug/deps/private_permissions-5b88b3582bb8f61d)

running 1 test
test private_system_permissions_ignore_umask_and_repair_upgrades ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.74s
exit_code=0

python3 -m unittest discover -s tests/adversarial -p test_admin_classification.py

..............
----------------------------------------------------------------------
Ran 14 tests in 0.975s

OK
exit_code=0

cargo clippy -p calternal-fs --all-targets -- -D warnings first reported this lint; the initializer was fixed and committed:

error: field assignment outside of initializer for an instance created with Default::default()
   --> crates/calternal-fs/src/root.rs:413:9
    |
413 |         counts.directories = u64::from(repaired_root);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
note: consider initializing the variable with `root::PrivatePermissionRepairCounts { directories: u64::from(repaired_root), ..Default::default() }` and removing relevant reassignments
   --> crates/calternal-fs/src/root.rs:412:9
    |
412 |         let mut counts = PrivatePermissionRepairCounts::default();
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#field_reassign_with_default
    = note: `-D clippy::field-reassign-with-default` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::field_reassign_with_default)]`

error: could not compile `calternal-fs` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
exit_code=101

The post-fix clippy rerun was interrupted at the time limit while waiting for Cargo's shared cache lock:

Blocking waiting for file lock on package cache
exit_code=130

Not run: cargo fmt --check; full cargo test/clippy gates for calternal-fs; all server, Mail and Calendar gates; the one-round local adversarial probe; and git fetch origin && git merge origin/dev plus git merge origin/job/merge-round-7a. No push, deploy or dev merge was made. The worktree is clean. cargo clean completed with Removed 1813 files, 860.7MiB total; apps/web/build was absent.

Known gaps: the server SQLite sidecar/snapshot regression and Mail/Calendar log capture regressions are committed but unverified. The production Rust gates, origin merges and adversarial round remain.

Decisions outside DESIGN: use Linux fchmodat2(AT_EMPTY_PATH) on a held O_PATH inode so mode repair remains umask-independent without following a replaced symlink. This syscall requires Linux 6.5 or newer when a permission change is needed; DESIGN §2 does not specify this minimum. Calendar access logs record User-Agent presence rather than its raw client-supplied text.

## Final report — hardening-728 Stopped at the requested two-hour limit. The code is committed, but final validation and the requested origin merges are incomplete. Built: - #728: private `.system` directories and files, startup permission repair with count-only logging, private SQLite Index files/sidecars, cache files, and hardened snapshots. Home content directories keep their existing 0755 mode. - #733: Mail errors now log fixed classes and only `NO`/`BAD` status words. Calendar and DAV logs were audited; raw Calendar User-Agent text and dynamic storage/queue/compression errors are no longer logged. - #716: Notes read/write/full scopes are in the admin denial fixtures; existing combined-scope expectations are unchanged. Commits on `job/hardening-728`: - `0815aabe72afcd25fa870a021a41bf3de6345b66` — add Notes App Password denial fixtures - `e1a7ae2cb15ec893c44811ff65f0af2f49a73295` — document the #716 fixture - `0a4707258606f74b3c2312ac287890ff454faa28` — enforce private system storage modes - `85875c93f257468ba647d255b9638fd6e907826c` — protect server Index and snapshot files - `61154b748ac75027ca36e37c3681a7864b5ea06e` — classify private provider failures - `9371ec82ae7c7fdc0b1f1eb937cde05220129e65` — initialize permission repair counts directly Gate output: `cargo test -p calternal-fs --test private_permissions` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 20s Running tests/private_permissions.rs (/mnt/hdd/targets/jobs/hardening-728/debug/deps/private_permissions-5b88b3582bb8f61d) running 1 test test private_system_permissions_ignore_umask_and_repair_upgrades ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.74s exit_code=0 ``` `python3 -m unittest discover -s tests/adversarial -p test_admin_classification.py` ``` .............. ---------------------------------------------------------------------- Ran 14 tests in 0.975s OK exit_code=0 ``` `cargo clippy -p calternal-fs --all-targets -- -D warnings` first reported this lint; the initializer was fixed and committed: ``` error: field assignment outside of initializer for an instance created with Default::default() --> crates/calternal-fs/src/root.rs:413:9 | 413 | counts.directories = u64::from(repaired_root); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: consider initializing the variable with `root::PrivatePermissionRepairCounts { directories: u64::from(repaired_root), ..Default::default() }` and removing relevant reassignments --> crates/calternal-fs/src/root.rs:412:9 | 412 | let mut counts = PrivatePermissionRepairCounts::default(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#field_reassign_with_default = note: `-D clippy::field-reassign-with-default` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::field_reassign_with_default)]` error: could not compile `calternal-fs` (lib) due to 1 previous error warning: build failed, waiting for other jobs to finish... exit_code=101 ``` The post-fix clippy rerun was interrupted at the time limit while waiting for Cargo's shared cache lock: ``` Blocking waiting for file lock on package cache exit_code=130 ``` Not run: `cargo fmt --check`; full `cargo test`/clippy gates for `calternal-fs`; all server, Mail and Calendar gates; the one-round local adversarial probe; and `git fetch origin && git merge origin/dev` plus `git merge origin/job/merge-round-7a`. No push, deploy or dev merge was made. The worktree is clean. `cargo clean` completed with `Removed 1813 files, 860.7MiB total`; `apps/web/build` was absent. Known gaps: the server SQLite sidecar/snapshot regression and Mail/Calendar log capture regressions are committed but unverified. The production Rust gates, origin merges and adversarial round remain. Decisions outside DESIGN: use Linux `fchmodat2(AT_EMPTY_PATH)` on a held `O_PATH` inode so mode repair remains umask-independent without following a replaced symlink. This syscall requires Linux 6.5 or newer when a permission change is needed; DESIGN §2 does not specify this minimum. Calendar access logs record User-Agent presence rather than its raw client-supplied text.
Author
Owner

Independent read-only review started on job/rev2-hardening-728. Review worktree base: 440e19dce2. Target: 9371ec82ae. I will review private file modes, startup repair and log redaction from source. The LIGHT job forbids builds and tests.

Independent read-only review started on job/rev2-hardening-728. Review worktree base: 440e19dce23040ac8ebaae88f0469b6535b1afcb. Target: 9371ec82ae7c7fdc0b1f1eb937cde05220129e65. I will review private file modes, startup repair and log redaction from source. The LIGHT job forbids builds and tests.
Author
Owner

Independent source review of hardening-728

Result: request changes. Two P1 findings can stop startup. One P2
finding leaves the private directory policy incomplete.

Target: 9371ec82ae7c7fdc0b1f1eb937cde05220129e65.
Review branch: job/rev2-hardening-728.
Initial review base: 440e19dce23040ac8ebaae88f0469b6535b1afcb.

Scope and method

I read CLAUDE.md, CONTEXT.md, DESIGN and issues #728 and #733. I inspected
the requested origin/dev...9371ec82a comparison. That comparison contains
264 files from an earlier merge round. The four final commits, from
0a4707258 through 9371ec82a, contain this job's 17 changed files. I
reviewed those changes against their parent, e1a7ae2cb, and read their
callers. I did not use the author's report as evidence.

All code references below use line numbers at the target SHA. Detailed
evidence and test ideas are in audit-findings.md.

Findings, in severity order

  1. R1, P1 — valid read-only signing assets stop startup.
    crates/calternal-fs/src/root.rs:496 forces every regular file to 0600.
    :1424 treats a valid 0400 key as needing repair. :1438 returns the
    read-only filesystem error, and crates/calternal-server/src/wire.rs:1123
    stops startup. deploy/cloud/README.md:29 requires a read-only secrets
    mount and permits 0400 keys. Keep external signing assets out of the
    writable Index repair. Use read_system_secret_file to validate those
    assets and preserve accepted key modes. Keep Index repair fail-closed.
    Test real startup with that mount and key mode.

  2. R2, P1 — the upgrade repair requires Linux 6.6 without a declared
    minimum.
    crates/calternal-fs/src/root.rs:1430 directly calls
    fchmodat2 with AT_EMPTY_PATH; :1438 has no compatibility path.
    An old 0755 .system cannot be repaired on kernels that support the
    existing openat2 API but lack this operation. The mandatory call at
    crates/calternal-server/src/wire.rs:1123 stops startup. Linux 6.6
    added this operation (Linux manual).
    Add a safe descriptor-based fallback; reuse set_private_mode for
    readable inodes. Preserve inode checks for restricted modes. Test an
    unavailable syscall. If the minimum is intentionally raised, document
    it and add an upgrade check with a clear error.

  3. R3, P2 — per-User Search directory creation bypasses private modes.
    crates/calternal-fs/src/root.rs:581 calls mkdir_p for the internal
    per-User Index paths. The changed :1241 helper selects 0755 and no
    repair. New tantivy, vectors and tantivy-rebuild directories thus
    bypass mkdir_internal. Use mkdir_internal on the validated internal
    path. Test all three methods with umask 0022 and 0777. The private root
    still prevents normal local traversal; no disclosure through that root
    is claimed.

Tracking: all three findings belong to #728. I searched existing issues.
No separate issue was filed because no reported defect is outside this
branch's fix scope.

Other checks

  • The Index is precreated before SQLx opens it. Startup repair covers old
    SQLite sidecars. All four server snapshot call sites apply final mode
    repair. The snapshot remains inside a private directory while SQLite
    creates it. This assessment does not prove runtime behaviour.
  • open_private_entry rejects symlinks and incorrect file types. Its held
    descriptor and inode comparison prevent repair through a replaced
    symlink. It does not grant access to another User's Home.
  • The changed backup route retains its admin guard before snapshot work.
    Calendar and Mail logging changes do not change route authorization or
    User scoping. No new route is added by these four commits.
  • Repair is synchronous and visits the full system tree on each startup.
    It is not on a normal UI read path. No performance measurements were
    permitted. DESIGN §58 at the target concerns agent discovery; the
    performance assessment uses the contract's performance priority.
  • The Mail error classifier does not format provider strings or byte
    buffers. The Calendar access event retains only User-Agent presence.
    The new capture assertions cover NO, BAD, malformed bytes and stored
    error text. No additional log disclosure was found in the changed calls.
  • I searched existing mode, secret-read and log-capture helpers. The fixes
    above can use the existing internal directory and secret-read helpers.
    No separate product-code duplication defect was found.
  • I read git log -p for the changed test files and inline tests. The
    four commits add assertions; they do not weaken existing expectations.
    The mode tests check filesystem results and SQLite sidecars. They call
    the helpers directly, so they do not prove build_live_app wiring.
    The logging tests assert emitted fields and exclude synthetic private
    values. Runtime failure on old code was not tested in this LIGHT job.
  • Changed comments explain confinement and private modes. The statements
    that the whole system tree stays private omit the R3 creation path.
    Update that comment when the path is fixed.

Validation and known gaps

No cargo or web gates were run. The LIGHT instruction forbids builds,
tests, servers and browsers. There is no gate output to quote.
git diff --check produced no output and exited with status 0.

The findings are source-derived. Read-only mount startup and older-kernel
behaviour were not reproduced. No runtime concurrency, performance or
adversarial result is claimed. No product code, deployment or author
worktree was changed. No merge, push or issue close was performed.

Decisions

  • Separate the four job commits from unrelated merge-round changes in the
    requested comparison. Keep all evidence tied to the fixed target SHA.
  • Record branch defects on #728 instead of creating duplicate issues.
  • Follow the LIGHT instruction instead of the general build, merge and
    cleanup workflow. This review created no build output to remove.

UX gaps closed: none; this is a source review with no UI change.
UX gaps left: not assessed; no browser work was permitted.

Review head: 1d8bb1698c8eeb901140ef7706b93fd385a11a9b.
Files: review-hardening-728.md, audit-findings.md.

# Independent source review of hardening-728 **Result: request changes.** Two P1 findings can stop startup. One P2 finding leaves the private directory policy incomplete. Target: `9371ec82ae7c7fdc0b1f1eb937cde05220129e65`. Review branch: `job/rev2-hardening-728`. Initial review base: `440e19dce23040ac8ebaae88f0469b6535b1afcb`. ## Scope and method I read CLAUDE.md, CONTEXT.md, DESIGN and issues #728 and #733. I inspected the requested `origin/dev...9371ec82a` comparison. That comparison contains 264 files from an earlier merge round. The four final commits, from `0a4707258` through `9371ec82a`, contain this job's 17 changed files. I reviewed those changes against their parent, `e1a7ae2cb`, and read their callers. I did not use the author's report as evidence. All code references below use line numbers at the target SHA. Detailed evidence and test ideas are in [audit-findings.md](audit-findings.md). ## Findings, in severity order 1. **R1, P1 — valid read-only signing assets stop startup.** `crates/calternal-fs/src/root.rs:496` forces every regular file to 0600. `:1424` treats a valid 0400 key as needing repair. `:1438` returns the read-only filesystem error, and `crates/calternal-server/src/wire.rs:1123` stops startup. `deploy/cloud/README.md:29` requires a read-only secrets mount and permits 0400 keys. Keep external signing assets out of the writable Index repair. Use `read_system_secret_file` to validate those assets and preserve accepted key modes. Keep Index repair fail-closed. Test real startup with that mount and key mode. 2. **R2, P1 — the upgrade repair requires Linux 6.6 without a declared minimum.** `crates/calternal-fs/src/root.rs:1430` directly calls `fchmodat2` with `AT_EMPTY_PATH`; `:1438` has no compatibility path. An old 0755 `.system` cannot be repaired on kernels that support the existing `openat2` API but lack this operation. The mandatory call at `crates/calternal-server/src/wire.rs:1123` stops startup. Linux 6.6 added this operation ([Linux manual](https://man7.org/linux/man-pages/man2/fchmodat.2.html)). Add a safe descriptor-based fallback; reuse `set_private_mode` for readable inodes. Preserve inode checks for restricted modes. Test an unavailable syscall. If the minimum is intentionally raised, document it and add an upgrade check with a clear error. 3. **R3, P2 — per-User Search directory creation bypasses private modes.** `crates/calternal-fs/src/root.rs:581` calls `mkdir_p` for the internal per-User Index paths. The changed `:1241` helper selects 0755 and no repair. New `tantivy`, `vectors` and `tantivy-rebuild` directories thus bypass `mkdir_internal`. Use `mkdir_internal` on the validated internal path. Test all three methods with umask 0022 and 0777. The private root still prevents normal local traversal; no disclosure through that root is claimed. Tracking: all three findings belong to #728. I searched existing issues. No separate issue was filed because no reported defect is outside this branch's fix scope. ## Other checks - The Index is precreated before SQLx opens it. Startup repair covers old SQLite sidecars. All four server snapshot call sites apply final mode repair. The snapshot remains inside a private directory while SQLite creates it. This assessment does not prove runtime behaviour. - `open_private_entry` rejects symlinks and incorrect file types. Its held descriptor and inode comparison prevent repair through a replaced symlink. It does not grant access to another User's Home. - The changed backup route retains its `admin` guard before snapshot work. Calendar and Mail logging changes do not change route authorization or User scoping. No new route is added by these four commits. - Repair is synchronous and visits the full system tree on each startup. It is not on a normal UI read path. No performance measurements were permitted. DESIGN §58 at the target concerns agent discovery; the performance assessment uses the contract's performance priority. - The Mail error classifier does not format provider strings or byte buffers. The Calendar access event retains only User-Agent presence. The new capture assertions cover NO, BAD, malformed bytes and stored error text. No additional log disclosure was found in the changed calls. - I searched existing mode, secret-read and log-capture helpers. The fixes above can use the existing internal directory and secret-read helpers. No separate product-code duplication defect was found. - I read `git log -p` for the changed test files and inline tests. The four commits add assertions; they do not weaken existing expectations. The mode tests check filesystem results and SQLite sidecars. They call the helpers directly, so they do not prove `build_live_app` wiring. The logging tests assert emitted fields and exclude synthetic private values. Runtime failure on old code was not tested in this LIGHT job. - Changed comments explain confinement and private modes. The statements that the whole system tree stays private omit the R3 creation path. Update that comment when the path is fixed. ## Validation and known gaps No cargo or web gates were run. The LIGHT instruction forbids builds, tests, servers and browsers. There is no gate output to quote. `git diff --check` produced no output and exited with status 0. The findings are source-derived. Read-only mount startup and older-kernel behaviour were not reproduced. No runtime concurrency, performance or adversarial result is claimed. No product code, deployment or author worktree was changed. No merge, push or issue close was performed. ## Decisions - Separate the four job commits from unrelated merge-round changes in the requested comparison. Keep all evidence tied to the fixed target SHA. - Record branch defects on #728 instead of creating duplicate issues. - Follow the LIGHT instruction instead of the general build, merge and cleanup workflow. This review created no build output to remove. UX gaps closed: none; this is a source review with no UI change. UX gaps left: not assessed; no browser work was permitted. Review head: `1d8bb1698c8eeb901140ef7706b93fd385a11a9b`. Files: `review-hardening-728.md`, `audit-findings.md`.
Author
Owner

Review fixes started on job/hardening-728, base/head 9371ec82ae7c7fdc0b1f1eb937cde05220129e65. Read the independent review and its issue comment. R1, R2 and R3 agree with the source: repair reaches the external signing mount, changed modes require fchmodat2, and per-User Search uses mkdir_p. I will fix these with focused regressions before the verification round. Host load at start: 52.51, 48.07, 51.49. Gates will respect the low-load instruction and the current per-crate verification policy. No UI or dependency changes are planned.

Review fixes started on `job/hardening-728`, base/head `9371ec82ae7c7fdc0b1f1eb937cde05220129e65`. Read the independent review and its issue comment. R1, R2 and R3 agree with the source: repair reaches the external signing mount, changed modes require fchmodat2, and per-User Search uses mkdir_p. I will fix these with focused regressions before the verification round. Host load at start: 52.51, 48.07, 51.49. Gates will respect the low-load instruction and the current per-crate verification policy. No UI or dependency changes are planned.
Author
Owner

Review fixes R1/R2/R3 are implemented. R1 excludes only the direct .system/secrets directory from writable data repair; nested directories named secrets are still repaired. The signing loader still reads through read_system_secret_file and rejects group-readable keys. R2 uses fchmod after the inode check, with the live O_PATH descriptor’s procfs link for mode-000 entries. No mutable data pathname is passed to chmod. The syscall regression returns ENOSYS for fchmodat2. R3 routes all three per-User Search directory methods through mkdir_internal; direct mkdir_p calls left in root.rs are Home roots and the public helper. Focused regression builds are running.

Decisions: preserve the deployment’s external signing mount, keep writable Index errors fail-closed, and keep the existing kernel minimum. AT_EMPTY_PATH needs Linux 6.6, correcting the earlier 6.5 note; source: https://man7.org/linux/man-pages/man2/fchmod.2.html. Restricted-mode repair needs procfs, which the server already uses for held directory paths. No dependencies or user-facing features changed.

Review fixes R1/R2/R3 are implemented. R1 excludes only the direct `.system/secrets` directory from writable data repair; nested directories named `secrets` are still repaired. The signing loader still reads through `read_system_secret_file` and rejects group-readable keys. R2 uses `fchmod` after the inode check, with the live O_PATH descriptor’s procfs link for mode-000 entries. No mutable data pathname is passed to chmod. The syscall regression returns ENOSYS for fchmodat2. R3 routes all three per-User Search directory methods through `mkdir_internal`; direct mkdir_p calls left in root.rs are Home roots and the public helper. Focused regression builds are running. Decisions: preserve the deployment’s external signing mount, keep writable Index errors fail-closed, and keep the existing kernel minimum. AT_EMPTY_PATH needs Linux 6.6, correcting the earlier 6.5 note; source: https://man7.org/linux/man-pages/man2/fchmod.2.html. Restricted-mode repair needs procfs, which the server already uses for held directory paths. No dependencies or user-facing features changed.
Author
Owner

R1/R2/R3 fixes are committed separately: 9ec0f437e (R2), 77a1f5f85 (R1), ac94bdb07 (R3). The regression run passes on the fixes. Restoring each old behavior individually makes its regression fail: R2 returns ENOSYS on private-tree repair; R1 repairs the external signing fixture instead of preserving it; R3 returns mode 0755 where 0700 is required. No existing expectation was changed.

git fetch origin and git merge origin/dev completed once, with merge head d41520c4e. The merge changed deployment media-test limits and DESIGN; no conflict occurred. cargo fmt --check produced no output and exited 0. The live read-only signing mount regression is compiling. The requested low-load condition is still not met (50.00, 49.93, 50.89 at the merge checkpoint), so the full per-crate gate round remains pending. The current verification policy defers the full adversarial matrices to the merge round.

Focused outputs:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.19s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.78s
Ran 14 tests in 1.387s

OK
R1/R2/R3 fixes are committed separately: `9ec0f437e` (R2), `77a1f5f85` (R1), `ac94bdb07` (R3). The regression run passes on the fixes. Restoring each old behavior individually makes its regression fail: R2 returns ENOSYS on private-tree repair; R1 repairs the external signing fixture instead of preserving it; R3 returns mode 0755 where 0700 is required. No existing expectation was changed. `git fetch origin` and `git merge origin/dev` completed once, with merge head `d41520c4e`. The merge changed deployment media-test limits and DESIGN; no conflict occurred. `cargo fmt --check` produced no output and exited 0. The live read-only signing mount regression is compiling. The requested low-load condition is still not met (50.00, 49.93, 50.89 at the merge checkpoint), so the full per-crate gate round remains pending. The current verification policy defers the full adversarial matrices to the merge round. Focused outputs: ``` test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.19s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.78s Ran 14 tests in 1.387s OK ```
Author
Owner

The focused SQLite permission integration test failed before its journal assertion: PRAGMA journal_mode=DELETE returned database is locked (SQLite code 5). Db::connect opens a read-only reader pool in addition to the writer. SQLite cannot leave WAL mode while that reader connection remains open. The test now closes its unused reader pool before the journal-mode probe. All existing journal, sidecar and snapshot permission assertions stay unchanged. This changes test setup only; database behavior is unchanged.

The full startup regression passed with a real read-only bind mount in a private mount namespace. The existing rejection regression also passed after startup repair for a group-readable signing key.

The focused SQLite permission integration test failed before its journal assertion: `PRAGMA journal_mode=DELETE` returned `database is locked` (SQLite code 5). `Db::connect` opens a read-only reader pool in addition to the writer. SQLite cannot leave WAL mode while that reader connection remains open. The test now closes its unused reader pool before the journal-mode probe. All existing journal, sidecar and snapshot permission assertions stay unchanged. This changes test setup only; database behavior is unchanged. The full startup regression passed with a real read-only bind mount in a private mount namespace. The existing rejection regression also passed after startup repair for a group-readable signing key.
Author
Owner

Review fixes for #728, with the #733/#716 protections retained. Merged origin/dev once (d41520c4e); no push or deploy.

Built:

  • R1: preserve only the direct .system/secrets external asset directory. Keep nested writable directories under repair. Validate signing key permissions without normalizing unsafe keys.
  • R2: use identity-checked file descriptors for chmod, with the held O_PATH procfs link for mode-000 entries. No fchmodat2 requirement. Reject symlinks and special files.
  • R3: create and repair all per-User Search directory components at 0700, including under umask 0777.
  • Regression coverage: ENOSYS syscall filter, old-mode and mode-000 repair, external asset preservation, per-User Search modes, real read-only bind-mount startup, unsafe signing-key rejection.
  • Fixed the SQLite permission test setup: close its unused reader pool before switching out of WAL. All existing assertions remain unchanged.
  • Removed the invalid-UTF-8 literal warning from the Mail privacy fixture, with the same bytes and assertions.

Files: crates/calternal-fs/src/root.rs, crates/calternal-fs/tests/private_permissions.rs, crates/calternal-fs/tests/legacy_kernel_permissions.rs, crates/calternal-server/src/wire.rs, crates/calternal-server/tests/private_index_permissions.rs, crates/plugins/mail/src/sync.rs, crates/plugins/calendar/src/feeds/publication.rs.

Verification:
The R1, R2 and R3 regressions each fail with the corresponding old behavior (exit 101), and pass with the fixes. R1 fails on the external directory repair count, R2 on ENOSYS, R3 on 0755 instead of 0700. Logs are retained under artifacts/ in this worktree.

Decisions:

  • Exempt only the direct external signing-assets directory, as required by the deployed read-only mount contract. Its existing bounded reader and signer validation remain authoritative.
  • Retain support for openat2-era kernels. Use the held descriptor's procfs link only for restricted inodes; procfs is already required by the server's SQLite and Tantivy adapters. No user path enters the procfs link. The Linux chmod manual dates AT_EMPTY_PATH support to Linux 6.6: Linux chmod manual.
  • Use a private user/mount namespace for the read-only startup regression; the test does not change host mounts.

UX gaps closed / UX gaps left: none; no UI changes.

Known gaps:
The full clippy and per-crate test round is pending. The brief requires low host load before that round; load was over 50 at start and 75.30, 73.04, 67.27 at the final checkpoint (22:34 CEST). Focused regressions ran under the verification-policy exception. Legacy-kernel behavior was tested with an ENOSYS syscall filter, not on an older-kernel VM. No full live adversarial matrices, web tests, screenshots, performance measurement or deployment ran.

For the merge round:
With CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4 TMPDIR=$PWD/target/tmp, run the commands below once. Prove no warnings or test failures.

cargo fmt --check
cargo clippy -p calternal-fs --all-targets -- -D warnings
cargo test -p calternal-fs
cargo clippy -p calternal-server --all-targets -- -D warnings
cargo test -p calternal-server
cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings
cargo test -p calternal-plugin-mail
cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings
cargo test -p calternal-plugin-calendar

Run bash tests/adversarial/run.sh once, time-boxed, on the combined branch. Prove real-server private modes after startup repair, no provider text in Mail error output, redacted Calendar logs, and Notes scope coverage in authz_matrix.py. The latest verification policy assigns full matrices to the merge round.

Head: aad63cfa1178e9a8b5255353583d1af11d07484a. Worktree clean. Module and changed function comments were read again before reporting.

cargo fmt --check: exit 0, no output. git diff --check: exit 0, no output.

Focused verification summary lines (verbatim):

FS modes and legacy syscall:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.19s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.78s

Read-only mount startup:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 167 filtered out; finished in 4.82s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

Unsafe signing-key rejection:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 167 filtered out; finished in 0.01s

SQLite sidecars and snapshot:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s

Mail logging, final warning-free fixture:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 49 filtered out; finished in 0.00s

Calendar logging:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 88 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s

Notes scope classifier:

Ran 14 tests in 1.387s
OK

Expected failures when the corresponding old behavior was restored (verbatim):

r1-before.log:

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.88s

r2-before.log:

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

r3-before.log:

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.81s

Cleanup: cargo clean completed (exit 0). Removed apps/web/build and target/tmp; retained only ignored verification artifacts.

     Removed 10177 files, 8.1GiB total
Review fixes for #728, with the #733/#716 protections retained. Merged `origin/dev` once (`d41520c4e`); no push or deploy. Built: - R1: preserve only the direct `.system/secrets` external asset directory. Keep nested writable directories under repair. Validate signing key permissions without normalizing unsafe keys. - R2: use identity-checked file descriptors for chmod, with the held O_PATH procfs link for mode-000 entries. No fchmodat2 requirement. Reject symlinks and special files. - R3: create and repair all per-User Search directory components at 0700, including under umask 0777. - Regression coverage: ENOSYS syscall filter, old-mode and mode-000 repair, external asset preservation, per-User Search modes, real read-only bind-mount startup, unsafe signing-key rejection. - Fixed the SQLite permission test setup: close its unused reader pool before switching out of WAL. All existing assertions remain unchanged. - Removed the invalid-UTF-8 literal warning from the Mail privacy fixture, with the same bytes and assertions. Files: `crates/calternal-fs/src/root.rs`, `crates/calternal-fs/tests/private_permissions.rs`, `crates/calternal-fs/tests/legacy_kernel_permissions.rs`, `crates/calternal-server/src/wire.rs`, `crates/calternal-server/tests/private_index_permissions.rs`, `crates/plugins/mail/src/sync.rs`, `crates/plugins/calendar/src/feeds/publication.rs`. Verification: The R1, R2 and R3 regressions each fail with the corresponding old behavior (exit 101), and pass with the fixes. R1 fails on the external directory repair count, R2 on ENOSYS, R3 on 0755 instead of 0700. Logs are retained under `artifacts/` in this worktree. Decisions: - Exempt only the direct external signing-assets directory, as required by the deployed read-only mount contract. Its existing bounded reader and signer validation remain authoritative. - Retain support for openat2-era kernels. Use the held descriptor's procfs link only for restricted inodes; procfs is already required by the server's SQLite and Tantivy adapters. No user path enters the procfs link. The Linux chmod manual dates AT_EMPTY_PATH support to Linux 6.6: [Linux chmod manual](https://man7.org/linux/man-pages/man2/fchmod.2.html). - Use a private user/mount namespace for the read-only startup regression; the test does not change host mounts. UX gaps closed / UX gaps left: none; no UI changes. Known gaps: The full clippy and per-crate test round is pending. The brief requires low host load before that round; load was over 50 at start and 75.30, 73.04, 67.27 at the final checkpoint (22:34 CEST). Focused regressions ran under the verification-policy exception. Legacy-kernel behavior was tested with an ENOSYS syscall filter, not on an older-kernel VM. No full live adversarial matrices, web tests, screenshots, performance measurement or deployment ran. For the merge round: With `CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4 TMPDIR=$PWD/target/tmp`, run the commands below once. Prove no warnings or test failures. ```sh cargo fmt --check cargo clippy -p calternal-fs --all-targets -- -D warnings cargo test -p calternal-fs cargo clippy -p calternal-server --all-targets -- -D warnings cargo test -p calternal-server cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings cargo test -p calternal-plugin-mail cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings cargo test -p calternal-plugin-calendar ``` Run `bash tests/adversarial/run.sh` once, time-boxed, on the combined branch. Prove real-server private modes after startup repair, no provider text in Mail error output, redacted Calendar logs, and Notes scope coverage in `authz_matrix.py`. The latest verification policy assigns full matrices to the merge round. Head: `aad63cfa1178e9a8b5255353583d1af11d07484a`. Worktree clean. Module and changed function comments were read again before reporting. `cargo fmt --check`: exit 0, no output. `git diff --check`: exit 0, no output. Focused verification summary lines (verbatim): FS modes and legacy syscall: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.19s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.78s ``` Read-only mount startup: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 167 filtered out; finished in 4.82s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s ``` Unsafe signing-key rejection: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 167 filtered out; finished in 0.01s ``` SQLite sidecars and snapshot: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s ``` Mail logging, final warning-free fixture: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 49 filtered out; finished in 0.00s ``` Calendar logging: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 88 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s ``` Notes scope classifier: ```text Ran 14 tests in 1.387s OK ``` Expected failures when the corresponding old behavior was restored (verbatim): r1-before.log: ```text test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.88s ``` r2-before.log: ```text test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` r3-before.log: ```text test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.81s ``` Cleanup: `cargo clean` completed (exit 0). Removed `apps/web/build` and `target/tmp`; retained only ignored verification artifacts. ```text Removed 10177 files, 8.1GiB total ```
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#728
No description provided.