Skip to content

REQ-70 file family: do not route — the gap is a security bypass - #72

Merged
ib823 merged 2 commits into
mainfrom
claude/continue-solution-4gn31y
Aug 18, 2026
Merged

REQ-70 file family: do not route — the gap is a security bypass#72
ib823 merged 2 commits into
mainfrom
claude/continue-solution-4gn31y

Conversation

@ib823

@ib823 ib823 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Investigated the file family and deliberately did not route it. The json precedent held — the 8 riina_builtin_fail_* C functions exist and have never executed — but the reason they must not simply be wired up is different in kind from the correctness divergences json, masa, simpan and net produced.

The finding

builtins::fail does not touch the host filesystem directly. All eight builtins first call gate_read / gate_write / gate_delete, which evaluate the Coq-modeled can_read / can_write predicates against an inode model (owner uid/gid, mode 0644 on first touch) and the AccessContext set by vfs_jadi_pengguna.

gating constructs
builtins/fail.rsgate_read/gate_write/gate_delete 12 call sites
emit.rs, all 8 helpers — gate/can_read/can_write/denied/permission/inode/owner 0

fail_baca is a bare fopen; fail_tulis a bare fwrite.

So adding a fail/vfs arm to builtin_canonical would mean riinac run denies an access that riinac build permits — the compiler becomes the way around a verified security check. That is REQ-27 enforcement parity failing in the direction that matters.

Measured on the interpreter:

uid 1 writes /tmp/owned.txt       -> ok, file contains "alice-data"
uid 2 writes the same 0644 file   -> Runtime Error: permission denied
                                     (verified can_write is false for the
                                     current uid), file unchanged

Prerequisite for routing, recorded in the REQ-70 row

Port the inode model, the uid/gid context, and the two predicates into the emitted runtime — then route fail and vfs together. vfs has no C helpers at all today, so routing it alone fails in the C compiler.

What this PR adds

crates/riinac/tests/file_gate_parity.rs, written as an invariant rather than a "stays unrouted" pin:

a compiled binary must not perform an access the interpreter refuses

  • Today — unrouted, so no binary exists; holds vacuously.
  • After a correct gate port — build succeeds and the binary refuses the write; still passes.
  • If someone routes without porting — the binary overwrites the file and this fails, which is exactly when someone needs stopping.

It therefore never has to be deleted to make progress. That shape is deliberate: my earlier masa exclusion used a "stays unrouted" pin, and #70 had to remove it when it resolved the semantics properly. An invariant does not create that friction.

Honest limitation

The SECURITY REGRESSION branch is reviewed but UNEXECUTED. Reaching it needs a compiled binary that switches uid and then writes, which cannot be built today: emit.rs has no riina_builtin_vfs_* functions, so routing vfs emits C that does not compile.

Two controls were run, and both correctly held vacuously:

  1. routing fail alone — no way to switch uid, so no bypass is expressible;
  2. routing fail + vfs — fails in the C compiler.

The assertion is verified in its vacuous arm and acquires teeth exactly when someone writes the vfs C helpers, which is the moment the bypass becomes shippable. Stated in the test's own docs too, not just here.

Type

  • Bug fix
  • New feature
  • Documentation
  • Example .rii file
  • Formal proof
  • Refactor
  • Test / guard for a known gap

Testing

  • cargo test --all (03_PROTO): 3323/0 (+1)
  • cargo clippy --all-targets -- -D warnings clean
  • audit-docs.sh 0 discrepancies
  • New test added — file_gate_parity.rs

Checklist

  • No external dependencies added
  • No new unsafe
  • No Admitted in Coq proofs (no proof changes)
  • Examples use Bahasa Melayu keywords (no example changes)

Note

Four families have now been routed (json, masa, simpan, net) and every one exposed a defect the "lowers to C" Backend column hid. This is the fifth, and the first where the hidden defect is a security property rather than a wrong answer — which suggests keselamatan (42 security sinks, still unrouted) deserves the same look before anyone routes it.


Generated by Claude Code

claude added 2 commits August 17, 2026 21:50
… bypass

REQ-70 family routing, file family: investigated, and DELIBERATELY NOT ROUTED.

The json precedent held — the 8 riina_builtin_fail_* C functions exist and
have never executed, because builtin_canonical does not route the family. But
the reason they must not simply be wired up is different in kind from the
correctness divergences json, masa, simpan and net produced.

builtins::fail does not touch the host filesystem directly. All eight builtins
first call gate_read/gate_write/gate_delete, which evaluate the Coq-modeled
can_read/can_write predicates against an inode model (owner uid/gid, mode 0644
on first touch) and the AccessContext set by vfs_jadi_pengguna. Twelve gate
call sites in that file.

The emitted C helpers contain ZERO gating constructs. fail_baca is a bare
fopen; fail_tulis a bare fwrite. Measured:

  builtins/fail.rs   gate_read|gate_write|gate_delete  -> 12 call sites
  emit.rs, all 8     gate|can_read|can_write|denied|
                     permission|inode|owner            -> 0 occurrences

So adding a fail/vfs arm to builtin_canonical would mean `riinac run` denies an
access that `riinac build` permits: the compiler becomes the way around a
verified security check. That is REQ-27 enforcement parity failing in the
direction that matters. Demonstrated on the interpreter — uid 1 writes a file,
uid 2 is refused with "permission denied (verified can_write is false for the
current uid)" and the file on disk is unchanged.

Prerequisite for routing, recorded in the REQ-70 row: port the inode model,
the uid/gid context and the two predicates into the emitted runtime, then route
fail and vfs TOGETHER — vfs has no C helpers at all today, so routing it alone
fails in the C compiler.

Adds file_gate_parity.rs, written as an INVARIANT rather than a
"stays unrouted" pin: a compiled binary must not perform an access the
interpreter refuses. It passes today (vacuously — no binary exists), passes
after a correct gate port, and fails only if someone routes the family without
porting. It therefore does not have to be deleted to make progress, unlike a
pin on the current routing state, which is the friction my masa exclusion
created for the session that later resolved it properly in #70.

Honest limitation, also stated in the test: the SECURITY REGRESSION branch is
reviewed but UNEXECUTED. Reaching it needs a compiled binary that switches uid
and writes, which cannot be built today because emit.rs has no
riina_builtin_vfs_* functions. Two controls were run and both correctly held
vacuously: routing `fail` alone (no way to switch uid, so no bypass is
expressible) and routing `fail`+`vfs` (fails in the C compiler). The assertion
acquires teeth exactly when someone writes the vfs C helpers — the moment the
bypass becomes shippable.

Verified: 03_PROTO 3323/0 (+1), clippy clean, audit-docs.sh 0 discrepancies.
…e guard

Regenerated by the pre-push riinac verify --full at 2536f0b.

Mode fast -> full. main carried a fast-mode record (Rust tests + clippy only,
scope line 'NOT a proof-checked verification'); this replaces it with a
full-mode one that re-compiles the primary proof lane: 331 .vo, 0 admits,
0 axioms, 5 capstones kernel-attested, funext the only whitelisted axiom.
Strictly more attested than what it replaces, which is the direction this
file is allowed to move.

Rust tests 3323 (+1, file_gate_parity.rs). TLA+/Alloy read PASS for the same
container-local reason recorded at the #68/#69 chains — the pinned jars are on
disk here; on an unprovisioned clone they return to WARN. Isabelle and F*
still WARN, correctly: both are RETIRED, and per the Wave 0.3 finding a
retired lane should not be demanding tooling at all.
@ib823
ib823 marked this pull request as ready for review August 18, 2026 20:58
@ib823
ib823 merged commit 681bb15 into main Aug 18, 2026
11 checks passed
ib823 pushed a commit that referenced this pull request Aug 18, 2026
Metrics unchanged in substance (3323 tests, 12678 Qed, 0 admits, 0 axioms);
timestamp, commit stamp and the rebuilt WASM asset only. The published count
is correct this round — the fail-closed guard added after the #69 chain (where
the deploy prebuild silently published 2659 for a true 3284) did its job.

dim1_dim9 promotion_ready reads true for the same container-local reason
recorded at the #68/#69 chains: the pinned tla2tools/alloy jars are on disk
here. Top-line verdict unchanged: overall_foundation FAIL.
ib823 pushed a commit that referenced this pull request Aug 18, 2026
…te fail/vfs

REQ-70. The file family was held back because routing it would have let a
compiled binary bypass a Coq-modeled access-control check that riinac run
enforces (12 gate call sites in the interpreter, 0 gating constructs in the C
helpers). The gate is now mirrored, so the family routes.

DESIGN, recorded because the obvious approach is impossible. Emitted C cannot
call into Rust: the pipeline is `cc -o out one.c` with nothing linked, so a
single shared implementation callable from both backends would mean shipping
per-target Rust staticlibs (native/wasm32/android/ios) and rebuilding the
compile pipeline. Instead the COQ MODEL is the single source of truth —
domains/VerifiedFileSystem.v (Inode/Ownership/Permission/is_owner/
get_permission) — with riina-os/src/vfs.rs and the emitted C as two
implementations of it, held together by a differential. Same shape as the masa
civil calendar and the GF128/AES Coq-Rust equivalences.

The C mirrors: the inode table, first-touch ownership at mode 0644 (owner rw,
group/other r), owner > group > other resolution, can_read/can_write as
is_root OR the applicable bit, and gate_delete clearing the mapping so a
re-creator owns the file. Denial exits non-zero with the interpreter's wording
and cannot fall through to the filesystem.

Matched op-for-op, including the negative space: fail_ada and fail_senarai are
UNGATED in the interpreter (an existence check is not an access), so they are
ungated in C too. Gating them would have been a divergence in the opposite
direction — denying what the interpreter allows.

Deliberately NOT the host OS's own permission bits: the gate is RIINA's model
and must deny where the model denies even when the host would allow (the
process typically owns these files), so letting the kernel decide would
silently make the check a no-op.

Routed: the 8 fail_* builtins plus vfs_mula/vfs_jadi_pengguna, which are what
make the gate meaningful. vfs_tulis/vfs_baca/vfs_padam stay interpreter-only —
they operate on the in-memory VirtualFs with quota accounting that has no C
implementation, and stubbing them would claim an enforcement this backend
cannot make.

THE #72 LIMITATION IS NOW CLOSED. file_gate_parity.rs's active SECURITY
REGRESSION arm was previously unexecutable — no vfs C helpers existed to build
the bypass with — and is now negative-controlled: deleting a single riina_gate
call from the emitted fail_tulis makes the compiled binary perform the write
(exit 0) and the test fails with its intended message.

Adds file_differential.rs (7 cases), chosen to separate the resolution arms
rather than to demonstrate the happy path: owner write; non-owner read ALLOWED
at 0644; non-owner write and append DENIED; delete clears ownership so a
different uid may re-create and own; ungated existence check. A C gate that
collapsed owner/group/other by always consulting perm_owner would pass a naive
same-uid test and fail these.

Also updates pkg_build's interpreter-only boundary example from fail_baca (now
compiles) to vfs_baca — a deliberate choice rather than the next arbitrary
one, since the VirtualFs quota has no C implementation and so it should stay
interpreter-only for as long as that holds. The test's own comment asks for
exactly this update.

Verified: 03_PROTO 3330/0 (+7), 05_TOOLING 323/0, clippy clean on both,
audit-docs.sh 0 discrepancies. STDLIB.md regenerated from the compiler:
fail_* and the two setters native-only, the VirtualFs trio still interp-only.
ib823 added a commit that referenced this pull request Aug 20, 2026
… fail/vfs

The file family was held back in #72 because routing it would have let a
compiled binary bypass a Coq-modeled access-control check that riinac run
enforces (12 gate call sites in the interpreter, 0 in the C helpers). The gate
is now mirrored, so the family routes.

Design: emitted C cannot call into Rust — the pipeline is `cc -o out one.c`
with nothing linked — so a single shared implementation would have meant
per-target Rust staticlibs. Instead the Coq model is the single source of
truth (domains/VerifiedFileSystem.v), with riina-os/src/vfs.rs and the emitted
C as two implementations of it, held together by a differential. Same shape as
the masa civil calendar and the GF128/AES Coq-Rust equivalences.

Matched op-for-op including the negative space: fail_ada and fail_senarai are
ungated in the interpreter, so they are ungated in C — gating them would be a
divergence in the opposite direction.

Routed: 8 fail_* plus vfs_mula/vfs_jadi_pengguna. The VirtualFs trio stays
interpreter-only; its quota accounting has no C implementation and stubbing it
would claim an enforcement this backend cannot make.

The #72 limitation is closed: file_gate_parity.rs's active SECURITY REGRESSION
arm was previously unexecutable and is now negative-controlled — deleting one
riina_gate call makes the compiled binary perform the forbidden write and the
test fails.

Adds file_differential.rs (7 cases) chosen to separate the owner/group/other
resolution arms rather than demonstrate the happy path.

Also records the keselamatan inspection: its taint discipline is compile-time
(verified by running riinac check), only 3 of 42 have a runtime property, and
those call the same gate — so this PR covers them too.

3330/0 tests in 03_PROTO, 323/0 in 05_TOOLING, clippy clean, 0 doc
discrepancies.
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.

2 participants