diff --git a/build.zig b/build.zig index ee7c0f8..5a0be99 100644 --- a/build.zig +++ b/build.zig @@ -34,6 +34,8 @@ pub fn build(b: *std.Build) void { const native_fs_application_profile = b.option(bool, "native-fs-application-profile", "Separate bounded Zsh application composition; existing ABI31 owner") orelse false; if (native_fs_application_profile and (!native_fs_owner_stack or release_artifact)) @panic("filesystem application profile requires its private ABI31 owner-stack profile"); options.addOption(bool, "native_fs_application_profile", native_fs_application_profile); + const native_zsh_external = b.option(bool, "native-zsh-external", "Private selected Zsh parent and one immutable helper child") orelse false; + options.addOption(bool, "native_zsh_external", native_zsh_external); const native_zsh_terminfo = b.option(bool, "native-zsh-terminfo", "Private large-Zsh MODE/SIGINT composition with retained Owner namespace custody") orelse false; options.addOption(bool, "native_zsh_terminfo", native_zsh_terminfo); const block_test = b.option([]const u8, "block-test", "Disposable disk bring-up: write or verify"); @@ -240,6 +242,8 @@ pub fn build(b: *std.Build) void { options.addOption(bool, "native_tty_signal_probe", native_tty_signal_probe); if (native_zsh_terminfo and (!native_fs_application_profile or !native_tty_mode_probe or !native_tty_signal_probe)) @panic("Zsh terminfo requires its private large-application MODE and exact-generation SIGINT closure"); + if (native_zsh_external and (!native_zsh_terminfo or !native_process_copy or !native_process_exec or !native_copy_signals or !native_signal_actions or !native_signal_public or !native_signal_suspend or !native_parent_identity)) + @panic("Zsh external requires its selected terminfo, copy/exec and generation-safe child signal closure"); options.addOption(bool, "native_exec_copy", negativeOption(b, release_artifact, af09_diagnostic, "native-exec-copy", "Copy an unpublished replacement for rejection testing")); options.addOption(bool, "native_exec_early", negativeOption(b, release_artifact, af09_diagnostic, "native-exec-early", "Publish replacement before owner confirmation")); diff --git a/docs/NATIVE-C.md b/docs/NATIVE-C.md index 643663f..fad71f1 100644 --- a/docs/NATIVE-C.md +++ b/docs/NATIVE-C.md @@ -242,6 +242,30 @@ and compact hashes are in [results](evidence/af07-owner-native-2026-10-23/README No new VM before fresh one-sequence disposition. A successful diagnostic without recurrence does not resolve the panic or authorize resumed affected acceptance. +## Selected external-Zsh Owner capacity + +The [issue143 owner decision](https://github.com/gonz0w/Aether/issues/143#issuecomment-6591806304) +selects73,728 Owner RX bytes and a12,288-byte mapped stack (12,280 usable) +only with the existing `native-zsh-external` closure. AENFS006/AENMX011 bytes do +not select this allowance: the kernel boot gate and loader explicitly choose +`parseExternalApplication`; old `parseApplication`, owner-stack and other +profiles retain their original code, stack and envelope limits. + +The selected stack grows downward to `[0x7ff000,0x802000)`, with three zeroed +RW/NX pages and absent guards at0x7fe000 and0x802000. ABI31, initial +RSP0x802000-8 and the8KiB data region at0x900000 stay exact. Runtime and +workspace remain4,096 bytes each. Maximum mapping is18 RX+3 stack+2 data pages, +23 of the existing128 Owner pages; total application backing stays11,520 pages. +This adds no allocator, syscall, namespace grant or peer mapping authority. + +`scripts/owner_stack.py` accepts an explicit `stack_bytes=12288` for this fit; +its4KiB default,8KiB selection and dynamic/recursive/unknown-edge rejection +remain. Run the pure inspector and fixture-source controls with +`python3 -B -m unittest discover -s tests -p test_zsh_owner_capacity.py -v`. +The same test file supplies actual parser/boot/mapping body fixtures for bounded +Zig execution recorded separately. These host checks do not execute page tables +or qualify a guest. Fresh matched fit and separate review still precede an image. + ## Guarded complete-owner candidate — AENFS005 / ABI31 The maintainer approved a separate8KiB guarded stack and measurement-led code @@ -4577,3 +4601,47 @@ The syscall scaffold executes the actual write, query and reset bodies over host storage. It does not establish hardware protection, linked static growth or a native reader result. Those remain matched kernel/artifact gates. Routine reviewed checks follow the [standing execution direction](policy/execution.md#current-milestone-execution-direction-2026-11-01). + + +## Selected Zsh parent/helper kernel composition + +Issue143 adds default-off `-Dnative-zsh-external=true`, requiring the existing +terminfo, copy/exec, copied signal actions, public actions, suspension and parent +identity selections. The exact Owner/A001 launch admits only parent1/C002. +Cooperative freeze accepts that live bound parent or its exact child2; only the +Owner may copy parent1 into unadmitted child2. The child receives a fresh native +generation and clock binding with retained namespace provenance only, process +endpoints and explicitly null TTY signal ownership. Owner generation0 remains +valid for the parent. Existing selected startup omits namespace/AENAP export. + +Only child2 can replace its image with the Owner-held C003 helper. Its current +parent relationship, child clock and namespace provenance must still match; +exec preserves native generation/binding and the existing receipt/publication +transaction. Parent exec and other copy targets remain denied. Existing +SIGCHLD17 validates the real parent generation. Ordinary profiles retain their +limits; this external profile uses only the +[approved Owner capacity](#selected-external-zsh-owner-capacity). See the selected +[parent/helper contract](../userland/docs/ZSH-FILESYSTEM.md#selected-parenthelper-extension). + +The fixed issue143 target trial passed in34.169s: actual Zsh launched the held +helper, collected status7, printed7 and returned to its prompt before normal +parent/engine exit and export. The Owner completed33/96 requests with mode +cleanup0; all21 cleanup phases and three reclamations completed, with halt +at2884/6000ticks. Screen/PTY and once-only staged input were observed; all +original host holders and clients were reaped with absent groups and output EOF. +[PR144](https://github.com/gonz0w/Aether/pull/144) records the source delivery. +This is the approved fixed-workload checkpoint, with full-host coverage still +open in [issue141](https://github.com/gonz0w/Aether/issues/141). General resource +peaks, arbitrary filesystem execution, child input/job control, login, persistent +shell writes/reboot readback, packages and R1 remain separate gates. + +Run focused public source checks from the repository root: + +```sh +python3 -B -m unittest discover -s tests -p test_native_zsh_external.py -v +``` + +The actual-body fixture uses real Binding/Pending and copy/exec/freeze/notification +bodies over host storage; MMU/device/scheduler installation is substituted. Its +private qualified compiler entry and cleanup records remain separate from these +stdlib-only public checks. No private runner API is required by the public test. diff --git a/docs/policy/execution.md b/docs/policy/execution.md index 400b818..69f2e28 100644 --- a/docs/policy/execution.md +++ b/docs/policy/execution.md @@ -167,6 +167,57 @@ No extra guest, unknown-effect retry or cleanup authority follows. External commands, login, persistent write/reboot/readback, packages and R1 retain their own gates. The earlier design and fixed diagnostic-fit decisions remain effective. +## Selected Zsh parent/helper composition —2026-11-02 + +Cam directly approved +[the scoped parent/helper design](https://github.com/gonz0w/Aether/issues/142#issuecomment-6589424662): +“Approve scoped parent/helper design (Recommended)”. Reviewed proposal03 in +issue142 selects one real Zsh parent and one immutable helper child with the +actual internal pipe, existing copy/exec/SIGCHLD, one context per process and +Rust Owner namespace custody. [Issue143](https://github.com/gonz0w/Aether/issues/143) +owns implementation; the [local consumer contract](../../userland/docs/ZSH-FILESYSTEM.md#selected-parenthelper-extension) +records the selected ownership and compatibility requirements. + +Child bindings are local to its real native slot/generation. MODE/private restore +and SIGINT stay parent-only; child SIGINT ownership is explicitly null. Only +READ/WRITE proxy descriptions are delegated, with an exclusive read lease and +truthful unsupported canonical/ISIG child input. Use complete336-byte selected +snapshots, preserve old304-byte profiles, and retain actual wait status through +known delivery/collection, reclaim and parent read-lease restoration. Unknown +effects contain without replay. No namespace grant, new syscall/capability kind, +global process table or numeric limit increase is selected. + +This approves scoped implementation under the standing milestone direction. +Correct and measure the known oversized external frame before any image; fresh +actual integrated fits and applicable verification/artifact/media/cleanup gates +remain. No image/run result, general utility or filesystem-exec installation, +child job control/input, login, persistence, package or R1 acceptance is claimed. +Issue137's diagnostic-fit/full-host exceptions remain issue-only; issue141 retains +its failed coverage and unverified cleanup. + +## Selected issue143 capacity and verification decision —2026-11-02 + +Owner direction,2026-11-02: Cam directly approved the +[scoped issue143 plan](https://github.com/gonz0w/Aether/issues/143#issuecomment-6591806304), including the reviewed proposal in comment6591273627. +Only native-zsh-external/native_zsh_process may use73,728 bytes of Owner RX +code and a12,288-byte mapped stack (12,280 usable), within unchanged128-page +Owner backing. Preserve ABI31/RSP0x802000-8, Runtime/workspace4,096 each, +RW/NX stack, absent guards and old-profile code/stack/parser/boot limits. +Fresh matched selected fit, boundary/guard tests and separate artifact/media +review precede an image/trial. The one fixed helper/status7/parent-prompt/clean +exit trial may establish remaining scoped workload fit despite unknown complete +parser/handler/heap/copied-peak proof. No general peak qualification follows. +Focused contract checks, separate review, required protected CI and successful +exact trial may qualify this issue143 checkpoint while issue141's full-root +integration gate stays open. This is an explicit issue143 policy exception, +not a full-host pass or transfer of issue137's exceptions. Preserve unknown +history/quarantines. Other app/time/request/mutation/capture/no-network, +disposable-media, finite-attempt and known-cleanup limits remain binding. +Login, persistence/reboot, packages, assembled-system and R1 gates remain separate. +Routine implementation, builds, reviewed bounded tests and delivery proceed +under standing approval; this decision grants no new syscall/capability kind, +arbitrary executable authority or unknown-effect retry. + ## Ordinary-app namespace and clock contract —2026-10-28 The owner approved the separately reviewed [issue98 proposal02](https://github.com/gonz0w/Aether/issues/98#issuecomment-6477628920) diff --git a/docs/workflow.json b/docs/workflow.json index e720256..e692845 100644 --- a/docs/workflow.json +++ b/docs/workflow.json @@ -2665,6 +2665,16 @@ "invariants": "One retained Owner namespace Client/epoch; exact app generation; readonly closed336/AEFP0004; actual held directory, no fake stat; AENAP002 process-only transfer and positive large/MODE/SIGINT closure. Existing resource/default/cleanup limits; old profiles preserved. Application boot-module bound derives from existing AENFS006 component caps; ordinary/large defaults unchanged. Fixed Screen4 private FIFO and original live holder association survive closed diagnostic pipes.", "oracle": "Actual realproducer epoch/inplace constructor and semantic omissions; exact libc/raw errno/mask/publication; compiled app/Owner origins, derived-source pins, current loader/TLS/layout/stack; known child cleanup and real hostVT100 input/render/exit. Actual extracted boot guard admits selected9,081,744/exactcap and rejects one-over/u64max before dispatch; compiling old/omitted gate controls reject. Source-only public checks are distinct from the private actual compiler run.", "gaps": "No current image/guest or complete Zsh startup/parser/handler stack/peakheap/whole-session request demand. Expected33requests and capacity are models, not native evidence; externalexecute still overbudget. Unknown historical interrupted hostattempt remains quarantined." + }, + { + "id": "zsh-external-parent-helper", + "reference": "userland/docs/ZSH-FILESYSTEM.md", + "invariants": "Selected one-parent/one-helper copy/exec/wait with full descriptor snapshots, Owner namespace custody, child-local binding, parent-only MODE/SIGINT, shared FILE lifetime, known wait delivery before reclaim/read-lease restoration. External Owner RX73728/stack12288 within128 backing; old profiles and other execution bounds stay exact.", + "command": "python3 -B -m unittest discover -s tests -p test_native_zsh_external.py -v; python3 -B -m unittest discover -s tests -p test_zsh_owner_capacity.py -v; actual changed parser/boot/mapping bodies and C/Rust controls in the owning qualified private recipes; lead-frozen selected target/Owner fit, reviewed image/media and fixed native helper/status/prompt workload.", + "oracle": "Real helper ELF executes and exits7; Zsh observes actual status7 and returns to its prompt; normal final app/engine exit, mode restoration, export and three known reclamations. Qualified issue143 fixed trial:33/96 requests,21 cleanup phases, halt2884/6000ticks, known host cleanup. Malformed/stale/unknown effects contain without replay.", + "gaps": "Approved comment6591806304 limits acceptance to the fixed helper/status/prompt workload and selected Owner capacity. Full-root141 stays open. No full-host/general peak, child input/job control, arbitrary filesystem exec, login, persistent shell write/reboot readback, packages or R1 qualification. Private recipes are not a standalone distribution.", + "owner": "Aether kernel, userland and sys-init", + "budget_seconds": 300 } ] } diff --git a/kernel/arch/x86_64/native_c.zig b/kernel/arch/x86_64/native_c.zig index 9ae3a55..a0cfdac 100644 --- a/kernel/arch/x86_64/native_c.zig +++ b/kernel/arch/x86_64/native_c.zig @@ -942,7 +942,7 @@ fn select() *const Frame { pub fn start(pbase: u64, vbase: u64, envelope: []const u8, hhdm: u64, map: *const @import("../../limine.zig").MemoryResponse) noreturn { // Reject the selected complete composition before reservation or device setup. if (combined_application_profile) { - _ = FsSession.parseApplication(envelope) catch @panic("invalid filesystem application envelope"); + _ = (if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) FsSession.parseExternalApplication(envelope) else FsSession.parseApplication(envelope)) catch @panic("invalid filesystem application envelope"); } require(envelope.len > 40); if (mktemp_reader) require(std.mem.eql(u8, envelope[0..8], "AENBD002")); @@ -2193,13 +2193,19 @@ fn supervisedEnvelopeValid(envelope: []const u8) bool { return len > 0 and len <= service_bytes.len and supervisor_len > 0 and supervisor_len <= limit and envelope.len == 48 + @as(usize, len) + supervisor_len; } fn mapOwnerStack(s: *State) void { - const pages: usize = if (combined_owner_stack) 2 else 1; + const external = @hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external; + if (external) require(combined_application_profile and combined_owner_stack); + const pages: usize = if (external) FsRoles.zsh_external_owner_stack_pages else if (combined_owner_stack) 2 else 1; for (0..pages) |i| { if (combined_owner_stack) { require(combined_owner_data and s == &states[supervisorSlot()] and s.count < s.backing.len); @memset(&s.backing[s.count], 0); } - addPage(s, 0x800000 + i * 4096, 7 | nx); + if (external) { + addPage(s, FsRoles.zsh_external_owner_stack_base + i * 4096, 7 | nx); + } else { + addPage(s, 0x800000 + i * 4096, 7 | nx); + } } } fn mapOwnerData(s: *State) void { @@ -2220,21 +2226,24 @@ fn ownerDataStartup(frame: *Frame) void { } fn auditOwnerStack(s: *State, code_pages: usize) void { if (!combined_owner_stack) return; - require(s == &states[supervisorSlot()] and s.count == code_pages + 4); - for (0..2) |i| { + const external = @hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external; + const pages: usize = if (external) FsRoles.zsh_external_owner_stack_pages else 2; + const base: usize = if (external) FsRoles.zsh_external_owner_stack_base else 0x800000; + require(s == &states[supervisorSlot()] and s.count == code_pages + pages + 2); + for (0..pages) |i| { const page = s.pages[code_pages + i]; - require(page.address == 0x800000 + i * 4096 and page.flags == 7 | nx); + require(page.address == base + i * 4096 and page.flags == 7 | nx); for (s.backing[code_pages + i]) |value| require(value == 0); } // byte() checks actual mapping membership; no extra authority or mapping. - require(byte(s, 0x7ff000, false) == null and byte(s, 0x802000, false) == null); + require(byte(s, base - 4096, false) == null and byte(s, 0x802000, false) == null); require(s.context().frame.rdi == 31 and s.context().frame.rsp == 0x802000 - 8); } fn startSupervised(envelope: []const u8) noreturn { require(envelope.len >= 48); var supervisor_code: []const u8 = undefined; if (combined_profile) { - const bundle = (if (combined_application_profile) FsSession.parseApplication(envelope) else if (combined_owner_stack) FsSession.parseOwnerStack(envelope) else if (combined_owner_data) FsSession.parseOwnerData(envelope) else FsSession.parseProfile(envelope, combined_init)) catch @panic("invalid filesystem SESSION envelope"); + const bundle = (if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) FsSession.parseExternalApplication(envelope) else if (combined_application_profile) FsSession.parseApplication(envelope) else if (combined_owner_stack) FsSession.parseOwnerStack(envelope) else if (combined_owner_data) FsSession.parseOwnerData(envelope) else FsSession.parseProfile(envelope, combined_init)) catch @panic("invalid filesystem SESSION envelope"); engine_len = bundle.engine.len; @memcpy(engine_bytes[0..engine_len], bundle.engine); engine_image = parseStartup(engine_bytes[0..engine_len]) catch @panic("invalid filesystem engine ELF"); @@ -2305,7 +2314,8 @@ fn startSupervised(envelope: []const u8) noreturn { require(page.address == 0x400000 + i * 4096 and page.flags == 5); for (&states) |*mapping| require(backingAliasEntry(mapping, s, i) == backingFrame(s, i) | 1 | nx); } - require(s.pages[code_pages].address == 0x800000 and s.pages[code_pages].flags == 7 | nx); + const stack_base: usize = if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) FsRoles.zsh_external_owner_stack_base else 0x800000; + require(s.pages[code_pages].address == stack_base and s.pages[code_pages].flags == 7 | nx); for (supervisor_code.len..code_pages * 4096) |i| if (s.backing[i / 4096][i % 4096] != 0) { @panic("native init code tail not zeroed"); }; @@ -2999,7 +3009,20 @@ fn memoryOperation(f: *const Frame) *const Frame { fn freezeOperation(f: *const Frame) *const Frame { const caller = &states[active]; if (f.rax == 0x23003) { - if (caller.app_binding.bound()) { + if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) { + const parent = &states[1]; + const owner = &states[supervisorSlot()]; + const parent_valid = owner.admitted and !owner.terminal and parent.tty_signal_owner_generation == owner.generation and + parent.app_binding.valid(1, parent.generation, parent.admitted, parent.terminal, parent.app_binding.clock); + const child_valid = active == 2 and caller.tty_signal_owner_generation == null and + caller.parent_pid == parent.generation * states.len + 2 and + caller.app_binding.valid(active, caller.generation, caller.admitted, caller.terminal, caller.app_binding.clock) and + caller.app_binding.reader == parent.app_binding.reader and caller.app_binding.writer == parent.app_binding.writer and caller.app_binding.epoch == parent.app_binding.epoch; + if (!parent_valid or (active != 1 and !child_valid)) { + result(caller, -95); + return select(); + } + } else if (caller.app_binding.bound()) { result(caller, -95); return select(); } @@ -3069,7 +3092,15 @@ fn copyOperation(f: *const Frame) *const Frame { return select(); } const source = &states[from]; - if (source.app_binding.bound()) { + const external = @hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external; + if (external) { + if (from != 1 or f.rdx != 2 or !caller.admitted or caller.terminal or source.tty_signal_owner_generation != caller.generation or + !source.app_binding.valid(from, source.generation, source.admitted, source.terminal, source.app_binding.clock)) + { + result(caller, -95); + return select(); + } + } else if (source.app_binding.bound()) { result(caller, -95); return select(); } @@ -3112,6 +3143,14 @@ fn copyOperation(f: *const Frame) *const Frame { result(caller, -12); return select(); } + var child_binding = child.app_binding; + if (external) { + // Namespace fields are retained kernel custody provenance, never grants. + child_binding = @TypeOf(source.app_binding).init(slot, child.generation + 1, source.app_binding.reader, source.app_binding.writer, source.app_binding.epoch) orelse { + result(caller, -28); + return select(); + }; + } // Identical fixed pools bound every source live page and leaf directory. The // source metadata is kernel-owned and validated at each map/load operation. const prior_generation = child.generation; @@ -3160,6 +3199,11 @@ fn copyOperation(f: *const Frame) *const Frame { if (@import("build_options").native_copy_signals_pending) child.signal_actions.pending = source.signal_actions.pending; if (@import("build_options").native_copy_signals_parent) source.signal_actions.pending = 0; } + if (external) { + child.app_binding = child_binding; + // Owner generation0 is valid, so numeric0 would retain signal authority. + child.tty_signal_owner_generation = null; + } child.parent_pid = source.generation * states.len + from + 1; child.memory_cap = 0x40000000 | (child.generation << 8) | (slot + 1); child.channel_cap = f.r8; @@ -3192,9 +3236,26 @@ fn execOperation(f: *const Frame) *const Frame { return select(); }; const s = &states[slot]; - if (f.rax != 0x23008 and s.app_binding.bound()) { - result(caller, -95); - return select(); + if (f.rax != 0x23008) { + if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) { + const parent = &states[1]; + if (slot != 2 or !caller.admitted or caller.terminal or parent.tty_signal_owner_generation != caller.generation or + !parent.app_binding.valid(1, parent.generation, parent.admitted, parent.terminal, parent.app_binding.clock) or + s.parent_pid != parent.generation * states.len + 2 or s.tty_signal_owner_generation != null or + !s.app_binding.valid(slot, s.generation, s.admitted, s.terminal, s.app_binding.clock) or + s.app_binding.reader != parent.app_binding.reader or s.app_binding.writer != parent.app_binding.writer or s.app_binding.epoch != parent.app_binding.epoch) + { + result(caller, -95); + return select(); + } + if (f.rax == 0x23007 and f.rdx != 0xc003) { + result(caller, -13); + return select(); + } + } else if (s.app_binding.bound()) { + result(caller, -95); + return select(); + } } if (!processSlot(slot)) { result(caller, -13); @@ -3314,6 +3375,12 @@ fn appLaunchOperation(f: *const Frame) *const Frame { result(caller, -13); return select(); } + if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) { + if (f.rsi != 0xc002 or f.rdx != 1) { + result(caller, -13); + return select(); + } + } if (f.rdx >= workerCount() or !processSlot(@intCast(f.rdx))) { result(caller, -22); return select(); diff --git a/kernel/arch/x86_64/native_fs_roles.zig b/kernel/arch/x86_64/native_fs_roles.zig index f1e96f0..e307cd2 100644 --- a/kernel/arch/x86_64/native_fs_roles.zig +++ b/kernel/arch/x86_64/native_fs_roles.zig @@ -15,6 +15,10 @@ pub const owner_data_bytes = 8192; // Complete ABI31 owner measured at39,376 bytes; twelve RX pages give about25% headroom. // Old profile limits remain in their exact selectors. pub const owner_stack_code_limit = 49152; +// Closed native-zsh-external capacity; old selectors above stay exact. +pub const zsh_external_owner_code_limit = 73728; +pub const zsh_external_owner_stack_pages = 3; +pub const zsh_external_owner_stack_base = 0x7ff000; pub const Error = error{InvalidComposition}; pub fn process(slot: usize) bool { diff --git a/kernel/main.zig b/kernel/main.zig index 7612055..0b76193 100644 --- a/kernel/main.zig +++ b/kernel/main.zig @@ -70,7 +70,7 @@ export fn _start() callconv(.c) noreturn { if (@import("build_options").native_c_test) { if (modules.count != 5) @panic("native C requires explicit fifth module"); const native = (modules.modules orelse @panic("missing modules"))[4]; - if (native.size > (if (@import("build_options").native_fs_application_profile) @import("native_fs_session.zig").application_envelope_limit else if (@import("build_options").native_image_large) @as(u64, 8 * 1024 * 1024 + 4096 + 48) else 3 * 1024 * 1024 + 8192 + 80)) @panic("native envelope oversized"); + if (native.size > (if (@hasDecl(@import("build_options"), "native_zsh_external") and @import("build_options").native_zsh_external) @import("native_fs_session.zig").external_application_envelope_limit else if (@import("build_options").native_fs_application_profile) @import("native_fs_session.zig").application_envelope_limit else if (@import("build_options").native_image_large) @as(u64, 8 * 1024 * 1024 + 4096 + 48) else 3 * 1024 * 1024 + 8192 + 80)) @panic("native envelope oversized"); const content = native.address orelse @panic("missing native envelope"); @import("arch/x86_64/native_c.zig").start(address.physical_base, address.virtual_base, content[0..@intCast(native.size)], hhdm.offset, memory); } diff --git a/kernel/native_fs_session.zig b/kernel/native_fs_session.zig index d3cbd86..929ddda 100644 --- a/kernel/native_fs_session.zig +++ b/kernel/native_fs_session.zig @@ -12,6 +12,8 @@ pub const application_magic = "AENFS006"; // Outer/header + existing engine + three selected apps + existing ABI31 Owner. pub const application_envelope_limit: u64 = 48 + elf.large_limits.file_bytes + 112 + 3 * elf.application_limits.file_bytes + roles.owner_stack_code_limit; +pub const external_application_envelope_limit: u64 = 48 + elf.large_limits.file_bytes + 112 + + 3 * elf.application_limits.file_bytes + roles.zsh_external_owner_code_limit; pub const Error = roles.Error; pub const Bundle = struct { engine: []const u8, @@ -34,21 +36,26 @@ pub fn parseOwnerStack(raw: []const u8) Error!Bundle { pub fn parseApplication(raw: []const u8) Error!Bundle { return parseSelected(raw, .application); } -const Profile = enum { conformance, init, owner_data, owner_stack, application }; +// The input magic cannot select its own larger Owner allowance. +pub fn parseExternalApplication(raw: []const u8) Error!Bundle { + return parseSelected(raw, .external_application); +} +const Profile = enum { conformance, init, owner_data, owner_stack, application, external_application }; fn parseSelected(raw: []const u8, comptime profile: Profile) Error!Bundle { const init_owner = profile != .conformance; - const limit: usize = if (profile == .owner_stack or profile == .application) roles.owner_stack_code_limit else if (init_owner) 16384 else roles.controller_limit; + const application = profile == .application or profile == .external_application; + const limit: usize = if (profile == .external_application) roles.zsh_external_owner_code_limit else if (profile == .owner_stack or profile == .application) roles.owner_stack_code_limit else if (init_owner) 16384 else roles.controller_limit; const selected_magic = switch (profile) { .conformance => magic, .init => init_magic, .owner_data => owner_data_magic, .owner_stack => owner_stack_magic, - .application => application_magic, + .application, .external_application => application_magic, }; if (raw.len < 48 or !std.mem.eql(u8, raw[0..8], selected_magic)) return error.InvalidComposition; const engine: usize = std.mem.readInt(u32, raw[8..12], .little); const session: usize = std.mem.readInt(u32, raw[12..16], .little); - const image_limit: usize = if (profile == .application) elf.application_limits.file_bytes else 1024 * 1024; + const image_limit: usize = if (application) elf.application_limits.file_bytes else 1024 * 1024; if (engine == 0 or engine > elf.large_limits.file_bytes or session < 112 or session > 112 + 3 * image_limit + limit or raw.len != 48 + engine + session) return error.InvalidComposition; @@ -56,11 +63,11 @@ fn parseSelected(raw: []const u8, comptime profile: Profile) Error!Bundle { const image = elf.parseWithLimits(bytes, elf.large_limits) catch return error.InvalidComposition; launch.layout(&image) catch return error.InvalidComposition; const nested = raw[48 + engine ..]; - if (!std.mem.eql(u8, nested[0..8], if (profile == .application) "AENMX011" else if (profile == .owner_stack) "AENMX010" else "AENMX009")) return error.InvalidComposition; - const bundle = (if (profile == .application) launch.applicationBundle(nested) else if (profile == .owner_stack) launch.ownerStackBundle(nested) else launch.pipelineBundle(nested)) catch return error.InvalidComposition; + if (!std.mem.eql(u8, nested[0..8], if (application) "AENMX011" else if (profile == .owner_stack) "AENMX010" else "AENMX009")) return error.InvalidComposition; + const bundle = (if (profile == .external_application) launch.externalApplicationBundle(nested) else if (profile == .application) launch.applicationBundle(nested) else if (profile == .owner_stack) launch.ownerStackBundle(nested) else launch.pipelineBundle(nested)) catch return error.InvalidComposition; if (bundle.supervisor.len > limit) return error.InvalidComposition; for (bundle.images) |process_bytes| { - const process_image = (if (profile == .application) elf.parseApplication(process_bytes) else elf.parse(process_bytes)) catch return error.InvalidComposition; + const process_image = (if (application) elf.parseApplication(process_bytes) else elf.parse(process_bytes)) catch return error.InvalidComposition; launch.layout(&process_image) catch return error.InvalidComposition; } return .{ .engine = bytes, .engine_entropy = raw[16..48], .session = bundle }; diff --git a/kernel/native_launch.zig b/kernel/native_launch.zig index dec3d57..ae5c161 100644 --- a/kernel/native_launch.zig +++ b/kernel/native_launch.zig @@ -48,6 +48,11 @@ pub fn applicationBundle(raw: []const u8) Error!Bundle { if (raw.len < 8 or !std.mem.eql(u8, raw[0..8], "AENMX011")) return error.InvalidLaunch; return threeImageBundleLimit(raw, 112, @import("arch/x86_64/native_fs_roles.zig").owner_stack_code_limit, elf.application_limits.file_bytes); } +/// Same application wire, only under the validated external-Zsh selection. +pub fn externalApplicationBundle(raw: []const u8) Error!Bundle { + if (raw.len < 8 or !std.mem.eql(u8, raw[0..8], "AENMX011")) return error.InvalidLaunch; + return threeImageBundleLimit(raw, 112, @import("arch/x86_64/native_fs_roles.zig").zsh_external_owner_code_limit, elf.application_limits.file_bytes); +} fn threeImageBundleLimit(raw: []const u8, header: usize, limit: usize, image_limit: usize) Error!Bundle { if (raw.len < header) return error.InvalidLaunch; const a: usize = std.mem.readInt(u32, raw[8..12], .little); diff --git a/scripts/owner_stack.py b/scripts/owner_stack.py index ae2656b..81faed3 100644 --- a/scripts/owner_stack.py +++ b/scripts/owner_stack.py @@ -11,7 +11,7 @@ def __init__(self, observation): def inspect(disassembly, elf=None, *, stack_bytes=4096): - if type(stack_bytes) is not int or stack_bytes not in (4096, 8192): + if type(stack_bytes) is not int or stack_bytes not in (4096, 8192, 12288): raise ValueError('unknown owner stack profile') usable = stack_bytes - 8 spans = None diff --git a/sys-init/prototypes/native-filesystem-session/children.rs b/sys-init/prototypes/native-filesystem-session/children.rs index 30e7623..a5a33a2 100644 --- a/sys-init/prototypes/native-filesystem-session/children.rs +++ b/sys-init/prototypes/native-filesystem-session/children.rs @@ -119,6 +119,18 @@ impl Children { Ok(()) } + /// Closed selected parent/helper flags. Stop/continue bits are accepted but + /// this profile has no such events; status still comes only from terminal(). + pub fn wait_flags(&mut self, pid: i32, flags: u32) -> Result { + if flags & !11 != 0 { return Err(-22); } + self.wait(pid, flags & 1 != 0) + } + pub fn reclaimed_exact(&self, identity: Identity) -> bool { + if identity.slot != 2 { return false; } + let child = &self.records[0]; + child.identity.is_none() && child.state == State::Empty + && child.last_generation == identity.generation + } /// At most one parent wait reply is outstanding on its authenticated channel. /// The dispatcher retries its cached reply until delivery is confirmed; a new /// wait call while that reservation exists is rejected. @@ -192,6 +204,33 @@ impl Children { } } +/// Input ownership is independent of wait eligibility. A terminal child keeps +/// the lease through known wait delivery and native/transport/ref settlement. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ReadLease { + #[default] + Parent, + Child(Identity), + Contained, +} +impl ReadLease { + pub fn give_child(&mut self, identity: Identity, parent_read_pending: bool) -> Result<(), i32> { + Identity::admitted(identity.slot, identity.generation, identity.handle)?; + if *self != Self::Parent || identity.slot != 2 || parent_read_pending { return Err(-16); } + *self = Self::Child(identity); Ok(()) + } + pub fn restore_parent(&mut self, identity: Identity, children: &Children) -> Result<(), i32> { + if *self != Self::Child(identity) || !children.reclaimed_exact(identity) { return Err(-16); } + *self = Self::Parent; Ok(()) + } + pub fn child_read(&self, identity: Identity, canonical: bool, isig: bool) -> Result<(), i32> { + if *self != Self::Child(identity) { return Err(-13); } + if canonical || isig { return Err(-95); } + Ok(()) + } + pub fn contain(&mut self) { *self = Self::Contained; } +} + #[cfg(test)] mod tests { use super::*; @@ -333,3 +372,32 @@ mod tests { } } } + +#[cfg(test)] +mod selected_lease_tests { + use super::*; + fn child() -> Identity { Identity::admitted(2, 1, 0xb0000103).unwrap() } + #[test] + fn real_wait_delivery_collection_reclaim_precede_parent_read_lease() { + let id = child(); let mut children = Children::new(); let mut lease = ReadLease::Parent; + assert!(lease.give_child(id, true).is_err()); lease.give_child(id, false).unwrap(); children.admit(id).unwrap(); + assert_eq!(children.wait_flags(-1, 11).unwrap(), Wait::NoChange); + children.terminal(id, 1792).unwrap(); + assert!(lease.restore_parent(id, &children).is_err()); + assert_eq!(children.wait_flags(-1, 11).unwrap(), Wait::Ready { identity: id, status: 1792 }); + assert!(children.reclaimed(id).is_err()); assert!(lease.restore_parent(id, &children).is_err()); + children.delivered(id).unwrap(); assert!(lease.restore_parent(id, &children).is_err()); + children.reclaimed(id).unwrap(); lease.restore_parent(id, &children).unwrap(); + assert_eq!(lease, ReadLease::Parent); assert_eq!(children.wait_flags(-1, 0).unwrap(), Wait::NoChild); + } + #[test] + fn child_canonical_or_isig_read_is_refused_and_unknown_is_sticky() { + let id = child(); let mut lease = ReadLease::Parent; lease.give_child(id, false).unwrap(); + assert_eq!(lease.child_read(id, true, false), Err(-95)); assert_eq!(lease.child_read(id, false, true), Err(-95)); + assert_eq!(lease.child_read(id, false, false), Ok(())); + assert_eq!(lease.child_read(Identity::admitted(2, 2, 0xb0000203).unwrap(), false, false), Err(-13)); + lease.contain(); assert!(lease.give_child(id, false).is_err()); assert!(lease.child_read(id, false, false).is_err()); + let mut children = Children::new(); assert_eq!(children.wait_flags(-1, 4), Err(-22)); + assert_eq!(children.wait_flags(0, 0), Err(-22)); + } +} diff --git a/sys-init/prototypes/native-filesystem-session/fd_startup.rs b/sys-init/prototypes/native-filesystem-session/fd_startup.rs index 2396055..98c5a9c 100644 --- a/sys-init/prototypes/native-filesystem-session/fd_startup.rs +++ b/sys-init/prototypes/native-filesystem-session/fd_startup.rs @@ -10,6 +10,8 @@ use abi29_dispatch_core::startup::{Identity, greeting}; fs_file16 ))] compile_error!("terminal bindings cannot select file16 startup"); +#[cfg(all(native_zsh_process, not(native_zsh_filesystem)))] +compile_error!("Zsh process startup requires the selected filesystem profile"); pub const CANONICAL_BINDINGS: bool = cfg!(native_tty_canonical_probe); pub const SIGNAL_BINDINGS: bool = cfg!(native_tty_signal_probe); pub const MODE_BINDINGS: bool = cfg!(native_tty_mode_probe); @@ -34,7 +36,9 @@ pub const BYTES: usize = if cfg!(native_zsh_filesystem) { }; pub const FILE_SLOTS_16: bool = cfg!(fs_file16); pub const FIRST_FILE_FD: u8 = if FILE_SLOTS_16 { 0 } else { 3 }; -pub const MAGIC: &[u8; 8] = if cfg!(native_zsh_filesystem) { +pub const MAGIC: &[u8; 8] = if cfg!(native_zsh_process) { + b"AEFDB009" +} else if cfg!(native_zsh_filesystem) { b"AEFDB008" } else if CANONICAL_BINDINGS { b"AEFDB007" @@ -51,7 +55,9 @@ pub const MAGIC: &[u8; 8] = if cfg!(native_zsh_filesystem) { } else { b"AEFDB001" }; -pub const MAP_MAGIC: &[u8; 8] = if cfg!(native_zsh_filesystem) { +pub const MAP_MAGIC: &[u8; 8] = if cfg!(native_zsh_process) { + b"AEFDT009" +} else if cfg!(native_zsh_filesystem) { b"AEFDT008" } else if CANONICAL_BINDINGS { b"AEFDT007" @@ -104,7 +110,11 @@ pub fn encode(plan: &Plan, output: &mut [u8; BYTES]) -> Result<(), i32> { if plan.generation == 0 || plan.history.iter().any(|g| *g > 9) { return Err(-22); } + if cfg!(native_zsh_process) && !matches!(plan.identity.slot, 1 | 2) { + return Err(-22); + } if DUPLEX_BINDINGS + && !cfg!(native_zsh_process) && (plan.history != [0; 2] || plan.bindings.iter().enumerate().any(|(fd, b)| { if fd < 3 { @@ -123,7 +133,11 @@ pub fn encode(plan: &Plan, output: &mut [u8; BYTES]) -> Result<(), i32> { } match binding.kind { 0 if binding.selector == 0 && binding.cloexec == 0 => {} - 1 | 4 if binding.selector <= 2 && (binding.kind == 1 || TERMINAL_BINDINGS) => { + 1 | 4 + if binding.selector <= 2 + && (binding.kind == 1 || TERMINAL_BINDINGS) + && (!cfg!(native_zsh_process) || binding.kind == 4) => + { if plan.bindings[..i].iter().any(|other| { matches!(other.kind, 1 | 4) && other.selector == binding.selector @@ -207,13 +221,21 @@ pub fn encode(plan: &Plan, output: &mut [u8; BYTES]) -> Result<(), i32> { put( output, 296, - if plan.identity.slot == 1 && !cfg!(native_zsh_filesystem) { + if plan.identity.slot == 1 && (!cfg!(native_zsh_filesystem) || cfg!(native_zsh_process)) { 0x76050001 } else { 0 }, ); - put(output, 300, if MODE_BINDINGS { 0x7d000001 } else { 0 }); + put( + output, + 300, + if MODE_BINDINGS && (!cfg!(native_zsh_process) || plan.identity.slot == 1) { + 0x7d000001 + } else { + 0 + }, + ); #[cfg(native_zsh_filesystem)] { for offset in [304, 308, 312, 316] { diff --git a/sys-init/prototypes/native-filesystem-session/native_app_runtime.rs b/sys-init/prototypes/native-filesystem-session/native_app_runtime.rs index 33a1bfd..5d0eacd 100644 --- a/sys-init/prototypes/native-filesystem-session/native_app_runtime.rs +++ b/sys-init/prototypes/native-filesystem-session/native_app_runtime.rs @@ -139,6 +139,80 @@ impl Bound { Ok(()) } } +#[cfg(native_zsh_process)] +// This shared closed codec also provides opposite-direction encoders for tests. +#[allow(dead_code)] +#[path = "zsh_process_frames.rs"] +mod zsh_process_frames; +#[cfg(native_zsh_process)] +use abi29_dispatch_core::children::{Children, Identity as ChildIdentity, ReadLease, Wait}; +#[cfg(native_zsh_process)] +use fd_startup::ForkStreams; +#[cfg(all(native_zsh_process, not(native_zsh_filesystem)))] +compile_error!("Zsh process controller requires retained filesystem custody"); + +#[cfg(native_zsh_process)] +struct ZshProcess { + binding: Option, + endpoints: Option>>, + ends: [End; 4], + children: Children, + lease: ReadLease, + request: [u8; 32], + at: usize, + parent_at: usize, + fork_sequence: u64, + sequence: u64, + epoch: u8, + prepaid: u8, + fork_ready: bool, + committed: bool, + exec_ready: bool, + wait_status: Option, + terminal: Option, + reclaimed: bool, + cleanup_attempted: u8, + cleanup_last: Status, +} +#[cfg(native_zsh_process)] +impl ZshProcess { + fn new() -> Self { + Self { + binding: None, + endpoints: None, + ends: [End::default(); 4], + children: Children::new(), + lease: ReadLease::Parent, + request: [0; 32], + at: 0, + parent_at: 0, + fork_sequence: 0, + sequence: 0, + epoch: 0, + prepaid: 0, + fork_ready: false, + committed: false, + exec_ready: false, + wait_status: None, + terminal: None, + reclaimed: false, + cleanup_attempted: 0, + cleanup_last: (-5, 0, 0), + } + } + fn identity(&self) -> Result { + let binding = self.binding.ok_or(-9)?; + Identity::admitted(2, u32::from(binding.epoch), binding.handle).map_err(i64::from) + } + fn child(&self) -> Result { + let id = self.identity()?; + ChildIdentity::admitted(2, id.epoch, id.handle).map_err(i64::from) + } + fn caller(&self) -> Result { + Ok(Caller::Child(self.identity()?.epoch)) + } +} + pub struct Runtime { kernel: K, bound: Bound, @@ -183,6 +257,8 @@ pub struct Runtime { cleanup: [[Status; 4]; 2], console_status: i64, observed_terminal: [Option; 2], + #[cfg(native_zsh_process)] + process: ZshProcess, } impl Runtime { pub fn new(kernel: K) -> Result { @@ -252,6 +328,8 @@ impl Runtime { cleanup: [[(-5, 0, 0); 4]; 2], console_status: -5, observed_terminal: [None; 2], + #[cfg(native_zsh_process)] + process: ZshProcess::new(), }) } /// Selected Owner construction in its final mapped slot. Initialization @@ -309,6 +387,8 @@ impl Runtime { core::ptr::addr_of_mut!((*pointer).cleanup).write([[(-5, 0, 0); 4]; 2]); core::ptr::addr_of_mut!((*pointer).console_status).write(-5); core::ptr::addr_of_mut!((*pointer).observed_terminal).write([None; 2]); + #[cfg(native_zsh_process)] + core::ptr::addr_of_mut!((*pointer).process).write(ZshProcess::new()); core::ptr::addr_of_mut!((*pointer).kernel).write(kernel); Ok(slot.assume_init_mut()) } @@ -629,6 +709,7 @@ impl Runtime { startup.fill(0); result } + #[cfg(not(native_zsh_process))] fn request( &mut self, #[allow(unused_variables)] workspace: &mut [u8; 4096], @@ -1066,30 +1147,63 @@ impl Runtime { result.map_err(fs_errno) } #[cfg(native_zsh_filesystem)] - fn file_request(&mut self, workspace: &mut [u8; 4096]) -> Result<(), i64> { + fn file_request( + &mut self, + workspace: &mut [u8; 4096], + #[cfg(native_zsh_process)] child: bool, + ) -> Result<(), i64> { + #[cfg(native_zsh_process)] + let binding = if child { + self.process.binding + } else { + self.file_binding + } + .ok_or(-9)?; + #[cfg(not(native_zsh_process))] let binding = self.file_binding.ok_or(-9)?; - if self - .identity - .map(|id| (id.slot as u8, id.epoch as u8, id.handle)) + #[cfg(native_zsh_process)] + let identity = if child { + Some(self.process.identity()?) + } else { + self.identity + }; + #[cfg(not(native_zsh_process))] + let identity = self.identity; + if identity.map(|id| (id.slot as u8, id.epoch as u8, id.handle)) != Some((binding.slot, binding.epoch, binding.handle)) { return Err(-9); } + #[cfg(native_zsh_process)] + self.zsh_live(child)?; + #[cfg(not(native_zsh_process))] self.mode_peer_live()?; let header = self.file_header; let length = self.with_files(|files| files.body_length(binding, &header))?; workspace.fill(0); + #[cfg(native_zsh_process)] + let endpoints = if child { + self.process.endpoints.as_mut() + } else { + self.endpoints.as_mut() + } + .ok_or(-9)?; + #[cfg(not(native_zsh_process))] + let endpoints = self.endpoints.as_mut().ok_or(-9)?; stream_transport::transfer( &mut OwnerSteps { kernel: &mut self.kernel, bound: &mut self.bound, - endpoints: self.endpoints.as_mut().ok_or(-9)?, + endpoints, progress: &mut self.request_at, }, &mut workspace[..length], false, )?; // Authenticate the exact admitted generation again after body assembly. + #[cfg(native_zsh_process)] + self.zsh_live(child)?; + #[cfg(not(native_zsh_process))] self.mode_peer_live()?; let files = unsafe { self.files.assume_init_mut() }; files @@ -1118,7 +1232,15 @@ impl Runtime { Ok(()) }) .map_err(fs_errno)?; - let delivery = if let Some(endpoints) = self.endpoints.as_mut() { + #[cfg(native_zsh_process)] + let endpoint = if child { + self.process.endpoints.as_mut() + } else { + self.endpoints.as_mut() + }; + #[cfg(not(native_zsh_process))] + let endpoint = self.endpoints.as_mut(); + let delivery = if let Some(endpoints) = endpoint { let mut steps = OwnerSteps { kernel: &mut self.kernel, bound: &mut self.bound, @@ -1158,6 +1280,729 @@ impl Runtime { } result } + #[cfg(native_zsh_process)] + fn zsh_expected(&self, child: bool) -> Result { + Ok(zsh_process_frames::Expected { + identity: if child { + self.process.identity()? + } else { + self.identity.ok_or(-9)? + }, + parent: if child { + self.identity.ok_or(-9)?.pid + } else { + 7 + }, + generation: if child { + self.process.binding.ok_or(-9)?.generation + } else { + self.file_binding.ok_or(-9)?.generation + }, + }) + } + #[cfg(native_zsh_process)] + #[inline(never)] + fn zsh_io(&mut self, child: bool, bytes: &mut [u8], writing: bool) -> Result<(), i64> { + let endpoints = if child { + self.process.endpoints.as_mut() + } else { + self.endpoints.as_mut() + } + .ok_or(-9)?; + let mut progress = 0; + stream_transport::transfer( + &mut OwnerSteps { + kernel: &mut self.kernel, + bound: &mut self.bound, + endpoints, + progress: &mut progress, + }, + bytes, + writing, + ) + } + #[cfg(native_zsh_process)] + fn zsh_live(&mut self, child: bool) -> Result<(), i64> { + self.mode_peer_live()?; + if child { + let state = self.ctl(0x20001, self.process.identity()?.handle, 0)?; + if state.0 != 0 { + return Err(if state.0 < 0 { state.0 } else { -5 }); + } + } + Ok(()) + } + // Child cleanup calls are prepaid from the SAME ordinary allowance before + // channel/copy effects. No reserve, deadline or new child budget is created. + #[cfg(native_zsh_process)] + fn zsh_leaf(&mut self, op: u64, argument: u64) -> Result { + self.bound.check(&mut self.kernel)?; + self.zsh_leaf_reserved(op, argument) + } + #[cfg(native_zsh_process)] + fn zsh_leaf_reserved(&mut self, op: u64, argument: u64) -> Result { + if self.process.prepaid == 0 { + return Err(-5); + } + self.process.prepaid -= 1; + Ok(self + .kernel + .ctl(op, self.process.identity()?.handle, argument)) + } + #[cfg(native_zsh_process)] + fn zsh_close_child(&mut self) -> Result<(), i64> { + let mut error = 0; + for index in 0..4 { + let end = self.process.ends[index]; + if !end.owned || end.attempted { + continue; + } + if self.process.prepaid == 0 { + return Err(-5); + } + self.process.prepaid -= 1; + self.process.ends[index].attempted = true; + self.process.ends[index].status = self.kernel.close(end.cap); + if self.process.ends[index].status != 0 { + error = self.process.ends[index].status; + } + } + zero(error) + } + #[cfg(native_zsh_process)] + fn zsh_pair(&mut self) -> Result<[u64; 4], i64> { + if self.process.ends.iter().any(|e| e.owned && !e.attempted) { + return Err(-16); + } + for _ in 0..6 { + self.bound.spend()?; + } + self.process.prepaid = 6; + self.process.ends = [End::default(); 4]; + for pair in 0..2 { + let value = self.ctl(0x24000, 0xd001, 0)?; + if value.0 <= 0 { + return Err(if value.0 < 0 { value.0 } else { -5 }); + } + for (side, cap) in [value.0 as u64, value.1].into_iter().enumerate() { + self.process.ends[pair * 2 + side] = End { + cap, + owned: true, + ..End::default() + }; + if cap >> 24 != 0xd1 + || cap & 255 != side as u64 + 1 + || ((cap >> 8) & 255) == 0 + || ((cap >> 8) & 255) > 15 + || ((cap >> 16) & 255) >= 10 + { + return Err(-5); + } + } + } + let caps = self.process.ends.map(|e| e.cap); + for i in 0..4 { + if caps[..i].contains(&caps[i]) { + return Err(-5); + } + } + Ok(caps) + } + #[cfg(native_zsh_process)] + fn zsh_frozen(&mut self, handle: u64) -> Result { + loop { + let state = self.ctl(0x23004, handle, 0)?; + if state.0 > 0 { + return Ok(state.0 as u64); + } + if state.0 < 0 { + return Err(state.0); + } + self.yield_now()?; + } + } + #[cfg(native_zsh_process)] + fn zsh_validate_files(&mut self, child: bool, raw: &[u8; 336]) -> Result<(), i64> { + let binding = if child { + self.process.binding + } else { + self.file_binding + } + .ok_or(-9)?; + let actual = self.with_files(|files| files.file_view(binding))?; + let (mut present, mut cloexec) = (0u16, 0u16); + for binding in raw[112..176].chunks_exact(4) { + if binding[0] == 3 { + present |= 1 << binding[1]; + if binding[2] != 0 { + cloexec |= 1 << binding[1]; + } + } + } + if actual != (present, cloexec) { + return Err(-9); + } + Ok(()) + } + #[cfg(native_zsh_process)] + #[inline(never)] + fn zsh_fork(&mut self, workspace: &mut [u8; 4096]) -> Result<(), i64> { + let expected = self.zsh_expected(false)?; + let (snapshot, sequence) = + zsh_process_frames::fork_request(&workspace[..368], expected).map_err(i64::from)?; + if sequence != self.process.fork_sequence.checked_add(1).ok_or(-28)? + || self.process.binding.is_some() + || self.process.lease != ReadLease::Parent + { + return Err(-16); + } + let plan = zsh_stream_plan(snapshot.raw(), expected)?; + self.streams + .validate_context(Caller::Parent, &plan) + .map_err(i64::from)?; + self.zsh_validate_files(false, snapshot.raw())?; + let freeze = self.zsh_frozen(expected.identity.handle)?; + let epoch = self + .process + .epoch + .checked_add(1) + .filter(|v| *v <= 15) + .ok_or(-28)?; + let id = Identity::admitted(2, u32::from(epoch), 0xb0000000 | u64::from(epoch) << 8 | 3) + .map_err(i64::from)?; + let caps = self.zsh_pair()?; + self.streams.stage_child(id.pid, &plan).map_err(i64::from)?; + self.process.fork_sequence = sequence; + self.process.sequence = sequence; + self.process.binding = Some(filesystem_readonly::Binding { + slot: 2, + epoch, + handle: id.handle, + generation: caps[0], + }); + self.process.fork_ready = false; + self.process.committed = false; + self.process.exec_ready = false; + self.process.terminal = None; + self.process.wait_status = None; + self.process.reclaimed = false; + self.process.cleanup_attempted = 0; + self.process.cleanup_last = (-5, 0, 0); + // On unknown copy, these two ends may already belong to child2. They + // must never be closed as Owner-held capabilities after that ambiguity. + self.process.ends[2].owned = false; + self.process.ends[1].owned = false; + self.bound.before(&mut self.kernel)?; + let copied = self + .kernel + .copy(expected.identity.handle, 2, freeze, caps[2], caps[1]); + if copied != id.handle as i64 { + if !(-4095..=-1).contains(&copied) { + return Err(-5); + } + self.process.ends[2].owned = true; + self.process.ends[1].owned = true; + self.process.binding = None; + self.streams.cancel_stage(id.pid).map_err(i64::from)?; + self.zsh_close_child()?; + self.process.prepaid = 0; + zero(self.ctl(0x23005, expected.identity.handle, freeze)?.0)?; + let mut reply = + zsh_process_frames::receipt(b"AEZFR001", sequence, copied).map_err(i64::from)?; + self.zsh_io(false, &mut reply, true)?; + workspace.fill(0); + return Ok(()); + } + self.process.epoch = epoch; + self.process + .children + .admit(self.process.child()?) + .map_err(i64::from)?; + let binding = self.process.binding.ok_or(-9)?; + let parent = self.file_binding.ok_or(-9)?; + self.with_files(|files| files.inherit_child(parent, binding))?; + self.process.endpoints = Some(ManuallyDrop::new( + Endpoints::new(self.kernel.clone(), caps[0], caps[3]).map_err(|_| -5)?, + )); + self.process + .lease + .give_child(self.process.child()?, false) + .map_err(i64::from)?; + // Publish the complete checked raw snapshot, including duplex byte3. + let out: &mut [u8; 336] = (&mut workspace[32..368]).try_into().unwrap(); + zsh_rebind_snapshot(out, id, expected.identity.pid, binding.generation)?; + self.zsh_io(true, out, true)?; + workspace.fill(0); + Ok(()) + } + #[cfg(native_zsh_process)] + fn zsh_child_ready(&mut self, frame: &[u8; 32]) -> Result<(), i64> { + let id = self.process.identity()?; + if self.process.fork_ready || self.process.committed { + return Err(-9); + } + let pid = zsh_process_frames::decode_receipt(frame, b"AEZFC001", self.process.sequence) + .map_err(i64::from)?; + if pid != i64::from(id.pid) { + return Err(-9); + } + self.streams.child_ready(id.pid).map_err(i64::from)?; + self.process.fork_ready = true; + let mut reply = zsh_process_frames::receipt(b"AEZFR001", self.process.sequence, pid) + .map_err(i64::from)?; + self.zsh_io(false, &mut reply, true) + } + #[cfg(native_zsh_process)] + #[inline(never)] + fn zsh_exec(&mut self, workspace: &mut [u8; 4096]) -> Result<(), i64> { + let expected = self.zsh_expected(true)?; + let (snapshot, sequence) = + zsh_process_frames::exec_request(&workspace[..672], expected).map_err(i64::from)?; + if !self.process.fork_ready + || self.process.terminal.is_some() + || sequence != self.process.sequence.checked_add(1).ok_or(-28)? + { + return Err(-9); + } + let mut plan = zsh_stream_plan(snapshot.raw(), expected)?; + self.streams + .validate_context(self.process.caller()?, &plan) + .map_err(i64::from)?; + self.zsh_validate_files(true, snapshot.raw())?; + let path = &workspace[368..400]; + let rejection = if path.starts_with(b"/inputs/missing\0") { + -2 + } else if path.starts_with(b"/inputs/denied\0") { + -13 + } else if !path.starts_with(b"/inputs/helper\0") { + -13 + } else if !zsh_helper_arguments((&workspace[400..672]).try_into().unwrap()) { + -13 + } else { + 0 + }; + let freeze = self.zsh_frozen(expected.identity.handle)?; + self.process.sequence = sequence; + self.bound.before(&mut self.kernel)?; + let before = self.kernel.receipt(expected.identity.handle); + if before.0 < 0 || before.2 != 0 { + return Err(-5); + } + let arguments: &[u8; 272] = (&workspace[400..672]).try_into().unwrap(); + self.bound.before(&mut self.kernel)?; + let committed = if rejection != 0 { + rejection + } else { + self.kernel + .exec(expected.identity.handle, freeze, arguments) + }; + if committed != 0 { + if !(-4095..=-1).contains(&committed) { + return Err(-5); + } + self.bound.before(&mut self.kernel)?; + if self.kernel.receipt(expected.identity.handle) != before { + return Err(-5); + } + zero(self.ctl(0x23005, expected.identity.handle, freeze)?.0)?; + let mut reply = + zsh_process_frames::receipt(b"AEZER001", sequence, committed).map_err(i64::from)?; + self.zsh_io(true, &mut reply, true)?; + workspace.fill(0); + return Ok(()); + } + self.bound.before(&mut self.kernel)?; + if self.kernel.receipt(expected.identity.handle) != (freeze as i64, 0xc003, 1) { + return Err(-5); + } + self.streams + .commit_exec(self.process.caller()?, &mut plan) + .map_err(i64::from)?; + let old = self.process.binding.ok_or(-9)?; + let generation = old + .generation + .checked_add(1) + .filter(|v| *v != u64::MAX) + .ok_or(-28)?; + let binding = self.with_files(|files| files.exec_child(old, generation))?; + self.process.binding = Some(binding); + let raw: &mut [u8; 336] = (&mut workspace[32..368]).try_into().unwrap(); + zsh_exec_snapshot(raw)?; + zsh_rebind_snapshot(raw, expected.identity, expected.parent, generation)?; + // Publish cannot be inferred from a returning exec or a fake READY. + self.bound.before(&mut self.kernel)?; + zero(self.kernel.publish(expected.identity.handle, freeze))?; + self.process.committed = true; + self.process.exec_ready = false; + self.zsh_io(true, raw, true)?; + workspace.fill(0); + Ok(()) + } + #[cfg(native_zsh_process)] + fn zsh_terminal(&mut self) -> Result<(), i64> { + if self.process.binding.is_none() || self.process.terminal.is_some() { + return Ok(()); + } + let state = self.ctl(0x20001, self.process.identity()?.handle, 0)?; + if state.0 < 0 { + return Err(state.0); + } + if self.process.committed && state.1 == 1 { + self.process.exec_ready = true; + } + if state.0 == 0 { + return Ok(()); + } + if !self.process.fork_ready || state.2 != 1 || !(1..=256).contains(&state.0) { + return Err(-5); + } + let status = ((state.0 - 1) as i32) << 8; + self.process + .children + .terminal(self.process.child()?, status) + .map_err(i64::from)?; + self.streams + .confirmed_terminal(self.process.caller()?) + .map_err(i64::from)?; + self.process.terminal = Some(status); + self.process.at = 0; + Ok(()) + } + #[cfg(native_zsh_process)] + fn zsh_collect(&mut self, identity: ChildIdentity, status: i32) -> Result<(), i64> { + self.process + .children + .delivered(identity) + .map_err(i64::from)?; + self.process.wait_status = Some(status); + let binding = self.process.binding.ok_or(-9)?; + self.with_files(|files| files.settle_child(binding))?; + self.zsh_close_child()?; + zero(self.zsh_leaf(0x20002, 0)?.0)?; + if self.zsh_leaf(0x20001, 0)?.0 != -9 { + return Err(-5); + } + self.process + .children + .reclaimed(identity) + .map_err(i64::from)?; + self.process + .lease + .restore_parent(identity, &self.process.children) + .map_err(i64::from)?; + self.process.binding = None; + self.process.endpoints = None; + self.process.prepaid = 0; + self.process.reclaimed = true; + self.process.at = 0; + Ok(()) + } + #[cfg(native_zsh_process)] + fn zsh_stream(&mut self, child: bool, frame: &[u8; 32]) -> Result { + let op = u32_at(frame, 0); + let selector = u32_at(frame, 8) as usize; + if op == 68 { + if child || frame[12..] != [0; 20] { + return Err(-22); + } + let wait = self + .process + .children + .wait_flags(u32_at(frame, 4) as i32, u32_at(frame, 8)) + .map_err(i64::from)?; + let (mut reply, collected) = match wait { + Wait::Pending => return Ok(false), + Wait::NoChange => ([0; 32], None), + Wait::NoChild => { + let mut r = [0; 32]; + r[..4].copy_from_slice(&(-10i32).to_le_bytes()); + (r, None) + } + Wait::Ready { identity, status } => { + let mut r = [0; 32]; + r[..4].copy_from_slice(&identity.pid.to_le_bytes()); + r[4..8].copy_from_slice(&status.to_le_bytes()); + (r, Some((identity, status))) + } + }; + self.zsh_io(false, &mut reply, true)?; + if let Some((identity, status)) = collected { + self.zsh_collect(identity, status)?; + } + return Ok(true); + } + if cfg!(native_tty_mode_probe) + && (matches!(op, 0x10..=0x1f) || u32_at(frame, 4) == 0x7d000001) + { + let mut reply = if child { + let mut r = [0; 32]; + r[..4].copy_from_slice(&(-95i32).to_le_bytes()); + r + } else { + self.mode_request()? + }; + self.zsh_io(child, &mut reply, true)?; + return Ok(true); + } + if op == 1 && selector < 3 { + if child { + let (terminal, refs, caller) = self.tty.as_ref().ok_or(-9)?; + let mode = terminal + .mode_state(*caller, &refs.stdin) + .map_err(tty_errno)? + .0; + if let Err(error) = self.process.lease.child_read( + self.process.child()?, + mode.canonical, + mode.signals, + ) { + let mut reply = [0; 32]; + reply[..4].copy_from_slice(&error.to_le_bytes()); + self.zsh_io(true, &mut reply, true)?; + return Ok(true); + } + } else if self.process.lease != ReadLease::Parent { + return Ok(false); + } + } + if op == 3 && selector < 3 { + self.flush_terminal_output()?; + } + let who = if child { + self.process.caller()? + } else { + Caller::Parent + }; + let kernel = &mut self.kernel; + let bound = &mut self.bound; + let (terminal, refs, caller) = self.tty.as_mut().ok_or(-9)?; + let reference = match selector { + 0 => refs.stdin, + 1 => refs.stdout, + _ => refs.stderr, + }; + let mut unknown = false; + let mut plain = *frame; + let eligible = if op == 1 { + if frame[16] > 1 || frame[17..] != [0; 15] { + return Err(-22); + } + plain[16] = 0; + frame[16] != 0 + } else { + false + }; + let outcome = self.streams.request(who, &plain, |writing, bytes| { + if let Err(e) = bound.before(kernel) { + return e; + } + let result = if writing { + terminal.write(*caller, &reference, bytes, |_, input| { + let mut scratch = [0; 16]; + scratch[..input.len()].copy_from_slice(input); + completion( + kernel.serial(true, &mut scratch[..input.len()]), + input.len(), + ) + }) + } else { + terminal + .read_signalled( + *caller, + &reference, + bytes, + eligible, + &mut SignalBackend { + kernel, + bound, + target: self.app, + }, + ) + .map(|n| match n { + ReadOutcome::Bytes(n) => n, + ReadOutcome::EmptyRequest | ReadOutcome::Eof => 0, + }) + }; + match result { + Ok(n) => n as i64, + Err(TtyError::UnknownCompletion) => { + unknown = true; + -5 + } + Err(e) => tty_errno(e), + } + }); + if unknown { + return Err(-5); + } + match outcome { + Outcome::Pending => Ok(false), + Outcome::Reply(mut reply) => { + self.zsh_io(child, &mut reply, true)?; + if !child && op == 3 && selector < 3 && u32_at(&reply, 0) == 0 { + self.closed[selector] = true; + } + Ok(true) + } + } + } + #[cfg(native_zsh_process)] + fn zsh_poll(&mut self, child: bool, workspace: &mut [u8; 4096]) -> Result, i64> { + if child && (self.process.binding.is_none() || self.process.terminal.is_some()) { + return Ok(None); + } + let mut at = if child { + self.process.at + } else { + self.process.parent_at + }; + if at < 16 { + self.bound.before(&mut self.kernel)?; + let endpoints = if child { + self.process.endpoints.as_mut() + } else { + self.endpoints.as_mut() + } + .ok_or(-9)?; + let frame = if child { + &mut self.process.request + } else { + &mut self.request + }; + let count = endpoints.transfer(false, &mut frame[at..16]); + if count == -11 { + return Ok(None); + } + if count == 0 { + if !child + && at == 0 + && self.closed == [true; DESCRIPTORS] + && self.process.binding.is_none() + { + return Ok(Some(false)); + } + return Err(-5); + } + if count < 0 || count as usize > 16 - at { + return Err(if count < 0 { count } else { -5 }); + } + at += count as usize; + if child { + self.process.at = at; + } else { + self.process.parent_at = at; + } + if at < 16 { + return Ok(Some(true)); + } + } + if at == 16 { + let frame = if child { + self.process.request + } else { + self.request + }; + let large = if &frame[..8] == b"AEZFK001" { + if child { + return Err(-13); + } + 368 + } else if &frame[..8] == b"AEZEX001" { + if !child { + return Err(-13); + } + 672 + } else { + 0 + }; + if large != 0 { + workspace.fill(0); + workspace[..16].copy_from_slice(&frame[..16]); + self.zsh_io(child, &mut workspace[16..large], false)?; + let result = if child { + self.zsh_exec(workspace) + } else { + self.zsh_fork(workspace) + }; + if child { + self.process.at = 0; + } else { + self.process.parent_at = 0; + } + return result.map(|()| Some(true)); + } + if frame[..8] == filesystem_readonly::MAGIC[..] { + self.file_header.fill(0); + self.file_header[..16].copy_from_slice(&frame[..16]); + let mut tail = [0; 48]; + self.zsh_io(child, &mut tail, false)?; + self.file_header[16..].copy_from_slice(&tail); + self.file_request(workspace, child)?; + if child { + self.process.at = 0; + } else { + self.process.parent_at = 0; + } + return Ok(Some(true)); + } + // A wrong size prefix cannot be a recoverable32-byte request. + // Reject it before reading any guessed tail or resynchronizing. + if !zsh_small_header(&frame, child) { + return Err(-22); + } + let mut tail = [0; 16]; + self.zsh_io(child, &mut tail, false)?; + if child { + self.process.request[16..].copy_from_slice(&tail); + self.process.at = 32; + } else { + self.request[16..].copy_from_slice(&tail); + self.process.parent_at = 32; + } + } + let frame = if child { + self.process.request + } else { + self.request + }; + if &frame[..8] == b"AEZFC001" { + if !child { + return Err(-13); + } + self.zsh_child_ready(&frame)?; + self.process.at = 0; + return Ok(Some(true)); + } + if self.zsh_stream(child, &frame)? { + if child { + self.process.at = 0; + } else { + self.process.parent_at = 0; + } + return Ok(Some(true)); + } + Ok(None) + } + #[cfg(native_zsh_process)] + fn request(&mut self, workspace: &mut [u8; 4096]) -> Result { + loop { + self.zsh_terminal()?; + if let Some(result) = self.zsh_poll(false, workspace)? { + return Ok(result); + } + if let Some(result) = self.zsh_poll(true, workspace)? { + return Ok(result); + } + self.streams.retire_idle([ + self.process.parent_at == 32, + self.process.at == 32, + false, + false, + ]); + self.yield_now()?; + } + } + fn terminal_zero(&mut self, handle: u64) -> Result<(), i64> { loop { let state = self.ctl(0x20001, handle, 0)?; @@ -1186,6 +2031,10 @@ impl Runtime { } pub fn run(&mut self, workspace: &mut [u8; 4096]) -> Result<(), i64> { let result = self.run_inner(workspace); + #[cfg(native_zsh_process)] + if result.is_err() { + self.process.lease.contain(); + } if let Err(error) = result && self.bound.failed == 0 { @@ -1197,6 +2046,17 @@ impl Runtime { self.provision(workspace)?; while self.request(workspace)? {} self.terminal_zero(self.app)?; + #[cfg(native_zsh_process)] + { + if self.process.binding.is_some() || self.process.lease != ReadLease::Parent { + return Err(-5); + } + self.flush_terminal_output()?; + let (terminal, refs, caller) = self.tty.as_mut().ok_or(-9)?; + for reference in [refs.stdin, refs.stdout, refs.stderr] { + terminal.close(*caller, reference).map_err(tty_errno)?; + } + } #[cfg(native_tty_mode_probe)] self.cleanup_mode()?; #[cfg(native_zsh_filesystem)] @@ -1256,6 +2116,25 @@ impl Runtime { if unsafe { self.files.assume_init_ref() }.cleanup() != Some([0; 2]) { return Err(-5); } + #[cfg(native_zsh_process)] + if self.process.wait_status != Some(1792) + || self.process.terminal != Some(1792) + || !self.process.committed + || !self.process.exec_ready + || !self.process.reclaimed + || self.process.lease != ReadLease::Parent + || self.process.binding.is_some() + || self + .process + .ends + .iter() + .any(|end| end.owned && (!end.attempted || end.status != 0)) + { + return Err(-5); + } + #[cfg(native_zsh_process)] + const ROW: &[u8] = b"\nAETHER ZSH OWNER v=2 requests=00 child_status=1792 exec_ready=1 child_reclaimed=1 parent_lease=1 mode_cleanup=0\n"; + #[cfg(not(native_zsh_process))] const ROW: &[u8; 49] = b"\nAETHER ZSH OWNER v=1 requests=00 mode_cleanup=0\n"; workspace[..ROW.len()].copy_from_slice(ROW); workspace[31] = b'0' + (self.filesystem_requests / 10) as u8; @@ -1326,6 +2205,33 @@ impl Runtime { return Err(-5); } self.cleanup_started = true; + #[cfg(native_zsh_process)] + { + self.process.lease.contain(); + let _ = self.zsh_close_child(); + if self.process.binding.is_some() { + if let Ok(status) = self.zsh_leaf_reserved(0x20007, 0) { + self.process.cleanup_attempted |= 1; + self.process.cleanup_last = status; + } + if let Ok(status) = self.zsh_leaf_reserved(0x20001, 0) { + self.process.cleanup_attempted |= 2; + self.process.cleanup_last = status; + if status.0 > 0 && matches!(status.2, 1..=4) { + if let Ok(status) = self.zsh_leaf_reserved(0x20002, 0) { + self.process.cleanup_attempted |= 4; + self.process.cleanup_last = status; + if status.0 == 0 { + if let Ok(status) = self.zsh_leaf_reserved(0x20001, 0) { + self.process.cleanup_attempted |= 8; + self.process.cleanup_last = status; + } + } + } + } + } + } + } #[cfg(native_zsh_filesystem)] if self.files_live { let _ = self.with_files(|files| { @@ -1368,6 +2274,117 @@ impl Runtime { Err(-5) } } +#[cfg(native_zsh_process)] +fn zsh_stream_plan(raw: &[u8; 336], expected: zsh_process_frames::Expected) -> Result { + zsh_process_frames::snapshot(raw, expected).map_err(i64::from)?; + let mut plan = Plan { + identity: expected.identity, + shell: if expected.identity.slot == 1 { + expected.identity.epoch + } else { + (expected.parent - 2) / 7 + }, + parent: expected.parent, + generation: expected.generation, + bindings: [Binding::default(); 16], + sources: [Source::default(); 7], + history: [0; 2], + }; + let mut held = [false; 7]; + let mut kept = [false; 7]; + for b in raw[112..176].chunks_exact(4) { + let mut mark = |selector: usize| { + held[selector] = true; + kept[selector] |= b[2] == 0; + }; + match b[0] { + 2 | 4 => mark(b[1] as usize), + 5 => { + mark(0); + mark(1); + } + 0 | 3 => (), + _ => return Err(-22), + } + } + // This is a stream-holder/CLOEXEC projection, never a serialized FD map. + // The complete raw map, including every duplex byte3, stays in workspace. + for i in 0..7 { + if held[i] { + plan.bindings[i] = Binding { + kind: if i < 3 { 1 } else { 2 }, + selector: i as u8, + cloexec: u8::from(!kept[i]), + }; + let at = 176 + i * 16; + plan.sources[i] = Source { + reference: u32::from_le_bytes(raw[at..at + 4].try_into().unwrap()), + selector: u32::from_le_bytes(raw[at + 4..at + 8].try_into().unwrap()), + rights: u32::from_le_bytes(raw[at + 8..at + 12].try_into().unwrap()), + bound: 16, + }; + } + } + plan.history = [ + u32::from_le_bytes(raw[288..292].try_into().unwrap()), + u32::from_le_bytes(raw[292..296].try_into().unwrap()), + ]; + Ok(plan) +} +#[cfg(native_zsh_process)] +fn zsh_rebind_snapshot( + raw: &mut [u8; 336], + id: Identity, + parent: u32, + generation: u64, +) -> Result<(), i64> { + let shell = if id.slot == 1 { + id.epoch + } else { + parent.checked_sub(2).ok_or(-9)? / 7 + }; + let greeting = abi29_dispatch_core::startup::greeting(id, shell, parent).map_err(i64::from)?; + raw[16..48].copy_from_slice(&greeting); + raw[56..64].copy_from_slice(&generation.to_le_bytes()); + raw[296..304].fill(0); + zsh_process_frames::snapshot( + raw, + zsh_process_frames::Expected { + identity: id, + parent, + generation, + }, + ) + .map_err(i64::from)?; + Ok(()) +} +#[cfg(native_zsh_process)] +fn zsh_exec_snapshot(raw: &mut [u8; 336]) -> Result<(), i64> { + for binding in raw[112..176].chunks_exact_mut(4) { + if binding[2] != 0 { + binding.fill(0); + } + } + let mut held = [false; 7]; + for binding in raw[112..176].chunks_exact(4) { + match binding[0] { + 2 | 4 => held[binding[1] as usize] = true, + 5 => { + held[0] = true; + held[1] = true; + } + 0 | 3 => (), + _ => return Err(-22), + } + } + for (i, used) in held.into_iter().enumerate() { + if !used { + raw[176 + i * 16..192 + i * 16].fill(0); + } + } + Ok(()) +} + struct OwnerSteps<'a, K: AppControl> { kernel: &'a mut K, bound: &'a mut Bound, @@ -1501,6 +2518,55 @@ impl serial_tty::SignalInput for SignalBackend<'_, K> { } } } +// Closed selected workload policy, after the wire codec checked canonical slots. +#[cfg(native_zsh_process)] +fn zsh_helper_arguments(raw: &[u8; 272]) -> bool { + u64::from_le_bytes(raw[..8].try_into().unwrap()) == 2 + && raw[8..16] == [0; 8] + && raw[16..30] == *b"/inputs/helper" + && raw[30..48] == [0; 18] + && raw[48..54] == *b"status" + && raw[54..] == [0; 218] +} +#[cfg(native_zsh_process)] +fn zsh_small_header(raw: &[u8; 32], child: bool) -> bool { + if &raw[..8] == b"AEZFC001" { + return child; + } + let op = u32_at(raw, 0); + let reference = u32_at(raw, 4); + let selector = u32_at(raw, 8); + let count = u32_at(raw, 12); + match op { + 1..=3 => { + let tagged = if selector < 3 { + reference == console(selector).reference + } else if selector < 7 { + let generation = (reference >> 8) & 255; + (1..=9).contains(&generation) + && reference + == 0x79000000 + | ((selector - 3) / 2) << 16 + | generation << 8 + | ((selector - 3) % 2 + 1) + } else { + false + }; + tagged && count <= 16 && (op != 3 || count == 0) + } + 82 => !child && reference == 0x76050001 && selector == 1 && count == 0, + 68 => { + !child + && (reference == u32::MAX + || (10..=108).contains(&reference) && (reference - 10) % 7 == 0) + && selector & !11 == 0 + && count == 0 + } + 0x10..=0x12 => reference == tty_mode::CONTROL && count == 0, + _ => false, + } +} +#[cfg(not(native_zsh_process))] fn output_header(frame: &[u8; 32]) -> bool { (matches!(u32_at(frame, 0), 2 | 3) || cfg!(any( @@ -1637,6 +2703,10 @@ fn fs_errno(error: filesystem_readonly::Error) -> i64 { #[cfg(all(test, native_zsh_filesystem))] extern crate std; -#[cfg(all(test, native_zsh_filesystem))] +#[cfg(all(test, native_zsh_filesystem, not(native_zsh_process)))] #[path = "zsh_owner_tests.rs"] mod zsh_owner_tests; + +#[cfg(all(test, native_zsh_process))] +#[path = "zsh_process_runtime_tests.rs"] +mod zsh_process_runtime_tests; diff --git a/sys-init/prototypes/native-filesystem-session/zsh_process_frames.rs b/sys-init/prototypes/native-filesystem-session/zsh_process_frames.rs new file mode 100644 index 0000000..0cfab47 --- /dev/null +++ b/sys-init/prototypes/native-filesystem-session/zsh_process_frames.rs @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: BSD-2-Clause +//! Closed 009 process snapshots. These inert bytes confer no native authority. +//! The caller supplies actual authenticated identity/channel observations. +use abi29_dispatch_core::startup::{Identity, greeting}; + +pub const STARTUP_BYTES: usize = 336; +pub const FORK_BYTES: usize = 368; +pub const EXEC_BYTES: usize = 672; + +#[derive(Clone, Copy)] +pub struct Expected { + pub identity: Identity, + pub parent: u32, + pub generation: u64, +} +#[derive(Clone, Copy)] +pub struct Snapshot<'a> { + raw: &'a [u8; STARTUP_BYTES], + expected: Expected, +} +impl Snapshot<'_> { + pub fn raw(&self) -> &[u8; STARTUP_BYTES] { + self.raw + } + pub fn identity(&self) -> Identity { + self.expected.identity + } + pub fn generation(&self) -> u64 { + self.expected.generation + } +} +fn u32_at(raw: &[u8], at: usize) -> u32 { + u32::from_le_bytes(raw[at..at + 4].try_into().unwrap()) +} +fn u64_at(raw: &[u8], at: usize) -> u64 { + u64::from_le_bytes(raw[at..at + 8].try_into().unwrap()) +} +fn sequence_ok(sequence: u64) -> bool { + sequence != 0 && sequence != u64::MAX +} + +/// Validate every field before returning a borrowed view. No partial publication. +pub fn snapshot(raw: &[u8], expected: Expected) -> Result, i32> { + let raw: &[u8; STARTUP_BYTES] = raw.try_into().map_err(|_| -22)?; + let id = expected.identity; + if !matches!(id.slot, 1 | 2) || expected.generation == 0 { + return Err(-9); + } + let shell = if id.slot == 1 { + id.epoch + } else { + expected.parent.checked_sub(2).ok_or(-9)? / 7 + }; + let identity = greeting(id, shell, expected.parent)?; + if &raw[..8] != b"AEFDB009" + || u32_at(raw, 8) != 1 + || u32_at(raw, 12) != 336 + || raw[16..48] != identity + || &raw[48..56] != b"AEFDT009" + || u64_at(raw, 56) != expected.generation + || u64_at(raw, 64) != 16 + || raw[72..112].iter().any(|v| *v != 0) + || u32_at(raw, 296) != if id.slot == 1 { 0x76050001 } else { 0 } + || u32_at(raw, 300) != if id.slot == 1 { 0x7d000001 } else { 0 } + || [304, 308, 312, 316] + .iter() + .any(|at| u32_at(raw, *at) != 1000) + || u32_at(raw, 320) != 1 + || raw[324..336].iter().any(|v| *v != 0) + { + return Err(-22); + } + let mut used = [false; 7]; + let mut files = [false; 16]; + for b in raw[112..176].chunks_exact(4) { + if b[2] > 1 { + return Err(-22); + } + match b[0] { + 0 if b[1..] == [0; 3] => (), + 2 if (3..=6).contains(&b[1]) && b[3] == 0 => used[b[1] as usize] = true, + 3 if (3..=15).contains(&b[1]) && b[3] == 0 => { + let slot = &mut files[b[1] as usize]; + if *slot { + return Err(-22); + } + *slot = true; + } + 4 if b[1] <= 2 && b[3] == 0 => used[b[1] as usize] = true, + 5 if b[1] == 0 && (1..=16).contains(&b[3]) => { + used[0] = true; + used[1] = true; + } + _ => return Err(-22), + } + } + let history = [u32_at(raw, 288), u32_at(raw, 292)]; + if history.iter().any(|v| *v > 9) { + return Err(-22); + } + for (i, held) in used.iter().enumerate() { + let at = 176 + i * 16; + if !*held { + if raw[at..at + 16].iter().any(|v| *v != 0) { + return Err(-9); + } + continue; + } + let reference = if i < 3 { + 0x7a000001 | (i as u32) << 16 + } else { + let generation = history[(i - 3) / 2]; + if generation == 0 { + return Err(-9); + } + 0x79000000 | (((i - 3) / 2) as u32) << 16 | generation << 8 | ((i - 3) % 2 + 1) as u32 + }; + if u32_at(raw, at) != reference + || u32_at(raw, at + 4) != i as u32 + || u32_at(raw, at + 8) != if matches!(i, 0 | 3 | 5) { 1 } else { 2 } + || u32_at(raw, at + 12) != 16 + { + return Err(-9); + } + } + Ok(Snapshot { raw, expected }) +} +fn header(raw: &[u8], magic: &[u8; 8], length: usize) -> Result { + if raw.len() != length + || &raw[..8] != magic + || u32_at(raw, 8) != 1 + || u32_at(raw, 12) != length as u32 + || !sequence_ok(u64_at(raw, 16)) + || u64_at(raw, 24) != 0 + { + return Err(-22); + } + Ok(u64_at(raw, 16)) +} +fn prefix(raw: &mut [u8], magic: &[u8; 8], sequence: u64) { + let length = raw.len() as u32; + raw[..32].fill(0); + raw[..8].copy_from_slice(magic); + raw[8..12].copy_from_slice(&1u32.to_le_bytes()); + raw[12..16].copy_from_slice(&length.to_le_bytes()); + raw[16..24].copy_from_slice(&sequence.to_le_bytes()); +} +pub fn fork_request(raw: &[u8], expected: Expected) -> Result<(Snapshot<'_>, u64), i32> { + let sequence = header(raw, b"AEZFK001", FORK_BYTES)?; + let view = snapshot(&raw[32..], expected)?; + if view.identity().slot != 1 { + return Err(-9); + } + Ok((view, sequence)) +} +pub fn encode_fork( + raw: &[u8; STARTUP_BYTES], + expected: Expected, + sequence: u64, + out: &mut [u8; FORK_BYTES], +) -> Result<(), i32> { + let view = snapshot(raw, expected)?; + if view.identity().slot != 1 || !sequence_ok(sequence) { + return Err(-22); + } + prefix(out, b"AEZFK001", sequence); + out[32..].copy_from_slice(raw); + Ok(()) +} +pub fn child_match<'a>( + request: &[u8], + parent: Expected, + startup: &'a [u8], + child: Expected, +) -> Result, i32> { + let (before, _) = fork_request(request, parent)?; + let after = snapshot(startup, child)?; + if after.identity().slot != 2 + || child.parent != before.identity().pid + || after.generation() == before.generation() + || before.raw[112..296] != after.raw[112..296] + || before.raw[304..336] != after.raw[304..336] + { + return Err(-9); + } + Ok(after) +} +fn arguments(raw: &[u8; 272]) -> Result<(), i32> { + let argc = u64_at(raw, 0); + let envc = u64_at(raw, 8); + if !(1..=4).contains(&argc) || envc > 4 { + return Err(-22); + } + for i in 0..8 { + let value = &raw[16 + i * 32..48 + i * 32]; + let active = i % 4 < if i < 4 { argc as usize } else { envc as usize }; + let end = if active { + value.iter().position(|v| *v == 0).ok_or(-22)? + } else { + 0 + }; + if value[end..].iter().any(|v| *v != 0) { + return Err(-22); + } + } + Ok(()) +} +pub fn exec_request(raw: &[u8], expected: Expected) -> Result<(Snapshot<'_>, u64), i32> { + let sequence = header(raw, b"AEZEX001", EXEC_BYTES)?; + let view = snapshot(&raw[32..368], expected)?; + if view.identity().slot != 2 { + return Err(-9); + } + let path = &raw[368..400]; + let end = path.iter().position(|v| *v == 0).ok_or(-22)?; + if end == 0 || path[end..].iter().any(|v| *v != 0) { + return Err(-22); + } + arguments(raw[400..].try_into().unwrap())?; + Ok((view, sequence)) +} +pub fn encode_exec( + raw: &[u8; STARTUP_BYTES], + expected: Expected, + sequence: u64, + path: &[u8], + args: &[u8; 272], + out: &mut [u8; EXEC_BYTES], +) -> Result<(), i32> { + let view = snapshot(raw, expected)?; + arguments(args)?; + if view.identity().slot != 2 + || !sequence_ok(sequence) + || path.is_empty() + || path.len() > 31 + || path.contains(&0) + { + return Err(-22); + } + prefix(out, b"AEZEX001", sequence); + out[32..368].copy_from_slice(raw); + out[368..400].fill(0); + out[368..368 + path.len()].copy_from_slice(path); + out[400..].copy_from_slice(args); + Ok(()) +} +fn child_pid(pid: i64) -> bool { + (10..=108).contains(&pid) && (pid - 3) % 7 == 0 +} +fn receipt_value(magic: &[u8; 8], result: i64) -> bool { + match magic { + b"AEZFR001" => child_pid(result) || (-4095..=-1).contains(&result), + b"AEZFC001" => child_pid(result), + b"AEZER001" => (-4095..=-1).contains(&result), + _ => false, + } +} +pub fn receipt(magic: &[u8; 8], sequence: u64, result: i64) -> Result<[u8; 32], i32> { + if !sequence_ok(sequence) || !receipt_value(magic, result) { + return Err(-22); + } + let mut out = [0; 32]; + out[..8].copy_from_slice(magic); + out[8..16].copy_from_slice(&sequence.to_le_bytes()); + out[16..24].copy_from_slice(&result.to_le_bytes()); + Ok(out) +} +pub fn decode_receipt(raw: &[u8], magic: &[u8; 8], sequence: u64) -> Result { + if raw.len() != 32 + || !sequence_ok(sequence) + || &raw[..8] != magic + || u64_at(raw, 8) != sequence + || u64_at(raw, 24) != 0 + { + return Err(-22); + } + let result = i64::from_le_bytes(raw[16..24].try_into().unwrap()); + if !receipt_value(magic, result) { + return Err(-22); + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + fn put(raw: &mut [u8], at: usize, value: u32) { + raw[at..at + 4].copy_from_slice(&value.to_le_bytes()); + } + fn fixture(child: bool) -> ([u8; 336], Expected) { + let identity = Identity::admitted( + if child { 2 } else { 1 }, + 1, + if child { 0xb0000103 } else { 0xb0000102 }, + ) + .unwrap(); + let expected = Expected { + identity, + parent: if child { 9 } else { 7 }, + generation: if child { 32 } else { 31 }, + }; + let mut raw = [0; 336]; + raw[..8].copy_from_slice(b"AEFDB009"); + put(&mut raw, 8, 1); + put(&mut raw, 12, 336); + raw[16..48].copy_from_slice(&greeting(identity, 1, expected.parent).unwrap()); + raw[48..56].copy_from_slice(b"AEFDT009"); + raw[56..64].copy_from_slice(&expected.generation.to_le_bytes()); + raw[64..72].copy_from_slice(&16u64.to_le_bytes()); + for i in 0..3 { + raw[112 + i * 4] = 4; + raw[113 + i * 4] = i as u8; + put(&mut raw, 176 + i * 16, 0x7a000001 | (i as u32) << 16); + put(&mut raw, 180 + i * 16, i as u32); + put(&mut raw, 184 + i * 16, if i == 0 { 1 } else { 2 }); + put(&mut raw, 188 + i * 16, 16); + } + raw[124] = 2; + raw[125] = 3; + raw[128] = 2; + raw[129] = 4; + raw[132] = 3; + raw[133] = 3; + raw[152] = 5; + raw[155] = 1; + for i in 3..5 { + put(&mut raw, 176 + i * 16, 0x79000100 | (i as u32 - 2)); + put(&mut raw, 180 + i * 16, i as u32); + put(&mut raw, 184 + i * 16, if i == 3 { 1 } else { 2 }); + put(&mut raw, 188 + i * 16, 16); + } + put(&mut raw, 288, 1); + put(&mut raw, 296, if child { 0 } else { 0x76050001 }); + put(&mut raw, 300, if child { 0 } else { 0x7d000001 }); + for at in [304, 308, 312, 316] { + put(&mut raw, at, 1000); + } + put(&mut raw, 320, 1); + (raw, expected) + } + #[test] + fn selected_startup_producer_encodes_complete_role_metadata() { + for child in [false, true] { + let (_, expected) = fixture(child); + let mut plan = fd_startup::Plan { + identity: expected.identity, + shell: 1, + parent: expected.parent, + generation: expected.generation, + bindings: [fd_startup::Binding::default(); 16], + sources: [fd_startup::Source::default(); 7], + history: [0; 2], + }; + for i in 0..3 { + plan.bindings[i] = fd_startup::Binding { + kind: 4, + selector: i as u8, + cloexec: 0, + }; + plan.sources[i] = fd_startup::Source { + reference: 0x7a000001 | (i as u32) << 16, + selector: i as u32, + rights: if i == 0 { 1 } else { 2 }, + bound: 16, + }; + } + let mut out = [0; 336]; + fd_startup::encode(&plan, &mut out).unwrap(); + snapshot(&out, expected).unwrap(); + assert_eq!(u32_at(&out, 296), if child { 0 } else { 0x76050001 }); + assert_eq!(u32_at(&out, 300), if child { 0 } else { 0x7d000001 }); + } + } + #[test] + fn whole_startup_roles_aliases_pipe_and_old_version_denials() { + for child in [false, true] { + let (raw, expected) = fixture(child); + let checked = snapshot(&raw, expected).unwrap(); + assert_eq!(checked.raw(), &raw); + for n in 0..336 { + assert!(snapshot(&raw[..n], expected).is_err()); + } + for at in [7, 55, 72, 115, 155, 176, 288, 296, 300, 304, 320, 324, 335] { + let mut bad = raw; + bad[at] ^= 0x80; + assert!(snapshot(&bad, expected).is_err(), "byte{at}"); + } + } + } + #[test] + fn fork_full_snapshot_sequence_and_child_correspondence() { + let (raw, parent) = fixture(false); + let (child_raw, child) = fixture(true); + let mut out = [0; 368]; + encode_fork(&raw, parent, 41, &mut out).unwrap(); + assert_eq!(&out[32..], &raw); + assert_eq!(fork_request(&out, parent).unwrap().1, 41); + child_match(&out, parent, &child_raw, child).unwrap(); + for n in 0..368 { + assert!(fork_request(&out[..n], parent).is_err()); + } + for seq in [0, u64::MAX] { + let mut bad = out; + bad[16..24].copy_from_slice(&seq.to_le_bytes()); + assert!(fork_request(&bad, parent).is_err()); + } + let mut changed = child_raw; + changed[154] = 1; + assert!(child_match(&out, parent, &changed, child).is_err()); + } + #[test] + fn exec_full_snapshot_arguments_padding_roles_and_atomic_failure() { + let (raw, child) = fixture(true); + let (parent_raw, parent) = fixture(false); + let mut args = [0; 272]; + args[..8].copy_from_slice(&2u64.to_le_bytes()); + args[16..30].copy_from_slice(b"/inputs/helper"); + args[48..54].copy_from_slice(b"status"); + let mut out = [0; 672]; + encode_exec(&raw, child, 42, b"/inputs/helper", &args, &mut out).unwrap(); + assert_eq!(&out[32..368], &raw); + assert_eq!(&out[400..], &args); + exec_request(&out, child).unwrap(); + for n in 0..672 { + assert!(exec_request(&out[..n], child).is_err()); + } + let mut bad = out; + bad[671] = 1; + assert!(exec_request(&bad, child).is_err()); + let saved = out; + assert!(encode_exec(&parent_raw, parent, 42, b"/inputs/helper", &args, &mut out).is_err()); + assert_eq!(out, saved); + let stale = Expected { + generation: 33, + ..child + }; + assert!(exec_request(&out, stale).is_err()); + } + #[test] + fn typed_receipts_reject_wrong_role_sequence_version_and_padding() { + for (magic, result) in [(b"AEZFR001", 10), (b"AEZFC001", 10), (b"AEZER001", -13)] { + let raw = receipt(magic, 43, result).unwrap(); + assert_eq!(decode_receipt(&raw, magic, 43), Ok(result)); + assert!(decode_receipt(&raw, magic, 44).is_err()); + let mut bad = raw; + bad[31] = 1; + assert!(decode_receipt(&bad, magic, 43).is_err()); + } + assert!(receipt(b"AEZER001", 43, 7).is_err()); + assert!(receipt(b"AEZFC001", 43, -13).is_err()); + assert!(receipt(b"AEZFR001", 43, 11).is_err()); + } +} diff --git a/sys-init/prototypes/native-filesystem-session/zsh_process_runtime_tests.rs b/sys-init/prototypes/native-filesystem-session/zsh_process_runtime_tests.rs new file mode 100644 index 0000000..f60678c --- /dev/null +++ b/sys-init/prototypes/native-filesystem-session/zsh_process_runtime_tests.rs @@ -0,0 +1,1063 @@ +// SPDX-License-Identifier: BSD-2-Clause +//! Focused actual Runtime fixtures with a mocked native boundary and real AF09 +//! Client/service, descriptor cores, pipes and child ledger. No native execution. +//! Compile native_app_runtime.rs with rustc --test and its existing selected +//! dependency rlibs; the qualified private invocation is retained in #143 evidence. +use super::*; +use af07_transport::Kernel; +use filesystem_policy as p; +use filesystem_service::Channel; +use std::{ + boxed::Box, + cell::RefCell, + collections::{BTreeMap, VecDeque}, + rc::Rc, + sync::mpsc, + thread, + time::Duration, + vec::Vec, +}; +const NSR: u64 = 0xd1000101; +const NSW: u64 = 0xd1000102; +const PR: u64 = 0xd1010101; +const PW: u64 = 0xd1010102; +const PARENT: u64 = 0xb0000102; +const CHILD: u64 = 0xb0000103; +#[derive(Default)] +struct Storage { + made: [bool; 3], + data: Vec, + mutations: u64, +} +fn node(id: u64, size: usize) -> p::Node { + p::Node::new( + id, + 1, + size as u64, + if id == 5 { + p::Kind::File + } else { + p::Kind::Directory + }, + ) +} +impl p::Backend for Storage { + fn root(&mut self) -> Result { + Ok(node(1, 0)) + } + fn directory(&mut self, n: p::Node) -> Result { + Ok(n) + } + fn lookup(&mut self, n: p::Node, s: &[u8]) -> Result { + let id = match (n.identity().0, s) { + (1, b"home") => 2, + (2, b".terminfo") if self.made[0] => 3, + (3, b"v") if self.made[1] => 4, + (4, b"vt100") if self.made[2] => 5, + _ => return Err(p::Error::Backend(2)), + }; + Ok(node(id, if id == 5 { self.data.len() } else { 0 })) + } + fn read(&mut self, _: p::Node, at: u64, out: &mut [u8]) -> Result { + let at = (at as usize).min(self.data.len()); + let n = out.len().min(self.data.len() - at); + out[..n].copy_from_slice(&self.data[at..at + n]); + Ok(n) + } + fn write( + &mut self, + n: p::Node, + at: u64, + bytes: &[u8], + _: p::Utc, + ) -> Result { + assert_eq!((n.identity().0, at), (5, 0)); + self.data = bytes.to_vec(); + self.mutations += 1; + Ok(p::Completion { + error: 0, + txg: self.mutations, + bytes_staged: bytes.len() as u64, + state: 2, + }) + } + fn mutate( + &mut self, + n: p::Node, + op: p::Namespace<'_>, + _: p::Utc, + ) -> Result { + let i = match (n.identity().0, op) { + ( + 2, + p::Namespace::MakeDirectory { + name: b".terminfo", .. + }, + ) => 0, + (3, p::Namespace::MakeDirectory { name: b"v", .. }) => 1, + (4, p::Namespace::Create { name: b"vt100", .. }) => 2, + _ => panic!("UNAUTHORIZED_MUTATION"), + }; + assert!(!self.made[i]); + self.made[i] = true; + self.mutations += 1; + Ok(p::Completion { + error: 0, + txg: self.mutations, + bytes_staged: 0, + state: 2, + }) + } +} +struct Time; +impl p::Clock for Time { + fn utc(&mut self) -> Result { + Ok(p::Utc { + seconds: 1, + nanoseconds: 0, + }) + } +} +struct ServicePipe { + tx: mpsc::Sender<[u8; 64]>, + rx: mpsc::Receiver<[u8; 64]>, +} +impl Channel for ServicePipe { + fn send(&mut self, b: &[u8; 64]) -> Result<(), i64> { + self.tx.send(*b).map_err(|_| -32) + } + fn receive(&mut self, b: &mut [u8; 64]) -> Result<(), i64> { + *b = self + .rx + .recv_timeout(Duration::from_secs(2)) + .map_err(|_| -32)?; + Ok(()) + } + fn close(&mut self) -> [i64; 2] { + [0; 2] + } +} +struct Host { + tx: Option>, + rx: mpsc::Receiver<[u8; 64]>, + send: [u8; 64], + send_at: usize, + receive: [u8; 64], + receive_at: usize, + input: BTreeMap>, + output: BTreeMap>, + calls: Vec<(u64, u64, u64)>, + closed: Vec, + pair: u64, + copy_result: i64, + exec_result: i64, + receipt: Status, + ready: bool, + terminal: Option, + gone: bool, + parent_terminal: bool, + parent_gone: bool, + stop_result: Status, + stop_observation: Status, + serial_reads: usize, + fail_write: Option<(u64, usize)>, + written: usize, +} +#[derive(Clone)] +struct Mock(Rc>); +impl Kernel for Mock { + fn transfer(&mut self, cap: u64, bytes: &mut [u8], writing: bool) -> i64 { + let mut h = self.0.borrow_mut(); + if cap == NSW && writing { + let at = h.send_at; + h.send[at..at + bytes.len()].copy_from_slice(bytes); + h.send_at += bytes.len(); + if h.send_at == 64 { + h.tx.as_ref().unwrap().send(h.send).unwrap(); + h.send_at = 0; + } + return bytes.len() as i64; + } + if cap == NSR && !writing { + if h.receive_at == 64 { + h.receive = h.rx.recv_timeout(Duration::from_secs(2)).unwrap(); + h.receive_at = 0; + } + let at = h.receive_at; + bytes.copy_from_slice(&h.receive[at..at + bytes.len()]); + h.receive_at += bytes.len(); + return bytes.len() as i64; + } + if writing { + if let Some((target, limit)) = h.fail_write { + if cap == target && h.written >= limit { + return -5; + } + } + h.output.entry(cap).or_default().extend_from_slice(bytes); + h.written += bytes.len(); + return bytes.len() as i64; + } + let q = h.input.entry(cap).or_default(); + if q.is_empty() { + return -11; + } + let n = bytes.len().min(q.len()); + for b in &mut bytes[..n] { + *b = q.pop_front().unwrap(); + } + n as i64 + } + fn wait(&mut self, _: u64) -> i64 { + 0 + } + fn close(&mut self, cap: u64) -> i64 { + let mut h = self.0.borrow_mut(); + assert!(!h.closed.contains(&cap), "NO_CLOSE_REPLAY"); + h.closed.push(cap); + if cap == NSW { + h.tx = None; + } + 0 + } +} +impl Control for Mock { + fn observe_events(&mut self) -> Status { + (0, 1, 1) + } + fn park_events(&mut self, _: u64, _: Option) -> Status { + (0, 1, 1) + } + fn ctl(&mut self, op: u64, cap: u64, arg: u64) -> Status { + let mut h = self.0.borrow_mut(); + h.calls.push((op, cap, arg)); + match op { + 0x24000 => { + let id = h.pair; + h.pair += 1; + ((0xd1000101 | id << 16) as i64, 0xd1000102 | id << 16, 0) + } + 0x23004 => (101, 0, 0), + 0x23005 | 0x1fffe => (0, 0, 0), + 0x20001 if cap == PARENT => { + if h.parent_gone { + (-9, 0, 0) + } else if h.parent_terminal { + (144, 0, 3) + } else { + (0, 1, 0) + } + } + 0x20007 if cap == PARENT => { + h.parent_terminal = true; + (0, 0, 0) + } + 0x20002 if cap == PARENT => { + assert!(h.parent_terminal); + h.parent_gone = true; + (0, 0, 0) + } + 0x20007 if cap == CHILD => { + if h.stop_result.0 == 0 && h.terminal.is_none() { + h.terminal = Some(h.stop_observation); + } + h.stop_result + } + 0x26002 if cap == 0xf001 => (0, 0, 0), + 0x100e7 if cap == 125 => (0, 0, 0), + 0x20001 if cap == CHILD => { + if h.gone { + (-9, 0, 0) + } else { + h.terminal.unwrap_or((0, u64::from(h.ready), 0)) + } + } + 0x20002 if cap == CHILD => { + h.gone = true; + (0, 0, 0) + } + _ => panic!("UNEXPECTED_NATIVE_CONTROL {op:x} {cap:x} {arg:x}"), + } + } + fn engine(&mut self, _: u64, _: u64) -> i64 { + panic!("NO_ENGINE_LAUNCH") + } + fn delegate(&mut self, _: u64, _: &[u64; 3]) -> i64 { + panic!("NO_DELEGATION") + } + fn launch(&mut self, _: &[u8; 272], _: u64, _: u64) -> i64 { + panic!("NO_LEGACY_LAUNCH") + } + fn copy(&mut self, parent: u64, slot: u32, token: u64, reader: u64, writer: u64) -> i64 { + let mut h = self.0.borrow_mut(); + assert_eq!((parent, slot, token), (PARENT, 2, 101)); + h.calls.push((0x23006, reader, writer)); + h.copy_result + } + fn serial(&mut self, writing: bool, bytes: &mut [u8]) -> i64 { + let mut h = self.0.borrow_mut(); + if writing { + bytes.len() as i64 + } else { + h.serial_reads += 1; + -11 + } + } + fn exec(&mut self, process: u64, token: u64, args: &[u8; 272]) -> i64 { + assert_eq!((process, token), (CHILD, 101)); + let mut h = self.0.borrow_mut(); + h.calls.push((0x23007, process, token)); + assert_eq!(u64::from_le_bytes(args[..8].try_into().unwrap()), 2); + if h.exec_result == 0 { + h.receipt = (101, 0xc003, 1); + h.ready = false; + } + h.exec_result + } + fn receipt(&mut self, process: u64) -> Status { + assert_eq!(process, CHILD); + self.0.borrow().receipt + } + fn publish(&mut self, process: u64, token: u64) -> i64 { + assert_eq!((process, token), (CHILD, 101)); + self.0.borrow_mut().calls.push((0x23009, process, token)); + 0 + } +} +impl AppControl for Mock { + fn launch_app(&mut self, _: &[u8; 320], _: u64, _: u64) -> i64 { + panic!("NO_NEW_APP") + } +} +struct Fixture { + runtime: Box>, + host: Mock, + worker: Option>, +} +impl Fixture { + fn new() -> Self { + let (a, b) = mpsc::channel(); + let (c, d) = mpsc::channel(); + let worker = thread::spawn(move || { + let mut owner = p::Owner::new(Storage::default(), Time, 91) + .unwrap() + .with_namespace_identity(p::Identity::new(1000, 1000).unwrap()); + let attached = owner.attach(7, p::Rights::new(15).unwrap()).unwrap(); + let home = owner + .lookup( + 7, + attached.session, + attached.root.handle, + b"home", + p::Rights::new(15).unwrap(), + ) + .unwrap(); + filesystem_service::run_home( + owner, + ServicePipe { tx: c, rx: b }, + 7, + attached.session, + home.handle, + 8, + 91, + ) + }); + let host = Mock(Rc::new(RefCell::new(Host { + tx: Some(a), + rx: d, + send: [0; 64], + send_at: 0, + receive: [0; 64], + receive_at: 64, + input: BTreeMap::new(), + output: BTreeMap::new(), + calls: Vec::new(), + closed: Vec::new(), + pair: 2, + copy_result: CHILD as i64, + exec_result: 0, + receipt: (0, 0, 0), + ready: false, + terminal: None, + gone: false, + parent_terminal: false, + parent_gone: false, + stop_result: (0, 0, 0), + stop_observation: (144, 0, 3), + serial_reads: 0, + fail_write: None, + written: 0, + }))); + let mut runtime = Box::new(Runtime::new(host.clone()).unwrap()); + runtime.app = PARENT; + runtime.identity = Some(Identity::admitted(1, 1, PARENT).unwrap()); + runtime.endpoints = Some(ManuallyDrop::new( + Endpoints::new(host.clone(), PR, PW).ok().unwrap(), + )); + let caller = serial_tty::Caller { + identity: PARENT, + generation: 1, + }; + let (mut tty, refs) = Terminal::admit( + PARENT, + caller, + [ + Rights::READ.union(Rights::MODE), + Rights::WRITE, + Rights::WRITE, + ], + Mode::IMMEDIATE, + ) + .unwrap(); + runtime.mode = Some(tty_mode::State::admit(&mut tty, &refs, caller).unwrap()); + runtime.tty = Some((tty, refs, caller)); + for (end, cap) in runtime.ends[..2].iter_mut().zip([NSR, NSW]) { + *end = End { + cap, + owned: true, + ..End::default() + }; + } + // These are the two Owner-held Parent process endpoints. Child cleanup + // must not borrow their existing global close/reclaim reserve. + for (end, cap) in runtime.ends[2..4].iter_mut().zip([PR, PW]) { + *end = End { + cap, + owned: true, + ..End::default() + }; + } + runtime.namespace_prepaid = [NSR, NSW]; + let binding = filesystem_readonly::Binding { + slot: 1, + epoch: 1, + handle: PARENT, + generation: PR, + }; + let mut channel = Namespace::new(host.clone(), NSR, NSW); + // SAFETY: disjoint loans remain fixed through synchronous final-slot connect. + unsafe { + channel + .arm( + &mut runtime.bound, + &mut runtime.ends, + &mut runtime.filesystem_requests, + &mut runtime.namespace_prepaid, + ) + .unwrap(); + } + filesystem_readonly::Files::connect_in_place(&mut runtime.files, channel, 91, binding) + .unwrap() + .configure(|io| { + io.disarm(); + Ok(()) + }) + .unwrap(); + runtime.files_live = true; + runtime.file_binding = Some(binding); + runtime + .with_files(|files| files.provision(TERMINFO, &mut [0; 4096])) + .unwrap(); + Self { + runtime, + host, + worker: Some(worker), + } + } + fn feed(&self, cap: u64, bytes: &[u8]) { + self.host + .0 + .borrow_mut() + .input + .entry(cap) + .or_default() + .extend(bytes); + } + fn output(&self, cap: u64) -> Vec { + self.host + .0 + .borrow() + .output + .get(&cap) + .cloned() + .unwrap_or_default() + } + fn clear(&self) { + self.host.0.borrow_mut().output.clear(); + self.host.0.borrow_mut().written = 0; + } + fn snapshot(&self, pipe: bool) -> [u8; 336] { + let r = &self.runtime; + let id = r.identity.unwrap(); + let mut p = Plan { + identity: id, + shell: 1, + parent: 7, + generation: PR, + bindings: [Binding::default(); 16], + sources: [Source::default(); 7], + history: [0; 2], + }; + for i in 0..3 { + p.bindings[i] = Binding { + kind: 4, + selector: i as u8, + cloexec: 0, + }; + p.sources[i] = console(i as u32); + } + if pipe { + p.history[0] = 1; + for i in 3..5 { + p.bindings[i] = Binding { + kind: 2, + selector: i as u8, + cloexec: 1, + }; + p.sources[i] = Source { + reference: 0x79000101 + (i as u32 - 3), + selector: i as u32, + rights: if i == 3 { 1 } else { 2 }, + bound: 16, + }; + } + } + let mut out = [0; 336]; + fd_startup::encode(&p, &mut out).unwrap(); + out + } + fn fork(&mut self, pipe: bool) -> [u8; 336] { + if pipe { + let q = frame(82, 0x76050001, 1, 0); + assert!(self.runtime.zsh_stream(false, &q).unwrap()); + self.clear(); + } + let raw = self.snapshot(pipe); + let mut request = [0; 368]; + zsh_process_frames::encode_fork( + &raw, + self.runtime.zsh_expected(false).unwrap(), + 1, + &mut request, + ) + .unwrap(); + let mut work = [0; 4096]; + work[..368].copy_from_slice(&request); + self.runtime.zsh_fork(&mut work).unwrap(); + let child_writer = self.runtime.process.ends[3].cap; + let child: [u8; 336] = self.output(child_writer).try_into().unwrap(); + self.clear(); + let ready = zsh_process_frames::receipt(b"AEZFC001", 1, 10).unwrap(); + self.runtime.zsh_child_ready(&ready).unwrap(); + assert_eq!( + zsh_process_frames::decode_receipt(&self.output(PW), b"AEZFR001", 1).unwrap(), + 10 + ); + self.clear(); + child + } + fn exec_request(&self, raw: &[u8; 336]) -> [u8; 4096] { + let mut work = [0; 4096]; + work[..8].copy_from_slice(b"AEZEX001"); + put(&mut work, 8, 1); + put(&mut work, 12, 672); + work[16..24].copy_from_slice(&2u64.to_le_bytes()); + work[32..368].copy_from_slice(raw); + work[368..382].copy_from_slice(b"/inputs/helper"); + work[400..408].copy_from_slice(&2u64.to_le_bytes()); + work[416..430].copy_from_slice(b"/inputs/helper"); + work[448..454].copy_from_slice(b"status"); + work + } +} +impl Drop for Fixture { + fn drop(&mut self) { + let _ = self.runtime.with_files(|files| { + assert_eq!(files.abort(), [0; 2]); + Ok(()) + }); + self.host.0.borrow_mut().tx = None; + self.worker.take().unwrap().join().unwrap(); + } +} +fn put(raw: &mut [u8], at: usize, n: u32) { + raw[at..at + 4].copy_from_slice(&n.to_le_bytes()); +} +fn frame(op: u32, reference: u32, selector: u32, n: u32) -> [u8; 32] { + let mut q = [0; 32]; + for (at, v) in [(0, op), (4, reference), (8, selector), (12, n)] { + put(&mut q, at, v); + } + q +} +fn result(raw: &[u8]) -> i32 { + i32::from_le_bytes(raw[..4].try_into().unwrap()) +} +#[test] +fn unknown_header_is_fatal_before_tail_consumption() { + let mut h = Fixture::new(); + let mut q = [0xa5; 32]; + q[..16].fill(0); + put(&mut q, 0, 0x9911); + h.feed(PR, &q); + assert_eq!( + h.runtime.zsh_poll(false, &mut [0; 4096]), + Err(-22), + "UNKNOWN_HEADER_MUST_NOT_RESYNC" + ); + assert_eq!(h.host.0.borrow().input[&PR].len(), 16); + assert!(h.output(PW).is_empty()); +} +#[test] +fn exact_helper_arguments_are_known_refusal_before_exec() { + for changed in [0, 1, 2, 3] { + let mut h = Fixture::new(); + let raw = h.fork(false); + let mut work = h.exec_request(&raw); + match changed { + 0 => work[448] = b'x', + 1 => { + work[408..416].copy_from_slice(&1u64.to_le_bytes()); + work[544..548].copy_from_slice(b"X=1\0"); + } + 2 => work[416] = b'x', + _ => { + work[400..408].copy_from_slice(&1u64.to_le_bytes()); + work[448..480].fill(0); + } + } + h.runtime.zsh_exec(&mut work).unwrap(); + assert!( + !h.host.0.borrow().calls.iter().any(|x| x.0 == 0x23007), + "EXACT_HELPER_VECTOR_BEFORE_NATIVE_EXEC" + ); + let writer = h.runtime.process.ends[3].cap; + assert_eq!( + zsh_process_frames::decode_receipt(&h.output(writer), b"AEZER001", 2).unwrap(), + -13 + ); + assert!(!h.runtime.process.committed); + assert_eq!( + h.runtime.process.binding.unwrap().generation, + h.runtime.process.ends[0].cap + ); + assert!(h.host.0.borrow().calls.contains(&(0x23005, CHILD, 101))); + } +} +#[test] +fn real_pipe_exec_ready_wait_delivery_reclaim_and_parent_lease() { + let mut h = Fixture::new(); + let raw = h.fork(true); + let pipe_read = frame(1, 0x79000101, 3, 1); + let mut pipe_write = frame(2, 0x79000102, 4, 1); + pipe_write[16] = b'!'; + assert!(!h.runtime.zsh_stream(false, &pipe_read).unwrap()); + assert!(h.runtime.zsh_stream(true, &pipe_write).unwrap()); + assert!(h.runtime.zsh_stream(false, &pipe_read).unwrap()); + assert_eq!(&h.output(PW)[16..17], b"!"); + h.clear(); + let input = frame(1, 0x7a000001, 0, 1); + assert!(!h.runtime.zsh_stream(false, &input).unwrap()); + assert_eq!(h.host.0.borrow().serial_reads, 0); + let mut work = h.exec_request(&raw); + h.runtime.zsh_exec(&mut work).unwrap(); + assert!(!h.runtime.process.exec_ready); + let writer = h.runtime.process.ends[3].cap; + let fresh: [u8; 336] = h.output(writer).try_into().unwrap(); + assert_eq!(&fresh[112 + 3 * 4..112 + 5 * 4], &[0; 8]); + assert_eq!(h.runtime.process.identity().unwrap().handle, CHILD); + assert_eq!( + u64::from_le_bytes(fresh[56..64].try_into().unwrap()), + h.runtime.process.ends[0].cap + 1 + ); + h.runtime.zsh_terminal().unwrap(); + assert!(!h.runtime.process.exec_ready); + h.host.0.borrow_mut().ready = true; + h.runtime.zsh_terminal().unwrap(); + assert!(h.runtime.process.exec_ready); + h.host.0.borrow_mut().terminal = Some((8, 1, 1)); + h.runtime.zsh_terminal().unwrap(); + assert_eq!( + h.runtime.process.lease, + ReadLease::Child(h.runtime.process.child().unwrap()) + ); + h.clear(); + let wait = frame(68, u32::MAX, 0, 0); + assert!(h.runtime.zsh_stream(false, &wait).unwrap()); + let reply = h.output(PW); + assert_eq!((result(&reply), result(&reply[4..])), (10, 1792)); + assert_eq!(&reply[8..], &[0; 24]); + assert_eq!(h.runtime.process.wait_status, Some(1792)); + assert_eq!(h.runtime.process.lease, ReadLease::Parent); + assert!(h.runtime.process.reclaimed); + assert!(h.host.0.borrow().gone); + assert!(h.runtime.process.binding.is_none()); + assert_eq!(h.runtime.bound.reserved, RESERVED); + h.clear(); + assert!(h.runtime.zsh_stream(false, &wait).unwrap()); + assert_eq!(result(&h.output(PW)), -10); +} +#[test] +fn child_canonical_read_and_mode_are_rejected_without_input() { + let mut h = Fixture::new(); + h.fork(false); + let (tty, refs, caller) = h.runtime.tty.as_mut().unwrap(); + let mut saved = tty.snapshot(*caller, &refs.stdin).unwrap(); + tty.apply_now( + *caller, + &refs.stdin, + &mut saved, + serial_tty::ModeRequest { + mode: Mode::CANONICAL, + action: serial_tty::ApplyWhen::Now, + }, + ) + .unwrap(); + let reader = frame(1, 0x7a000001, 0, 1); + assert!(h.runtime.zsh_stream(true, &reader).unwrap()); + let writer = h.runtime.process.ends[3].cap; + assert_eq!(result(&h.output(writer)), -95); + assert_eq!(h.host.0.borrow().serial_reads, 0); + h.clear(); + let mode = frame(0x10, tty_mode::CONTROL, 0, 0); + assert!(h.runtime.zsh_stream(true, &mode).unwrap()); + assert_eq!(result(&h.output(writer)), -95); +} +#[test] +fn known_exec_refusal_preserves_old_context_and_unknown_exec_never_publishes() { + for error in [-2, 1] { + let mut h = Fixture::new(); + let raw = h.fork(true); + h.host.0.borrow_mut().exec_result = error; + let old = h.runtime.process.binding; + let mut work = h.exec_request(&raw); + let observed = h.runtime.zsh_exec(&mut work); + if error < 0 { + assert_eq!(observed, Ok(())); + let writer = h.runtime.process.ends[3].cap; + assert_eq!( + zsh_process_frames::decode_receipt(&h.output(writer), b"AEZER001", 2).unwrap(), + -2 + ); + assert!( + h.runtime + .zsh_stream(true, &frame(2, 0x79000102, 4, 0)) + .unwrap() + ); + } else { + assert_eq!(observed, Err(-5)); + assert!(!h.host.0.borrow().calls.contains(&(0x23005, CHILD, 101))); + } + assert_eq!(h.runtime.process.binding, old); + assert!(!h.runtime.process.committed); + assert!(!h.host.0.borrow().calls.iter().any(|r| r.0 == 0x23009)); + } +} +#[test] +fn failed_wait_delivery_keeps_reserved_status_child_lease_and_no_reclaim() { + let mut h = Fixture::new(); + let raw = h.fork(false); + let mut work = h.exec_request(&raw); + h.runtime.zsh_exec(&mut work).unwrap(); + h.host.0.borrow_mut().ready = true; + h.runtime.zsh_terminal().unwrap(); + h.host.0.borrow_mut().terminal = Some((8, 1, 1)); + h.runtime.zsh_terminal().unwrap(); + h.clear(); + h.host.0.borrow_mut().fail_write = Some((PW, 16)); + let wait = frame(68, u32::MAX, 1, 0); + assert_eq!(h.runtime.zsh_stream(false, &wait), Err(-5)); + assert_eq!(h.output(PW).len(), 16); + assert_eq!(h.runtime.process.wait_status, None); + assert!(!h.host.0.borrow().gone); + assert!(!h.runtime.process.reclaimed); + assert_ne!(h.runtime.process.lease, ReadLease::Parent); + assert_eq!(h.runtime.process.children.wait(-1, true), Err(-16)); +} +#[test] +fn stale_malformed_copy_unknown_and_reserve_exhaustion_have_no_fabricated_commit() { + for mode in 0..4 { + let mut h = Fixture::new(); + let raw = h.snapshot(false); + let expected = h.runtime.zsh_expected(false).unwrap(); + let mut fork = [0; 368]; + zsh_process_frames::encode_fork(&raw, expected, 1, &mut fork).unwrap(); + match mode { + 0 => fork[32 + 56] ^= 1, + 1 => fork[24] = 1, + 2 => h.runtime.bound.left = 5, + _ => h.host.0.borrow_mut().copy_result = 1, + } + let mut work = [0; 4096]; + work[..368].copy_from_slice(&fork); + assert!(h.runtime.zsh_fork(&mut work).is_err()); + assert_eq!(h.runtime.bound.reserved, RESERVED); + if mode < 3 { + assert!(!h.host.0.borrow().calls.iter().any(|v| v.0 == 0x23006)); + } else { + assert_eq!(h.runtime.process.ends.iter().filter(|e| e.owned).count(), 2); + assert!(h.host.0.borrow().closed.is_empty()); + } + } +} +#[test] +fn full_duplex_snapshot_keeps_byte3_and_aggregate_cloexec_projection() { + let h = Fixture::new(); + let mut raw = h.snapshot(false); + raw[112..116].copy_from_slice(&[5, 0, 1, 16]); + raw[116..120].fill(0); + let expected = h.runtime.zsh_expected(false).unwrap(); + let plan = zsh_stream_plan(&raw, expected).unwrap(); + assert_eq!(plan.bindings[0].kind, 1); + assert_eq!(plan.bindings[1].kind, 1); + assert_eq!(raw[115], 16); + let id = Identity::admitted(2, 1, CHILD).unwrap(); + zsh_rebind_snapshot(&mut raw, id, 9, 700).unwrap(); + assert_eq!(raw[115], 16); + zsh_exec_snapshot(&mut raw).unwrap(); + assert_eq!(&raw[112..120], &[0; 8]); +} + +#[test] +fn fixed_catalog_missing_and_all_other_nonhelper_paths_preserve_old_image() { + for (path, code) in [ + (b"/inputs/missing".as_slice(), -2), + (b"/inputs/denied".as_slice(), -13), + (b"/inputs/other".as_slice(), -13), + ] { + let mut h = Fixture::new(); + let raw = h.fork(false); + let mut work = h.exec_request(&raw); + work[368..400].fill(0); + work[368..368 + path.len()].copy_from_slice(path); + h.runtime.zsh_exec(&mut work).unwrap(); + assert!(!h.host.0.borrow().calls.iter().any(|x| x.0 == 0x23007)); + let writer = h.runtime.process.ends[3].cap; + assert_eq!( + zsh_process_frames::decode_receipt(&h.output(writer), b"AEZER001", 2).unwrap(), + code + ); + assert!(!h.runtime.process.committed); + } +} +#[test] +fn known_copy_refusal_closes_only_owner_ends_thaws_and_preserves_parent_lease() { + let mut h = Fixture::new(); + h.host.0.borrow_mut().copy_result = -11; + let raw = h.snapshot(false); + let mut fork = [0; 368]; + zsh_process_frames::encode_fork(&raw, h.runtime.zsh_expected(false).unwrap(), 1, &mut fork) + .unwrap(); + let mut work = [0; 4096]; + work[..368].copy_from_slice(&fork); + h.runtime.zsh_fork(&mut work).unwrap(); + assert_eq!( + zsh_process_frames::decode_receipt(&h.output(PW), b"AEZFR001", 1).unwrap(), + -11 + ); + assert_eq!(h.host.0.borrow().closed.len(), 4); + assert!( + h.runtime + .process + .ends + .iter() + .all(|e| e.attempted && e.status == 0) + ); + assert_eq!(h.runtime.process.lease, ReadLease::Parent); + assert!(h.runtime.process.binding.is_none()); + assert_eq!(h.runtime.bound.reserved, RESERVED); + assert!(h.host.0.borrow().calls.contains(&(0x23005, PARENT, 101))); +} +#[test] +fn selected_header_shapes_and_partial_poll_preserve_frame_boundaries() { + for q in [ + frame(1, 0x7a000001, 0, 16), + frame(2, 0x79000102, 4, 1), + frame(3, 0x79000101, 3, 0), + frame(82, 0x76050001, 1, 0), + frame(68, u32::MAX, 11, 0), + frame(0x10, tty_mode::CONTROL, 0, 0), + ] { + assert!(zsh_small_header(&q, false)); + } + for q in [ + frame(4, 0x7a000001, 0, 0), + frame(1, 0x79000001, 3, 1), + frame(1, 0x79000a01, 3, 1), + frame(1, 0x7a000001, 0, 17), + frame(3, 0x7a000001, 0, 1), + frame(82, 0x76050001, 0, 0), + frame(68, 9, 0, 0), + frame(68, 10, 4, 0), + frame(0x13, tty_mode::CONTROL, 0, 0), + ] { + assert!(!zsh_small_header(&q, false)); + } + assert!(!zsh_small_header(&frame(68, 10, 0, 0), true)); + assert!(!zsh_small_header(&frame(82, 0x76050001, 1, 0), true)); + let mut h = Fixture::new(); + let q = frame(2, 0x7a010001, 1, 0); + h.feed(PR, &q[..8]); + assert_eq!(h.runtime.zsh_poll(false, &mut [0; 4096]), Ok(Some(true))); + assert_eq!(h.runtime.process.parent_at, 8); + assert_eq!(h.runtime.zsh_poll(false, &mut [0; 4096]), Ok(None)); + h.feed(PR, &q[8..]); + assert_eq!(h.runtime.zsh_poll(false, &mut [0; 4096]), Ok(Some(true))); + assert_eq!(h.runtime.process.parent_at, 0); + assert_eq!(h.output(PW), [0; 32]); +} + +// Full fatal path: each branch uses the real Runtime body and the same child +// credits debited before copy. Mocked ctl outputs are explicit observations; +// an unknown query is deliberately not converted to a terminal/reclaim proof. +#[test] +fn contain_live_and_unknown_copy_settle_known_child_once_without_replay() { + for unknown_copy in [false, true] { + let mut h = Fixture::new(); + if unknown_copy { + h.host.0.borrow_mut().copy_result = 1; + let raw = h.snapshot(false); + let mut request = [0; 368]; + zsh_process_frames::encode_fork( + &raw, + h.runtime.zsh_expected(false).unwrap(), + 1, + &mut request, + ) + .unwrap(); + let mut work = [0; 4096]; + work[..368].copy_from_slice(&request); + assert_eq!(h.runtime.zsh_fork(&mut work), Err(-5)); + assert!(h.runtime.process.endpoints.is_none()); + } else { + h.fork(false); + } + let child_ends = h.runtime.process.ends.map(|end| end.cap); + assert_eq!(h.runtime.process.prepaid, 6); + assert_eq!(h.runtime.bound.reserved, RESERVED); + h.runtime.bound.left = 0; + h.runtime.bound.failed = -110; + let calls_before = h.host.0.borrow().calls.len(); + assert_eq!(h.runtime.contain(), Err(-5)); + let host = h.host.0.borrow(); + let child_calls: Vec<_> = host.calls[calls_before..] + .iter() + .copied() + .filter(|(_, cap, _)| *cap == CHILD) + .collect(); + assert_eq!( + child_calls, + [ + (0x20007, CHILD, 0), + (0x20001, CHILD, 0), + (0x20002, CHILD, 0), + (0x20001, CHILD, 0) + ] + ); + assert!(host.gone); + assert!(host.parent_gone); + assert_eq!( + host.closed, + [child_ends[0], child_ends[3], NSR, NSW, PR, PW] + ); + assert!( + !host.closed.contains(&child_ends[1]) && !host.closed.contains(&child_ends[2]), + "TRANSFERRED_ENDS_ARE_NOT_OWNER_CLOSES" + ); + assert_eq!( + host.calls.iter().filter(|call| call.0 == 0x23006).count(), + 1 + ); + assert!( + !host + .calls + .iter() + .any(|call| matches!(call.0, 0x23007 | 0x23009)) + ); + assert_eq!( + host.calls + .iter() + .filter(|call| call.0 == 0x20007 && call.1 == CHILD) + .count(), + 1 + ); + let calls = host.calls.clone(); + let closes = host.closed.clone(); + drop(host); + assert_eq!(h.runtime.process.prepaid, 0); + assert_eq!(h.runtime.process.cleanup_attempted, 15); + assert_eq!(h.runtime.process.cleanup_last, (-9, 0, 0)); + assert_eq!(h.runtime.process.lease, ReadLease::Contained); + assert_eq!(h.runtime.process.wait_status, None); + assert!(!h.runtime.process.exec_ready && !h.runtime.process.reclaimed); + assert_eq!(h.runtime.bound.left, 0); + // Four original Parent lifecycle calls + its two physical-end closes + + // console close + terminal failure leaf use8. Child's6 use no reserve. + assert_eq!(h.runtime.bound.reserved, RESERVED - 8); + assert_eq!(h.runtime.cleanup_attempted[0], [true; 4]); + assert_eq!(h.runtime.cleanup_attempted[1], [false; 4]); + assert_eq!(h.runtime.contain(), Err(-5)); + assert_eq!(h.host.0.borrow().calls, calls); + assert_eq!(h.host.0.borrow().closed, closes); + } +} +#[test] +fn contain_unknown_copy_unknown_terminal_never_reclaims_or_restores_parent_lease() { + let mut h = Fixture::new(); + h.host.0.borrow_mut().copy_result = 1; + let raw = h.snapshot(false); + let mut request = [0; 368]; + zsh_process_frames::encode_fork( + &raw, + h.runtime.zsh_expected(false).unwrap(), + 1, + &mut request, + ) + .unwrap(); + let mut work = [0; 4096]; + work[..368].copy_from_slice(&request); + assert_eq!(h.runtime.zsh_fork(&mut work), Err(-5)); + h.host.0.borrow_mut().stop_observation = (-5, 0, 0); + let child_ends = h.runtime.process.ends.map(|end| end.cap); + let before = h.host.0.borrow().calls.len(); + assert_eq!(h.runtime.contain(), Err(-5)); + let host = h.host.0.borrow(); + let child_calls: Vec<_> = host.calls[before..] + .iter() + .copied() + .filter(|(_, cap, _)| *cap == CHILD) + .collect(); + assert_eq!(child_calls, [(0x20007, CHILD, 0), (0x20001, CHILD, 0)]); + assert!(!host.gone, "UNKNOWN_TERMINAL_MUST_NOT_RECLAIM"); + assert_eq!( + host.closed, + [child_ends[0], child_ends[3], NSR, NSW, PR, PW] + ); + assert_eq!( + host.calls.iter().filter(|call| call.0 == 0x23006).count(), + 1 + ); + assert!( + !host + .calls + .iter() + .any(|call| matches!(call.0, 0x23007 | 0x23009)) + ); + let calls = host.calls.clone(); + let closes = host.closed.clone(); + drop(host); + assert_eq!(h.runtime.process.cleanup_attempted, 3); + assert_eq!(h.runtime.process.cleanup_last, (-5, 0, 0)); + assert_eq!( + h.runtime.process.prepaid, 2, + "UNUSED_PREPAID_IS_NOT_REFUNDED_OR_RENEWED" + ); + assert_eq!(h.runtime.bound.reserved, RESERVED - 8); + assert_eq!(h.runtime.process.lease, ReadLease::Contained); + assert_eq!(h.runtime.process.wait_status, None); + assert!(h.runtime.process.binding.is_some()); + assert!(!h.runtime.process.reclaimed); + assert_eq!(h.runtime.contain(), Err(-5)); + assert_eq!(h.host.0.borrow().calls, calls); + assert_eq!(h.host.0.borrow().closed, closes); +} diff --git a/tests/test_build_policy.py b/tests/test_build_policy.py index 4d31a2c..b1a2f7c 100644 --- a/tests/test_build_policy.py +++ b/tests/test_build_policy.py @@ -6,6 +6,7 @@ ROOT = Path(__file__).resolve().parents[1] ALLOWED = { + 'native-zsh-external', # Approved closed parent1/helper2 copy/exec/SIGCHLD; old terminfo required; default-off. 'native-zsh-terminfo', # Approved closed large-Zsh/MODE/SIGINT; Owner retains namespace; default-off/private. 'native-tty-signal-probe', # Approved default-off MODE Owner/app SIGINT; checked safe unmask; release rejected. 'native-tty-mode-probe', # Approved default-off ABI31 holder TEMT query; private BSP only, release rejected. diff --git a/tests/test_native_zsh_external.py b/tests/test_native_zsh_external.py new file mode 100644 index 0000000..42525ff --- /dev/null +++ b/tests/test_native_zsh_external.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Closed parent/helper source checks and actual-body host fixture; no guest. + +Run python3 -B -m unittest discover -s tests -p test_native_zsh_external.py -v. +Public checks use only stdlib and launch no process. Private qualified capture +compiles checker_source with the actual kernel modules; no public dependency on +uncommitted bounded_capture APIs is introduced. +""" +import ast +from pathlib import Path +import unittest +ROOT=Path(__file__).resolve().parents[1] + + +def block(source,anchor): + start=source.index(anchor);opening=source.index('{',start);depth=1;end=opening+1 + while depth: + depth+=(source[end]=='{')-(source[end]=='}');end+=1 + return source[start:end] + + +def literal(path,name): + tree=ast.parse(path.read_text()) + return next(ast.literal_eval(n.value) for n in tree.body if isinstance(n,ast.Assign) + and any(isinstance(t,ast.Name) and t.id==name for t in n.targets)) + + +FACADE='''pub const App=@import("native_app.zig");pub const elf=@import("native_elf.zig"); +pub const memory=@import("native_memory.zig");pub const roles=@import("arch/x86_64/native_fs_roles.zig"); +pub const launch=@import("native_launch.zig");pub const Pending=@import("arch/x86_64/native_pending.zig");''' + + +def checker_source(native,omission=None): + scaffold=literal(ROOT/'tests/test_fs_application_profile.py','SCAFFOLD').split('fn launchCase()',1)[0] + scaffold=scaffold.replace('const elf=@import("elf");','const K=@import("kernel");const elf=K.elf;const launch=K.launch;') + scaffold=scaffold.replace('const memory=@import("memory");','const memory=K.memory;').replace('const FsRoles=@import("roles");','const FsRoles=K.roles;') + scaffold=scaffold.replace('app_binding:struct{clock:u64=0,fn bound(b:*const @This())bool{return b.clock!=0;}}=.{},', + 'app_binding:K.App.Binding=.{},tty_signal_owner_generation:?u64=null,signal_actions:K.Pending.State=.{},') + scaffold=scaffold.replace('fn payloadBytes(s:*State)', 'fn context(s:*State)*Execution{return &s.execution;}\n fn payloadBytes(s:*State)') + scaffold=scaffold.replace('fn result(_: *State,value:i64)void{response=value;}', 'fn result(s:*State,value:i64)void{response=value;s.execution.frame.rax=@bitCast(value);}') + scaffold=scaffold.replace('s.admitted=false;s.execution=.{};', 's.admitted=false;s.ready=false;s.freeze_sequence=0;s.image_commit=.{};s.app_binding=.{};s.tty_signal_owner_generation=null;s.signal_actions=.{};s.execution=.{};') + bodies='\n'.join(block(native,'fn '+name+'(') for name in ('imagePreflight','copyPreflight','freezeOperation','copyOperation','execOperation','notifyParent')) + aliases=block(native,'fn aliases(');bodies+='\n'+aliases[:aliases.index(' asm volatile')]+'}\n' + if omission=='clock': + old='child.app_binding = child_binding;';assert bodies.count(old)==1;bodies=bodies.replace(old,'child.app_binding = source.app_binding;') + elif omission=='tty': + old='child.tty_signal_owner_generation = null;';assert bodies.count(old)==1;bodies=bodies.replace(old,'child.tty_signal_owner_generation = source.tty_signal_owner_generation;') + elif omission=='exec-image': + old='if (f.rax == 0x23007 and f.rdx != 0xc003)';assert bodies.count(old)==1;bodies=bodies.replace(old,'if (false)') + elif omission is not None:raise ValueError('closed omission') + extra=''' +const af09_enabled=false;const af09=struct{const engine_bound=false;}; +fn af09Bind(_:usize,_:usize)void{unreachable;}fn af09Fail(_:usize,_:i64)void{unreachable;} +const session_lifecycle=true;fn rotate()void{} +''' + return scaffold+extra+bodies+CASES + + +OPTIONS=dict(native_process_exec=True,native_exec_copy=False,native_signal_actions=True, + native_copy_signals=True,native_copy_signals_drop=False,native_copy_signals_pending=False,native_copy_signals_parent=False, + native_copy_lose_anonymous=False,native_copy_share=False,native_copy_fail_stage=False,native_copy_skip_zero=False, + native_freeze_skip_park=False,native_freeze_ignore_token=False,native_exec_token=False,native_exec_early=False, + native_signal_suspend=True,native_tty_signal_probe=True) + +CASES=r''' +fn setup()void{ + reset();copySource();states[6].admitted=true;states[6].generation=0; + states[1].app_binding=K.App.Binding.init(1,7,33,44,55).?;states[1].tty_signal_owner_generation=0; + states[1].signal_actions.records[16]=.{.entry=0x200010,.restorer=0x200020}; + states[1].signal_actions.pending=2; +} +fn copyFrame()Frame{return .{.rax=0x23006,.rdi=0xa001,.rsi=1,.rdx=2,.r10=5,.r8=10,.r9=11};} +fn execFrame()Frame{return .{.rax=0x23007,.rdi=0xa001,.rsi=2,.rdx=0xc003,.r10=6,.r8=0x1000};} +fn copied()void{ + setup();_=copyOperation(©Frame());check(response==2 and states[2].admitted,"SELECTED_CHILD_ADMITTED"); + const child=&states[2];const binding=child.app_binding; + check(child.generation==8 and binding.valid(2,8,true,false,binding.clock),"CHILD_CLOCK_GENERATION"); + check(binding.clock!=states[1].app_binding.clock and !binding.valid(1,7,true,false,binding.clock),"CHILD_CLOCK_NOT_PARENT"); + check(binding.reader==33 and binding.writer==44 and binding.epoch==55,"CHILD_INTERNAL_PROVENANCE"); + check(child.tty_signal_owner_generation==null,"CHILD_NO_TTY_OWNER_INCLUDING_ZERO"); + check(channels[0].owners[0]==2 and channels[1].owners[1]==2 and child.channel_cap==10 and child.channel_reply_cap==11,"CHILD_PROCESS_ENDPOINTS_ONLY"); + check(child.parent_pid==51 and states[1].execution.frame.rax==59 and child.execution.frame.rax==0,"ACTUAL_PARENT_CHILD_IDENTITIES"); + check(child.signal_actions.pending==0 and states[1].signal_actions.pending==2 and child.signal_actions.records[16].entry==0x200010,"CHILD_PENDING_EMPTY_DISPOSITION_INHERITED"); + check(!binding.valid(2,7,true,false,binding.clock) and !binding.valid(2,8,false,false,binding.clock) and !binding.valid(2,8,true,true,binding.clock),"CHILD_CLOCK_STALE_DENIALS"); +} +fn copyDenied()void{ + for(0..10)|case|{ + setup();var f=copyFrame(); + switch(case){0=>f.rdi=0,1=>active=1,2=>f.rdx=3,3=>f.r10=4,4=>states[2].admitted=true, + 5=>states[1].tty_signal_owner_generation=null,6=>states[1].generation=8,7=>states[6].generation=1, + 8=>states[1].terminal=true,9=>states[1].app_binding.clock=0,else=>unreachable,} + const child=states[2];_=copyOperation(&f); + check(response<0 and std.meta.eql(child,states[2]) and mutations==0 and channels[0].owners[0]==6,"COPY_DENIAL_NO_EFFECT"); + } + setup();states[2].generation=15;_=copyOperation(©Frame());check(response==-28 and !states[2].admitted,"CHILD_LIFETIME_CAP"); +} +fn freezeCases()void{ + setup();states[1].frozen=false;active=1;_=freezeOperation(&.{.rax=0x23003});check(response==-11 and states[1].frozen,"PARENT_COOPERATIVE_FREEZE"); + copied();active=2;_=freezeOperation(&.{.rax=0x23003});check(response==-11 and states[2].frozen and states[2].freeze_sequence==1,"CHILD_COOPERATIVE_EXEC_FREEZE"); + states[2].frozen=false;states[2].tty_signal_owner_generation=0;_=freezeOperation(&.{.rax=0x23003});check(response==-95 and !states[2].frozen,"CHILD_SOME_ZERO_NOT_NULL"); +} +fn execCases()void{ + copied();const before=states[2].app_binding;const before_mutations=mutations;states[2].frozen=true;states[2].freeze_sequence=6; + for([_]u64{0xc001,0xc002})|image|{var f=execFrame();f.rdx=image;_=execOperation(&f);check(response==-13 and mutations==before_mutations and !states[2].image_commit.pending,"CHILD_EXEC_C003_ONLY");} + var f=execFrame();f.rsi=1;_=execOperation(&f);check(response==-95,"PARENT_EXEC_DENIED"); + f=execFrame();f.r10=5;_=execOperation(&f);check(response==-9 and std.meta.eql(before,states[2].app_binding),"EXEC_STALE_TOKEN_NO_CHANGE"); + f=execFrame();_=execOperation(&f);check(response==0 and states[2].image_commit.pending and states[2].frozen and !states[2].ready,"CHILD_EXEC_PENDING"); + check(states[2].generation==8 and std.meta.eql(before,states[2].app_binding) and states[2].tty_signal_owner_generation==null,"EXEC_PRESERVES_CHILD_LIFETIME"); + _=execOperation(&.{.rax=0x23008,.rdi=0xa001,.rsi=2});check(response==6 and states[6].execution.frame.rdx==0xc003 and states[6].execution.frame.r8==1,"EXACT_EXEC_RECEIPT"); + _=execOperation(&.{.rax=0x23009,.rdi=0xa001,.rsi=2,.rdx=5});check(response==-9 and states[2].image_commit.pending,"STALE_PUBLISH_DENIED"); + _=execOperation(&.{.rax=0x23009,.rdi=0xa001,.rsi=2,.rdx=6});check(response==0 and !states[2].frozen and !states[2].image_commit.pending,"CHECKED_EXEC_PUBLICATION"); +} +fn execDenied()void{ + for(0..8)|case|{ + copied();states[2].frozen=true;states[2].freeze_sequence=6; + switch(case){0=>states[1].generation+=1,1=>states[6].generation=1,2=>states[2].generation+=1, + 3=>states[2].tty_signal_owner_generation=0,4=>states[2].app_binding.epoch+=1, + 5=>states[1].terminal=true,6=>states[2].parent_pid=0,7=>states[2].terminal=true,else=>unreachable,} + const binding=states[2].app_binding;const before=mutations;_=execOperation(&execFrame()); + check(response<0 and mutations==before and std.meta.eql(binding,states[2].app_binding) and !states[2].image_commit.pending,"EXEC_STALE_RELATIONSHIP_NO_EFFECT"); + } + copied();states[2].frozen=true;states[2].freeze_sequence=6;third_image.segments[0].memsz=640*4096; + const before=mutations;_=execOperation(&execFrame());check(response==-12 and mutations==before and states[2].frozen and !states[2].image_commit.pending,"FAILED_EXEC_OLD_IMAGE_RETAINED"); + setup();states[2].generation=14;_=copyOperation(©Frame());check(response==2 and states[2].generation==15 and states[2].app_binding.valid(2,15,true,false,states[2].app_binding.clock),"REUSED_CHILD_ACTUAL_NEXT_GENERATION"); +} +fn rollbackCase()void{ + setup();_=copyOperation(©Frame()); + check(response==-12 and !states[2].admitted and states[2].generation==7 and states[1].frozen and !states[2].app_binding.bound() and states[2].tty_signal_owner_generation==null,"FAILED_COPY_UNPUBLISHED_NO_BINDING"); + check(channels[0].owners[0]==6 and channels[1].owners[1]==6 and std.mem.allEqual(u8,states[2].payloadBytes(),0),"FAILED_COPY_ZERO_NO_ENDPOINT_TRANSFER"); +} +fn notifyCases()void{ + copied();active=2;notifyParent(&states[2]);check(states[1].signal_actions.pending==65538,"EXACT_SIGCHLD17"); + states[1].signal_actions.pending=0;states[1].generation+=1;notifyParent(&states[2]);check(states[1].signal_actions.pending==0,"STALE_PARENT_NO_SIGCHLD"); +} +pub fn main()void{ + const selected=@hasDecl(@import("build_options"),"native_zsh_external") and @import("build_options").native_zsh_external; + if(@import("build_options").native_copy_fail_stage){rollbackCase();} + else if(selected){copied();copyDenied();freezeCases();execCases();execDenied();notifyCases();} + else{setup();_=copyOperation(©Frame());check(response==-95 and !states[2].admitted,"OLD_BOUND_COPY_DENIAL");active=1;states[1].frozen=false;_=freezeOperation(&.{.rax=0x23003});check(response==-95 and !states[1].frozen,"OLD_BOUND_FREEZE_DENIAL");} + std.debug.print("ZSH EXTERNAL MODEL PASS\n",.{}); +} +''' + + +class ZshExternalSource(unittest.TestCase): + def test_actual_model_and_closed_binding_sites(self): + native=(ROOT/'kernel/arch/x86_64/native_c.zig').read_text() + self.assertIn('child.app_binding = child_binding;',native) + self.assertIn('child.tty_signal_owner_generation = null;',native) + self.assertIn('if (f.rax == 0x23007 and f.rdx != 0xc003)',native) + self.assertIn('s.app_binding.bound() and !(@hasDecl(@import("build_options"), "native_zsh_terminfo")',native) + self.assertIn('ZSH EXTERNAL MODEL PASS',checker_source(native)) + for omission in ('clock','tty','exec-image'): + self.assertNotEqual(checker_source(native),checker_source(native,omission)) + + def test_build_selection_remains_explicit_default_off(self): + source=(ROOT/'build.zig').read_text() + self.assertIn('b.option(bool, "native-zsh-external",',source) + self.assertIn('const native_zsh_external = ',source) + self.assertIn('options.addOption(bool, "native_zsh_external", native_zsh_external);',source) + guard=next(line for line in source.splitlines() if 'if (native_zsh_external and' in line) + for dependency in ('native_zsh_terminfo','native_process_copy','native_process_exec','native_copy_signals','native_signal_actions','native_signal_public','native_signal_suspend','native_parent_identity'): + self.assertIn('!'+dependency,guard) + + +if __name__=='__main__':unittest.main() diff --git a/tests/test_zsh_owner_capacity.py b/tests/test_zsh_owner_capacity.py new file mode 100644 index 0000000..0db01f5 --- /dev/null +++ b/tests/test_zsh_owner_capacity.py @@ -0,0 +1,354 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Closed #143 capacity fixtures; no compiler/VM is launched by this module. + +Run pure controls: python3 -B -m unittest discover -s tests -p test_zsh_owner_capacity.py -v +The lead's bounded private capture compiles parser_source, boot_source and +mapping_source against actual current Zig bodies; exact invocations/results are +recorded separately. This file needs no uncommitted capture API. +""" +import importlib.util +from pathlib import Path +import re +import struct +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +def block(source, needle): + start = source.index(needle) + end = source.index('{', start) + depth = 1 + end += 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +def small_elf(): + raw = bytearray(4096) + raw[:8] = b'\x7fELF\x02\x01\x01\0' + struct.pack_into('73728,"EXTERNAL_OWNER_BOUND");} + if(Fs.parseApplication(raw))|good|{check(size<=49152 and good.session.supervisor.len==size,"OLD_APPLICATION_OWNER_BOUND");} + else |_|{check(size>49152,"OLD_APPLICATION_OWNER_BOUND");} + if(Fs.parseOwnerStack(raw))|_|{check(false,"MAGIC_NOT_PROFILE_AUTHORITY");}else |_|{} + } + // This is the real old wire and old entry, not just a foreign-magic denial. + const old_sizes=[_]usize{49152,49153,67704}; + const old_raws=[_] []const u8{@embedFile("old-49152.bin"),@embedFile("old-49153.bin"),@embedFile("old-67704.bin")}; + for(old_raws,old_sizes)|raw,size|{ + if(Fs.parseOwnerStack(raw))|good|{check(size<=49152 and good.session.supervisor.len==size,"OLD_OWNER_STACK_BOUND");} + else |_|{check(size>49152,"OLD_OWNER_STACK_BOUND");} + } + for([_] []const u8{@embedFile("badmagic.bin"),@embedFile("reserved.bin"),@embedFile("truncated.bin"),@embedFile("tail.bin")})|raw|{ + const bad=if(@hasDecl(Fs,"parseExternalApplication")) Fs.parseExternalApplication(raw) else Fs.parseApplication(raw); + if(bad)|_|{check(false,"MALFORMED_SELECTED_ACCEPTED");}else |_|{} + } + check(Fs.application_envelope_limit==13156512,"OLD_DERIVED_OUTER_CAP"); + const external_cap=if(@hasDecl(Fs,"external_application_envelope_limit"))Fs.external_application_envelope_limit else Fs.application_envelope_limit; + check(external_cap==13181088,"EXTERNAL_DERIVED_OUTER_CAP"); + std.debug.print("PARSER_CAPACITY_PASS\n",.{}); +} +''' + + +def parser_inputs(): + inputs = {str(n)+'.bin': envelope(n) for n in (49152, 49153, 67704, 73728, 73729)} + raw = inputs['73728.bin'] + changed = bytearray(raw) + changed[:8] = b'AENFS005' + inputs['badmagic.bin'] = bytes(changed) + changed = bytearray(raw) + changed[4144+24] = 1 + inputs['reserved.bin'] = bytes(changed) + inputs['truncated.bin'] = raw[:-1] + inputs['tail.bin'] = raw+b'\0' + for size in (49152, 49153, 67704): + old = bytearray(envelope(size)) + old[:8] = b'AENFS005' + old[4144:4152] = b'AENMX010' + inputs['old-'+str(size)+'.bin'] = bytes(old) + return inputs + + +def start_preflight_source(*, omit=False): + """Execute start's actual first preflight, stopping before all later setup. + + Only the panic boundary becomes a false return in this host fixture. The + sentinel after the extracted prefix records reaching the effect boundary; + no reservation, device setup, native start or page table is executed here. + """ + if type(omit) is not bool: + raise ValueError('closed preflight omission control') + native = (ROOT/'kernel/arch/x86_64/native_c.zig').read_text() + start = block(native, 'pub fn start(') + prefix = start[start.index('{')+1:start.index(' require(envelope.len > 40);')] + if omit: + guard = block(prefix, ' if (combined_application_profile)') + prefix = prefix.replace(guard, '', 1) + prefix += ' _ = combined_application_profile; _ = envelope;\n' + prefix = prefix.replace('@import("build_options")', 'O') + prefix = prefix.replace('@panic("invalid filesystem application envelope")', 'return false') + return r'''const std=@import("std");const FsSession=@import("session"); +var effects:usize=0; +fn check(ok:bool,comptime label:[]const u8)void{if(!ok){std.debug.print("{s}\n",.{label});std.process.exit(91);}} +fn early(comptime O:type,envelope:[]const u8)bool{ + const combined_application_profile=O.application; +''' + prefix + r''' + effects+=1;return true; +} +fn expect(comptime O:type,raw:[]const u8,allowed:bool,comptime label:[]const u8)void{ + const before=effects;const accepted=early(O,raw);check(accepted==allowed,label); + check(effects==before+@intFromBool(accepted),"EARLY_START_EFFECT_BOUNDARY"); +} +pub fn main()void{ + const External=struct{pub const application=true;pub const native_zsh_external=true;}; + const Default=struct{pub const application=true;}; + const ExplicitFalse=struct{pub const application=true;pub const native_zsh_external=false;}; + const Legacy=struct{pub const application=false;}; + expect(External,@embedFile("67704.bin"),true,"EARLY_START_SELECTED_67704"); + expect(External,@embedFile("73728.bin"),true,"EARLY_START_SELECTED_MAX"); + expect(External,@embedFile("73729.bin"),false,"EARLY_START_SELECTED_ONE_OVER"); + expect(External,@embedFile("49152.bin"),true,"EARLY_START_SELECTED_SMALL"); + expect(External,@embedFile("49153.bin"),true,"EARLY_START_SELECTED_ABOVE_OLD"); + inline for(.{Default,ExplicitFalse})|O|{ + expect(O,@embedFile("49152.bin"),true,"EARLY_START_DEFAULT_49152"); + expect(O,@embedFile("49153.bin"),false,"EARLY_START_DEFAULT_49153"); + expect(O,@embedFile("67704.bin"),false,"EARLY_START_DEFAULT_67704"); + } + for([_] []const u8{@embedFile("badmagic.bin"),@embedFile("reserved.bin"),@embedFile("truncated.bin"),@embedFile("tail.bin"),@embedFile("old-49152.bin")})|raw|{ + expect(External,raw,false,"EARLY_START_MALFORMED_EXTERNAL"); + expect(Default,raw,false,"EARLY_START_MALFORMED_DEFAULT"); + } + const sizes=[_]usize{49152,49153,67704}; + const legacy=[_] []const u8{@embedFile("old-49152.bin"),@embedFile("old-49153.bin"),@embedFile("old-67704.bin")}; + for(legacy,sizes)|raw,size|{ + // Non-application profiles still skip this application-only prefix. Their + // actual old typed parser, rather than this prefix, enforces the old cap. + expect(Legacy,raw,true,"EARLY_START_LEGACY_GUARD_UNCHANGED"); + if(FsSession.parseOwnerStack(raw))|good|{check(size<=49152 and good.session.supervisor.len==size,"EARLY_START_LEGACY_TYPED_CAP");} + else |_|{check(size>49152,"EARLY_START_LEGACY_TYPED_CAP");} + } + std.debug.print("EARLY_START_PREFLIGHT_PASS\n",.{}); +} +''' + + +def boot_source(): + source = (ROOT/'kernel/main.zig').read_text() + gate = next(line.strip() for line in source.splitlines() if '@panic("native envelope oversized")' in line) + gate = gate.replace('@import("build_options")', 'O').replace('@import("native_fs_session.zig")', 'Fs') + gate = gate.replace('@panic("native envelope oversized")', 'return false') + return r'''const std=@import("std");const Fs=@import("session"); +fn check(ok:bool,comptime label:[]const u8)void{if(!ok){std.debug.print("{s}\n",.{label});std.process.exit(91);}} +fn accept(comptime O:type,size:u64)bool{const native=struct{size:u64}{.size=size}; +''' + gate + r''' +return true;} +pub fn main()void{ + const External=struct{pub const native_zsh_external=true;pub const native_fs_application_profile=true;pub const native_image_large=true;}; + const Old=struct{pub const native_fs_application_profile=true;pub const native_image_large=true;}; + const Large=struct{pub const native_fs_application_profile=false;pub const native_image_large=true;}; + const Ordinary=struct{pub const native_fs_application_profile=false;pub const native_image_large=false;}; + check(accept(External,13181088) and !accept(External,13181089),"EXTERNAL_BOOT_BOUND"); + check(accept(Old,13156512) and !accept(Old,13156513) and !accept(Old,13181088),"OLD_BOOT_BOUND"); + check(accept(Large,8392752) and !accept(Large,8392753) and !accept(Large,9081744),"LARGE_BOOT_BOUND"); + check(accept(Ordinary,3154000) and !accept(Ordinary,3154001),"ORDINARY_BOOT_BOUND"); + std.debug.print("BOOT_CAPACITY_PASS\n",.{}); +} +''' + + +MAPPING = r''' +const std=@import("std");const FsRoles=@import("roles");const O=@import("build_options"); +const combined_profile=true;const combined_init=true;const combined_owner_data=true; +const combined_owner_stack=O.stack;const combined_application_profile=true;const nx:u64=1<<63; +const Page=struct{address:usize=0,flags:u64=0}; +const Frame=struct{rsp:u64=0x801000-8,rdi:u64=29,rsi:u64=0xa001,rdx:u64=0xf101,rcx:u64=0xc002,r8:u64=0xd001,r9:u64=0xc003,r10:u64=0,r11:u64=0}; +const Context=struct{frame:Frame=.{}}; +const State=struct{pages:[128]Page=@splat(.{}),backing:[128][4096]u8=@splat(@splat(0xa5)),count:usize=0, + pt_index:[32]usize=@splat(0),pt_count:usize=0,pts:[32][512]u64=@splat(@splat(0)),pd:[512]u64=@splat(0),memory_cap:u64=0,execution:Context=.{}, + fn context(s:*State)*Context{return &s.execution;}}; +var states:[7]State=@splat(.{}); +fn supervisorSlot()usize{return 6;} +fn check(ok:bool,comptime label:[]const u8)void{if(!ok){std.debug.print("{s}\n",.{label});std.process.exit(91);}} +fn require(ok:bool)void{check(ok,"PRODUCTION_INVARIANT");} +fn phys(p:anytype)u64{return @intFromPtr(p)&~@as(u64,4095);} +fn backingFrame(s:*State,i:usize)u64{return 0x10000000+((@intFromPtr(s)-@intFromPtr(&states[0]))/@sizeOf(State)*128+i)*4096;} +fn pte(s:*State,address:usize)u64{for(s.pt_index[0..s.pt_count],0..)|n,i|if(n==address/(2*1024*1024))return s.pts[i][address/4096%512];return 0;} +''' + + +def mapping_source(*, external, owner_stack=True, omission=None, code_bytes=None): + native = (ROOT/'kernel/arch/x86_64/native_c.zig').read_text() + bodies = '\n'.join(block(native, name) for name in ( + 'fn addPage(', 'fn byte(', 'fn mapOwnerStack(', 'fn mapOwnerData(', 'fn ownerDataStartup(', 'fn auditOwnerStack(')) + if omission is not None: + before, after = omission + if before not in bodies: + raise ValueError('omission must match actual production body') + bodies = bodies.replace(before, after, 1) + rx = next(line.strip() for line in native.splitlines() if line.strip().startswith('for (0..(supervisor_code.len + 4095) / 4096)')) + pages = 18 if external else 12 if owner_stack else 2 + actual_bytes = pages*4096 if code_bytes is None else code_bytes + pages = (actual_bytes+4095)//4096 + count = 3 if external else 2 if owner_stack else 1 + base = 0x7ff000 if external else 0x800000 + cases = r''' +pub fn main()void{ + const s=&states[6];const supervisor_code=[_]u8{0}**CODE_BYTES; + RX_LOOP + mapOwnerStack(s);mapOwnerData(s);ownerDataStartup(&s.context().frame); + check(s.count==CODE_PAGES+STACK_PAGES+2,"STACK_PAGE_COUNT"); + for(0..CODE_PAGES)|i|{check(pte(s,0x400000+i*4096)==backingFrame(s,i)|5,"RX_FLAGS");} + for(0..STACK_PAGES)|i|{ + check(pte(s,STACK_BASE+i*4096)==backingFrame(s,CODE_PAGES+i)|7|nx,"STACK_RW_NX"); + for(s.backing[CODE_PAGES+i])|v|check(v==STACK_BYTE,"STACK_ZERO"); + } + check(pte(s,STACK_BASE-4096)==0 and pte(s,STACK_BASE+STACK_PAGES*4096)==0,"STACK_GUARDS"); + check(s.execution.frame.rsp==STACK_TOP-8 and s.execution.frame.rdi==ABI,"ENTRY_UNCHANGED"); + check(s.execution.frame.r10==0x900000 and s.execution.frame.r11==8192,"DATA_ENTRY_UNCHANGED"); + for(0..2)|i|{check(pte(s,0x900000+i*4096)==backingFrame(s,CODE_PAGES+STACK_PAGES+i)|7|nx,"DATA_RW_NX");} + for(states[0..6])|*peer|check(peer.count==0,"OWNER_ONLY_MAPPING"); + check(s.count<=try_pages() and FsRoles.application_backing_pages==11520,"UNCHANGED_BACKING"); + check(s.memory_cap==0 and s.pt_count<=32,"NO_ALLOCATOR_GRANT"); + auditOwnerStack(s,CODE_PAGES);std.debug.print("MAPPING_CAPACITY_PASS\n",.{}); +} +fn try_pages()usize{return FsRoles.applicationPages(6) catch unreachable;} +''' + values = dict(CODE_BYTES=actual_bytes, CODE_PAGES=pages, STACK_PAGES=count, + STACK_BASE=hex(base), STACK_TOP=hex(base+count*4096), STACK_BYTE=0 if owner_stack else 0xa5, ABI=31 if owner_stack else 30) + cases = cases.replace('RX_LOOP', rx) + for key in sorted(values, key=len, reverse=True): + cases = re.sub(r'\b'+key+r'\b', str(values[key]), cases) + return MAPPING+bodies+cases + + +def activation_audit_source(*, external=True, code_bytes=None, wrong_stack=False): + """Compose actual mapper, initial audit, alias loop and later activation audit. + + Alias leaf storage is synthetic host backing, not an executed MMU. The actual + production loops/guards consume it. No kernel/VM or device setup runs here. + """ + native = (ROOT/'kernel/arch/x86_64/native_c.zig').read_text() + source = mapping_source(external=external, code_bytes=code_bytes) + copy_body = block(native, 'fn copy(') + start = block(native, 'fn startSupervised(') + anchor = ' if (init_large or connections_large or @import("build_options").native_service_activation)' + alias_loop = start[start.index(' for (&states) |*mapping| for (s.pages[0..s.count]'):start.index(anchor)] + audit = block(start, anchor) + source = source.replace('fn require(ok:bool)void{check(ok,"PRODUCTION_INVARIANT");}', + 'var late_audit=false;fn require(ok:bool)void{if(late_audit)check(ok,"LATE_ACTIVATION_AUDIT_REJECT")else check(ok,"PRODUCTION_INVARIANT");}') + leaves = r''' +const init_large=false;const connections_large=true;const external_backing=true; +var alias_entries:[128]u64=@splat(0); +fn setBackingAlias(_: *State,owner:*State,i:usize,writable:bool)void{alias_entries[i]=backingFrame(owner,i)|(if(writable)@as(u64,3)else 1)|nx;} +fn backingAliasEntry(_: *const State,_:*const State,i:usize)u64{return alias_entries[i];} +''' + needle = 'pub fn main()void{' + source = source.replace(needle, leaves+copy_body+'\n'+needle, 1) + setup = 'mapOwnerStack(s);mapOwnerData(s);ownerDataStartup(&s.context().frame);' + pages = ((code_bytes or (73728 if external else 49152))+4095)//4096 + source = source.replace(setup, setup+'\n for(0..'+str(pages)+')|i|@memset(&s.backing[i],0);copy(s,0x400000,&supervisor_code);', 1) + insertion = 'auditOwnerStack(s,'+str(pages)+');' + require_tail = ('s.pages['+str(pages)+'].address='+('0x800000' if external else '0x7ff000')+';') if wrong_stack else '' + source = source.replace(insertion, insertion+'\n'+alias_loop+'\n'+require_tail+'late_audit=true;\n'+audit, 1) + return source + + +def inspector(): + spec = importlib.util.spec_from_file_location('actual_owner_stack', ROOT/'scripts/owner_stack.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class OwnerCapacity(unittest.TestCase): + def test_explicit_three_page_inspector_and_old_capacity_refusal(self): + s = inspector() + assembly = '0000000000400000 <_start>:\n400000: sub rsp, 9280\n400007: ret\n' + self.assertEqual(s.inspect(assembly, stack_bytes=12288)['entry_stack_bytes'], 9408) + for capacity in (4096, 8192): + with self.assertRaises(s.StackLimit): + s.inspect(assembly, stack_bytes=capacity) + + def test_stack_inspector_default_and_unsupported_profiles(self): + s = inspector() + assembly = '0000000000400000 <_start>:\n400000: ret\n' + self.assertEqual(s.inspect(assembly)['usable_stack_bytes'], 4088) + self.assertEqual(s.inspect(assembly, stack_bytes=8192)['usable_stack_bytes'], 8184) + for capacity in (True, 0, 8193, 16384): + with self.assertRaises(ValueError): + s.inspect(assembly, stack_bytes=capacity) + + def test_selected_inspector_keeps_dynamic_recursive_unknown_edge_denials(self): + s = inspector() + for instruction in ('sub rsp, rax', 'xchg rsp, rax', 'call 400000 <_start>', 'call rax'): + with self.subTest(instruction=instruction), self.assertRaises(ValueError): + s.inspect('0000000000400000 <_start>:\n400000: '+instruction+'\n', stack_bytes=12288) + + def test_actual_activation_audit_is_composed_after_the_mapper(self): + source = activation_audit_source(external=True, code_bytes=67704) + native = (ROOT/'kernel/arch/x86_64/native_c.zig').read_text() + late = block(native, ' if (init_large or connections_large or @import("build_options").native_service_activation)') + self.assertIn(late, source) + self.assertLess(source.index('auditOwnerStack(s,17);'), source.index(late)) + self.assertIn('const supervisor_code=[_]u8{0}**67704;', source) + self.assertIn('s.pages[17].address=0x800000;', activation_audit_source(external=True, code_bytes=67704, wrong_stack=True)) + + def test_actual_start_prefix_has_a_compiling_effect_boundary_control(self): + source = start_preflight_source() + self.assertIn('EARLY_START_SELECTED_67704', source) + self.assertIn('EARLY_START_SELECTED_ONE_OVER', source) + self.assertIn('FsSession.parseApplication(envelope)', source) + self.assertNotIn('prepareBacking(', source) + omitted = start_preflight_source(omit=True) + self.assertNotIn('FsSession.parseApplication(envelope)', omitted) + self.assertIn('effects+=1;return true;', omitted) + + def test_fixtures_use_actual_sources_and_closed_extents(self): + raw = envelope(73728) + self.assertEqual(len(raw), 48+4096+112+3*4096+73728) + self.assertEqual(struct.unpack_from('', boot_source()) + dispatch = block((ROOT/'kernel/arch/x86_64/native_c.zig').read_text(), 'fn startSupervised(') + self.assertIn('native_zsh_external) FsSession.parseExternalApplication(envelope) else if (combined_application_profile) FsSession.parseApplication(envelope)', dispatch) + for external in (False, True): + self.assertIn(block((ROOT/'kernel/arch/x86_64/native_c.zig').read_text(), 'fn auditOwnerStack('), mapping_source(external=external)) + + +if __name__ == '__main__': + unittest.main() diff --git a/userland/docs/ZSH-FILESYSTEM.md b/userland/docs/ZSH-FILESYSTEM.md index eae3725..f3d61e0 100644 --- a/userland/docs/ZSH-FILESYSTEM.md +++ b/userland/docs/ZSH-FILESYSTEM.md @@ -1,12 +1,18 @@ # Selected Zsh filesystem integration -[Issue137](https://github.com/gonz0w/Aether/issues/137) owns implementation and -qualification. Cam approved this scoped design on2026-11-02; the canonical +[Issue137](https://github.com/gonz0w/Aether/issues/137) completed the first real +Zsh prompt, one builtin and normal exit/export/cleanup through +[PR138](https://github.com/gonz0w/Aether/pull/138). Its scoped fit and delivery +decisions remain specific to that checkpoint. +[Issue143](https://github.com/gonz0w/Aether/issues/143) owns the selected +parent/helper extension below; its native acceptance remains open. The canonical [execution policy](../../docs/policy/execution.md#selected-real-zsh-filesystem-composition-2026-11-02) -records the answer. The contracts below are selected implementation requirements, -not evidence that a prompt or runtime qualification has passed. +records the approvals and limits. -## Current offline evidence +## Retained pre-trial offline evidence for issue137 + +The following measurements and preparation limits describe that earlier +builtin-only candidate. They do not qualify the new external-command composition. The selected source is implemented and separately reviewed. The fresh Zsh/tinfo executable is1,274,352 bytes and fits314 pages including the existing mapped stack; @@ -205,3 +211,188 @@ then normal exit, mode restoration and clean export on disposable no-network media. The selected VT100 frontend needs real PTY/input/termination checks; serial byte capture alone does not qualify an emulator. No current result here claims a prompt, login, external command, cold persistence, package or R1 gate. + +## Selected parent/helper extension + +The [owner-approved issue142 design](https://github.com/gonz0w/Aether/issues/142#issuecomment-6589424662) +selects the separate issue143 implementation. `native-zsh-external`, +`AEFD_ZSH_PROCESS_BINDINGS` and `native_zsh_process` name its closed kernel, +consumer and Owner selections. They extend the real terminfo/MODE parent only +for one child at a time. Existing selections keep their original bounds. + +The [issue143 capacity decision](https://github.com/gonz0w/Aether/issues/143#issuecomment-6591806304) +allows only this external selection to use73,728 Owner RX bytes and three mapped +stack pages (12,288 bytes,12,280 usable). The stack is `[0x7ff000,0x802000)` +RW/NX, zeroed, with absent guards at0x7fe000/0x802000. ABI31 and +RSP0x802000-8 stay unchanged. Runtime/workspace remain4,096 bytes each; maximum +Owner mappings are18 RX+3 stack+2 data=23 of128 pages. Total backing stays11,520 +pages. The unchanged AENFS006/AENMX011 magic cannot choose this allowance; +explicit kernel selection uses `parseExternalApplication`, while old +`parseApplication` retains49,152 bytes and the two-page stack. The app's limits, +channels and execution/capture bounds are unchanged. A fresh matched Owner fit +and separate review remain required. The scoped workload-fit and delivery +conditions are recorded in the execution policy linked above. + +Pure controls run from the repository root with +`python3 -B -m unittest discover -s tests -p test_zsh_owner_capacity.py -v`. +The test also supplies actual kernel-body fixtures; their bounded compiled +results are separate from pure checks and native qualification. + +The fixed target is the existing `/inputs/helper status` test executable, +owned image C003, with real exit7. At the actual +m prompt, +`(exec -c /inputs/helper status)` forks once through Zsh's real synchronization +pipe, executes with two arguments and a non-null empty environment, and returns +to the parent. A subsequent `print -r -- $?` must report7 and a real prompt. +The image selector is not an installed OpenZFS path or general utility claim. + +The Rust Owner retains the sole AF09 Client/epoch, HOME15/root3 and terminfo/FILE +custody. One stable mutable context exists per process. Use a new closed selected +fork/exec frame containing the full336-byte context and checked parent/child +identity; never compress it into the older304-byte snapshot or reuse its magic. +Retain shared descriptions, positions, pipe endpoints and CLOEXEC flags through +checked admission/rebind. Rebind all child-local pointers and endpoints under +the existing saved-mask guard; child pending signals start empty. An exec refusal +preserves the old child image/descriptors; close CLOEXEC only after known commit. + +Only the admitted selected parent can create the one child. Construct its native +admission/clock identity from its actual slot/generation, with namespace fields +remaining kernel custody provenance rather than exported endpoints. Preserve +exact Owner/A001/unadmitted launch guards and selected AENAP-startup suppression. +Exec preserves native process generation and the child-local lifetime binding. +Child `tty_signal_owner_generation` is null, not numeric0. No new syscall or +capability kind and no global mutable owner/table is introduced. + +The terminal core keeps its existing four aliases. The authenticated Owner proxies +child READ/WRITE through retained descriptions, without inventing a child Caller +for the single-caller terminal core. Parent MODE/control/private restore and the +existing SIGINT target remain parent-only. An explicit Parent/Child/Contained +read lease excludes competing consumers. This helper performs no input; child +canonical/ISIG reads reject EOPNOTSUPP before consumption. This is not general +POSIX terminal inheritance or child VINTR/job-control support. + +Use the pinned target's existing waitpid branch (HAVE_WAIT3 absent, HAVE_WAITPID +present) and its actual Owner route. Parent-scoped child selectors and only +WNOHANG1, WUNTRACED2, WCONTINUED8 are admitted; no stop/continue events exist in +this closed profile. Report real encoded exit status, truthful ECHILD/no-change +and untouched status output when no status is returned. No fake rusage: existing +getrusage remains ENOSYS without buffer writes and supplies no usage evidence. +Actual generation-checked SIGCHLD/sigsuspend supply notification and waiting. + +After a real child terminal, reserve/cache status, deliver the reply, mark known +collection, reconcile transport/descriptors/native reclaim, then restore Parent +read lease. A prompt READ arriving during reconciliation remains pending until +that lease returns. Do not require reclaim before status delivery, return false +WNOHANG0 for cleanup delay, lose notification or expose another reader. Unknown +delivery/copy/exec/cleanup contains the session without replay. Final parent exit +still restores mode and closes/exports/reclaims the actual session. + +Before any image, correct and measure the known external execute frame beyond +48KiB without increasing stack or reducing PATH_MAX. Fresh parent/helper/Owner +code/runtime/workspace/stack/heap/backing/TLS/request fits and affected semantic +checks remain required. Only exact reviewed artifacts/media and known-cleanup +selection may enter a later bounded guest. Issue137's fit and full-host delivery +exceptions do not transfer; issue141's coverage/UNKNOWN history stays separate. + +### Parent/helper selected startup and frame serialization + +The process selection uses full336-byte `AEFDB009`/`AEFDT009` snapshots. It retains +all four bytes of every binding, including kind5's nonzero duplex-description tag +in byte3. Do not convert that tag into the old three-field Rust Binding type. +Terminal labels remain0x7a000001/0x7a010001/0x7a020001; a child receives proxy +labels on its authenticated process connection, never another terminal-core Caller. +The seven-source/two-pipe histories and existing tagged pipe references remain. +A freshly admitted child/helper FILE client begins at sequence1 on its new +checked map/channel generation; the parent retains its existing next sequence. +The first copied child retains process counter1 and sends exec sequence2. +Only parent slot1 or child slot2 is valid in this selected representation. + +Snapshot offsets0..295 retain the complete identity/map/source/history layout. +At296 the parent has pipe factory0x76050001, and at300 parent MODE control is +0x7d000001. Both fields are0 for the child. Credential bytes304..335 retain four +u32 values1000, source-kind1 and zero flags/reserved bytes. The whole startup, +including exact identity, role, authenticated channel generation, all bindings, +sources and padding, validates before publication. Native process generation +and map/channel generation remain separate checked identities. + +| Frame | Exact extent and layout | +| --- | --- | +| Fork `AEZFK001` |368 bytes:32-byte operation header, then the complete336-byte parent snapshot | +| Exec `AEZEX001` |672 bytes:32-byte operation header, complete336-byte child snapshot,32-byte padded path,272-byte argv/env representation | +| Parent disposition `AEZFR001` |32 bytes:magic, sequence, actual slot2 PID or bounded negative known refusal, zero reserved word | +| Child ready `AEZFC001` |32 bytes:magic, sequence, actual slot2 PID, zero reserved word | +| Exec refusal `AEZER001` |32 bytes:magic, sequence, bounded negative refusal, zero reserved word | + +The operation header contains8-byte magic, u32 version1, u32 total extent, u64 +sequence and u64 reserved0. Sequence0 and u64 maximum reject; counter reservation +and exhaustion never reset or replay. Snapshot begins at32; exec path is368..399 +and arguments400..671. The existing272-byte arguments format retains u64 argc +1..4, u64 envc0..4 and eight32-byte string slots; each used string has a terminator +within31 bytes and zero tail, and every unused slot is zero. This is bounded +representation, not permission for arbitrary paths, environments or executables; +the selected workload and Owner admission still require their exact contract. + +The selected C `fd-zsh-process` and Rust `zsh_process_frames` codecs are inert +validators/serializers. Rust uses a borrowed full snapshot view so no alias tag or +advisory field is lost. Invalid decode publishes no view or sequence; encode +validates before output effects. Child correspondence preserves exact binding, +source, pipe-history and credential bytes while requiring its actual child +identity, distinct map generation, no MODE/factory authority and distinct native +endpoint observations. Complete runtime fork/exec/controller/lease wiring and its +native qualification are separate acceptance; codec tests do not establish it. +Existing008 and older304-byte formats and their control paths remain unchanged. + +The selected C provider now stores one `aefd_fork` aggregate in +`fd-zsh-external.c`; both the external-startup getter and the fork getter borrow +that same `boot`. The consumed initialized entry guard remains the sole4-byte +TLS object. `fd-fork-stage.c` captures a complete snapshot while holding the +existing mask, sends368 bytes, authenticates the correlated parent disposition, +and rebuilds child-local pointers only after the actual new-grant rebind. +`fd-exec.c` sends672 bytes and accepts only an exact known negative refusal after +raw thaw-11; an unknown result retires the context. These C paths do not implement +the Rust Owner's copy/commit, shared-description or terminal lease policy. + +The parent keeps its mutable `zsh -f -i +m` argument storage. A helper provider +selects `AEFD_ZSH_HELPER_ENTRY` with `AEFD_ZSH_PROCESS_BINDINGS` and external +startup, omits the parent diagnostic, and adds `fd-zsh-helper-entry.c`. That entry +consumes its actual336-byte startup, requires role2, publishes native READY, and +passes the CRT's actual argc/argv to real main before the existing checked exit. +It supplies no replacement argument or environment record. Both providers add +`fd-zsh-process`, `fd-fork-stage`, `fd-fork-native`, `fd-exec` and `fd-exec-native` +to the filesystem provider set; neither links the legacy `fd-fork-frame` codec. +The strong `waitpid` definition in `fd-libc-bindings.c` uses the actual Owner68 +route; its raw helper preserves caller errno, copies status only for a fully valid positive +reply and rejects other flag bits, child slots and malformed reply bytes. + +`fd-zsh-runtime-tests.c` is a focused controller fixture against the actual C +providers. It covers short transfers, fresh helper refusal paths, pipe creation, +CLOEXEC and duplex-tag inheritance, child-local pointer rebind, stale receipts, +exec argument/environment serialization and refusal containment, wait flags and +status-output preservation, masked reentry, and real checked helper finalization. +Its injected native observations are host qualification, not native execution. +The independently linkable codec fixture remains `fd-zsh-process-tests.c`. +Matched Rust controller tests, actual linked resource fits, separate review and +a later approved native trial are still required. + +### Selected parent/helper completion observation + +Only the external profile uses this exact row, with leading and trailing LF: + +```text +AETHER ZSH OWNER v=2 requests=NN child_status=1792 exec_ready=1 child_reclaimed=1 parent_lease=1 mode_cleanup=0 +``` + +NN is the actual14..96 service request count. Child status1792 is the actual +encoded normal exit7 reserved and delivered through the wait reply; it must +agree with the real child terminal. `exec_ready` requires the matched C003 +receipt, publication and fresh helper readiness. Reclaim, parent read lease and +mode cleanup report completed phases. Missing or unknown facts contain the +session instead of emitting this success observation. No capability or pointer +is printed; the older v1 profile retains its exact row. + +The fixed external trial submits `(exec -c /inputs/helper status)`, observes the +child terminal and reclaim followed by a parent prompt, then submits +`print -r -- $?` and requires standalone7 plus another prompt before `exit 0`. +Each input submission is once-only; unknown submission or cleanup stops without +replay. Supplied UART controls are synthetic and cannot establish execution, +image association, actual frontend/input or known process cleanup. Fresh linked +fits and separately reviewed image/media still precede any guest. diff --git a/userland/experiments/filesystem-service/fd-context.c b/userland/experiments/filesystem-service/fd-context.c index 25d4863..b7bc6e5 100644 --- a/userland/experiments/filesystem-service/fd-context.c +++ b/userland/experiments/filesystem-service/fd-context.c @@ -23,6 +23,7 @@ int64_t aefd_context_init(struct aefd_context *c, aefd_native_call call, generations, factory)) return -22; #ifdef AEFD_TERMINAL_DUPLEX_BINDINGS +#ifndef AEFD_ZSH_PROCESS_BINDINGS /*004 attests this fixed owner-held relationship, never initial kind5. */ for (unsigned i = 0; i < 3; ++i) if (map.slots[i].kind != AEFD_BIND_TERMINAL || @@ -33,10 +34,25 @@ int64_t aefd_context_init(struct aefd_context *c, aefd_native_call call, if (map.slots[fd].kind != AEFD_BIND_EMPTY && map.slots[fd].kind != AEFD_BIND_TERMINAL) return -22; +#else + /*009 snapshots preserve validated terminal aliases, pipes and FILE slots. + */ + for (unsigned fd = 0; fd < AEFD_BINDINGS_COUNT; ++fd) + if (map.slots[fd].kind == AEFD_BIND_CONSOLE) + return -22; + for (unsigned i = 0; i < 3; ++i) + if (sources[i].reference && + sources[i].reference != (UINT32_C(0x7a000001) | i << 16)) + return -22; +#endif #endif unsigned seen = 0; for (unsigned fd = 0; fd < AEFD_BINDINGS_COUNT; ++fd) { struct aefd_binding slot = map.slots[fd]; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + if (slot.kind == AEFD_BIND_DUPLEX_TERMINAL) + seen |= 3U; +#endif if (slot.kind == AEFD_BIND_CONSOLE || slot.kind == AEFD_BIND_PIPE #ifdef AEFD_TERMINAL_BINDINGS || slot.kind == AEFD_BIND_TERMINAL diff --git a/userland/experiments/filesystem-service/fd-exec.c b/userland/experiments/filesystem-service/fd-exec.c index ae6e17c..fe7aed2 100644 --- a/userland/experiments/filesystem-service/fd-exec.c +++ b/userland/experiments/filesystem-service/fd-exec.c @@ -1,11 +1,16 @@ /* SPDX-License-Identifier: BSD-2-Clause */ #define _POSIX_C_SOURCE 200809L #include "fd-exec.h" +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#include "fd-zsh-process.h" +#endif +#ifndef AEFD_ZSH_PROCESS_BINDINGS static void put32(unsigned char *raw, unsigned at, uint32_t value) { for (unsigned i = 0; i < 4; ++i) raw[at + i] = (unsigned char)(value >> (i * 8)); } +#endif static int move(struct aefd_io io, unsigned char *raw, size_t length, int writing, unsigned *budget) { @@ -83,8 +88,47 @@ int64_t aefd_exec(struct aefd_fork *f, struct aefd_startup_auxv observed, if (b->context.native.active || b->context.native.busy) return -16; if ((f->phase != AEFD_FORK_READY && f->phase != AEFD_FORK_IDLE) || - f->counter == UINT64_MAX) + f->counter == UINT64_MAX +#ifdef AEFD_ZSH_PROCESS_BINDINGS + || f->counter == UINT64_MAX - 1 +#endif + ) return -75; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + unsigned char raw[AEFD_ZSH_EXEC_BYTES] = {0}; + if (b->identity.slot != 2 || !arguments(raw + 400, argv, envp)) + return -7; + int64_t result = aefd_native_enter(&b->context.native); + if (result) + return failed(f); + int64_t pid = + b->context.native.call(b->context.native.context, 0x10027, 0, 0, 0, 0); + int64_t parent = + b->context.native.call(b->context.native.context, 0x1006e, 0, 0, 0, 0); + result = aefd_zsh_capture(b, observed, (long)pid, (long)parent, raw + 32); + if (result || + !aefd_zsh_exec_encode(raw, sizeof(raw), raw + 32, observed, (long)pid, + (long)parent, f->counter + 1, path, raw + 400)) { + if (!b->context.client.live || b->context.native.retired) + return failed(f); + aefd_native_leave(&b->context.native); + return result ? result : -22; + } + uint64_t sequence = ++f->counter; + unsigned budget = AEFD_IO_BUDGET; + struct aefd_io io = aefd_native_io(&b->context.native); + if (!move(io, raw, sizeof(raw), 1, &budget)) + return failed(f); + result = + b->context.native.call(b->context.native.context, 0x23003, 0, 0, 0, 0); + int64_t error; + if (result != -11 || !move(io, raw, 32, 0, &budget) || + !aefd_zsh_receipt_decode(raw, "AEZER001", sequence, &error)) + return failed(f); + memset(raw, 0, sizeof(raw)); + aefd_native_leave(&b->context.native); + return error; +#else unsigned char raw[608] = {0}; size_t len = strnlen(path, 32); if (!len || len == 32) @@ -149,4 +193,5 @@ int64_t aefd_exec(struct aefd_fork *f, struct aefd_startup_auxv observed, memset(raw, 0, sizeof raw); aefd_native_leave(&b->context.native); return error; +#endif } diff --git a/userland/experiments/filesystem-service/fd-fork-frame.h b/userland/experiments/filesystem-service/fd-fork-frame.h index c0ed166..710bd08 100644 --- a/userland/experiments/filesystem-service/fd-fork-frame.h +++ b/userland/experiments/filesystem-service/fd-fork-frame.h @@ -2,7 +2,11 @@ #ifndef AETHER_FD_FORK_FRAME_H #define AETHER_FD_FORK_FRAME_H #include "fd-startup.h" +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#define AEFD_FORK_REQUEST_BYTES 368U +#else #define AEFD_FORK_REQUEST_BYTES 304U +#endif #define AEFD_FORK_RECEIPT_BYTES 32U /* Inert borrowed bytes, never authenticated authority or an admitted context. */ diff --git a/userland/experiments/filesystem-service/fd-fork-native.c b/userland/experiments/filesystem-service/fd-fork-native.c index 404844b..f1541de 100644 --- a/userland/experiments/filesystem-service/fd-fork-native.c +++ b/userland/experiments/filesystem-service/fd-fork-native.c @@ -16,12 +16,17 @@ _Static_assert(sizeof(unsigned long) == 8 && sizeof(void *) == 8, "native x86-64 fork ABI required"); /* Real initialized TLS template, retained by live startup/copy checks. This * matches the old session invariant; never reset it to conceal a bad rebind. */ +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#include "fd-zsh-external.h" +int aefd_fork_tls_valid(void) { return aefd_zsh_tls_valid(); } +#else static _Thread_local volatile uint32_t fork_tls = UINT32_C(0x73657373); int aefd_fork_tls_valid(void) { return fork_tls == UINT32_C(0x73657373); } #ifdef AEFD_FORK_HOST_TEST void aefd_fork_test_tls_set(uint32_t); void aefd_fork_test_tls_set(uint32_t value) { fork_tls = value; } #endif +#endif /* old fork TLS; process reuses the consumed external entry guard */ static int native_rebind(void *context, struct aefd_startup_auxv parent, struct aefd_startup_auxv child) { diff --git a/userland/experiments/filesystem-service/fd-fork-stage.c b/userland/experiments/filesystem-service/fd-fork-stage.c index 6bd6d30..11a1293 100644 --- a/userland/experiments/filesystem-service/fd-fork-stage.c +++ b/userland/experiments/filesystem-service/fd-fork-stage.c @@ -1,11 +1,63 @@ /* SPDX-License-Identifier: BSD-2-Clause */ #include "fd-fork-stage.h" +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#include "fd-zsh-process.h" +int64_t aefd_zsh_capture(struct aefd_startup *boot, + struct aefd_startup_auxv observed, long pid, + long parent, unsigned char out[336]) +{ + if (!boot || !out || boot->phase != 2 || boot->context.state != 1 || + !boot->context.native.active || boot->context.native.busy || + observed.input != boot->context.native.input || + observed.output != boot->context.native.output) + return -22; + memset(out, 0, 336); + memcpy(out, "AEFDB009", 8); + aefd_put(out, 1, 1 | UINT64_C(336) << 32); + memcpy(out + 16, "AEFSH001", 8); + aefd_put(out + 24, 0, 29 | UINT64_C(7) << 32); + aefd_put(out + 32, 0, + boot->identity.slot | (uint64_t)boot->identity.epoch << 32); + aefd_put(out + 40, 0, + (uint32_t)parent | (uint64_t)boot->identity.shell_generation + << 32); + int64_t result = + aefd_session_snapshot(&boot->context.session, out + 48, 128, 0); + for (unsigned i = 0; i < 7; ++i) { + struct aefd_native_source source = boot->context.native.sources[i]; + aefd_put(out + 176 + i * 16, 0, + source.reference | (uint64_t)source.selector << 32); + aefd_put(out + 176 + i * 16, 1, + source.rights | (uint64_t)source.bound << 32); + } + aefd_put(out + 288, 0, + boot->context.native.generations[0] | + (uint64_t)boot->context.native.generations[1] << 32); + aefd_put(out + 296, 0, + boot->context.native.factory | (uint64_t)boot->context.mode_control + << 32); + for (unsigned i = 0; i < 2; ++i) + aefd_put(out + 304, i, + boot->credentials[i * 2] | + (uint64_t)boot->credentials[i * 2 + 1] << 32); + aefd_put(out + 320, 0, 1); + struct aefd_zsh_snapshot checked; + if (result || + !aefd_zsh_snapshot_decode(out, 336, observed, pid, parent, &checked)) { + memset(out, 0, 336); + return result ? result : -22; + } + return 0; +} +#endif +#ifndef AEFD_ZSH_PROCESS_BINDINGS static void put32(unsigned char *raw, unsigned offset, uint32_t value) { for (unsigned i = 0; i < 4; ++i) raw[offset + i] = (unsigned char)(value >> (i * 8)); } +#endif static int move(struct aefd_io io, unsigned char *raw, size_t length, int writing, unsigned *budget, int startup) { @@ -53,7 +105,11 @@ int64_t aefd_fork_arm(struct aefd_fork *f, struct aefd_startup_auxv observed) observed.input != b->context.native.input || observed.output != b->context.native.output) return -22; - if (f->counter == UINT64_MAX) + if (f->counter == UINT64_MAX +#ifdef AEFD_ZSH_PROCESS_BINDINGS + || f->counter == UINT64_MAX - 1 +#endif + ) return -75; f->previous_mask = b->context.native.saved_mask; int64_t result = aefd_native_enter(&b->context.native); @@ -64,6 +120,12 @@ int64_t aefd_fork_arm(struct aefd_fork *f, struct aefd_startup_auxv observed) int64_t pid = f->call(f->owner, 0x10027, 0, 0, 0, 0); int64_t parent = f->call(f->owner, 0x1006e, 0, 0, 0, 0); unsigned char *raw = b->scratch; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + result = aefd_zsh_capture(b, observed, (long)pid, (long)parent, raw + 32); + if (result || + !aefd_zsh_fork_encode(raw, AEFD_FORK_REQUEST_BYTES, raw + 32, observed, + (long)pid, (long)parent, f->counter + 1)) { +#else memset(raw, 0, AEFD_STARTUP_BYTES); memcpy(raw, AEFD_STARTUP_MAGIC, 8); put32(raw, 8, 1); @@ -88,6 +150,7 @@ int64_t aefd_fork_arm(struct aefd_fork *f, struct aefd_startup_auxv observed) put32(raw, 296, b->context.native.factory); if (result || !aefd_fork_encode(raw, 304, raw, 304, observed, (long)pid, (long)parent, f->counter + 1)) { +#endif if (!b->context.client.live) return parent_failure(f); aefd_native_leave(&b->context.native); @@ -101,7 +164,12 @@ int64_t aefd_fork_arm(struct aefd_fork *f, struct aefd_startup_auxv observed) f->phase = AEFD_FORK_SENDING; f->status = 0; f->budget = AEFD_IO_BUDGET; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + if (!move(aefd_native_io(&b->context.native), raw, AEFD_FORK_REQUEST_BYTES, + 1, &f->budget, 0)) +#else if (!move(aefd_native_io(&b->context.native), raw, 304, 1, &f->budget, 0)) +#endif return parent_failure(f); f->phase = AEFD_FORK_ARMED; return 0; @@ -118,8 +186,15 @@ int64_t aefd_fork_parent(struct aefd_fork *f, int64_t native_result) int64_t result; if (!move(aefd_native_io(&f->boot.context.native), f->boot.scratch, 32, 0, &f->budget, 0) || +#ifdef AEFD_ZSH_PROCESS_BINDINGS + !aefd_zsh_receipt_decode(f->boot.scratch, "AEZFR001", f->sequence, + &result) || + (native_result > 0 ? result != native_result : result >= 0) +#else !aefd_fork_parent_reply(f->boot.scratch, 32, f->sequence, native_result, - &result)) + &result) +#endif + ) return parent_failure(f); aefd_native_leave(&f->boot.context.native); f->boot.context.native.saved_mask = f->previous_mask; @@ -131,11 +206,14 @@ int64_t aefd_fork_parent(struct aefd_fork *f, int64_t native_result) static int child_grants(struct aefd_fork *f, struct aefd_startup_auxv grants, long pid, long parent) { - if (!aether_fs_session_child_pid(pid) || parent != f->parent_pid || - grants.memory == f->parent.memory || !grants.input || !grants.output || - grants.input == grants.output || grants.input == f->parent.input || - grants.input == f->parent.output || grants.output == f->parent.input || - grants.output == f->parent.output) + if (!aether_fs_session_child_pid(pid) || +#ifdef AEFD_ZSH_PROCESS_BINDINGS + (pid - 3) % 7 != 0 || +#endif + parent != f->parent_pid || grants.memory == f->parent.memory || + !grants.input || !grants.output || grants.input == grants.output || + grants.input == f->parent.input || grants.input == f->parent.output || + grants.output == f->parent.input || grants.output == f->parent.output) return 0; unsigned value = (unsigned)pid - 1; return grants.memory == (UINT64_C(0x40000000) | (uint64_t)(value / 7) << 8 | @@ -158,7 +236,14 @@ int64_t aefd_fork_child(struct aefd_fork *f, struct aefd_startup_auxv grants, unsigned history[2] = {0}; int borrowed = 0, reset = 0, constructed = 0; if (!f->call || !f->sequence || - aefd_word(b->scratch + 296, 0) != f->sequence) + aefd_word(b->scratch + +#ifdef AEFD_ZSH_PROCESS_BINDINGS + 16 +#else + 296 +#endif + , + 0) != f->sequence) goto failed; int64_t pid = f->call(f->owner, 0x10027, 0, 0, 0, 0); int64_t parent = f->call(f->owner, 0x1006e, 0, 0, 0, 0); @@ -172,15 +257,33 @@ int64_t aefd_fork_child(struct aefd_fork *f, struct aefd_startup_auxv grants, borrowed = 1; if (aefd_native_enter(&guard)) goto failed; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + memmove(b->context.workspace, b->scratch, AEFD_FORK_REQUEST_BYTES); +#else memmove(b->context.workspace, b->scratch, AEFD_STARTUP_BYTES); +#endif memset(b->scratch, 0, AEFD_STARTUP_BYTES); +#ifdef AEFD_ZSH_PROCESS_BINDINGS + if (!move(aefd_native_io(&guard), b->scratch, AEFD_STARTUP_BYTES, 0, + &f->budget, 1)) +#else if (!move(aefd_native_io(&guard), b->scratch, 304, 0, &f->budget, 1)) +#endif goto failed; struct aether_fs_session_identity identity; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + struct aefd_zsh_snapshot child; + if (!aefd_zsh_child_match(b->context.workspace, AEFD_FORK_REQUEST_BYTES, + f->parent, f->parent_pid, b->scratch, grants, + (long)pid, &child)) + goto failed; + identity = child.identity; +#else if (!aefd_fork_child_match(b->context.workspace, 304, f->parent, f->parent_pid, b->scratch, 304, grants, (long)pid, &identity)) goto failed; +#endif for (unsigned i = 0; i < 7; ++i) { const unsigned char *raw = b->scratch + 176 + i * 16; sources[i] = (struct aefd_native_source){ @@ -205,7 +308,15 @@ int64_t aefd_fork_child(struct aefd_fork *f, struct aefd_startup_auxv grants, guard.active = 0; guard.initialized = 0; b->identity = identity; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + b->context.mode_control = aether_fs_session_u32(b->scratch + 300); + for (unsigned i = 0; i < 4; ++i) + b->credentials[i] = aether_fs_session_u32(b->scratch + 304 + i * 4); + if (!aefd_zsh_receipt_encode(b->scratch, "AEZFC001", f->sequence, + (long)pid) || +#else if (!aefd_fork_ready_encode(b->scratch, 32, f->sequence, (long)pid) || +#endif !move(aefd_native_io(&b->context.native), b->scratch, 32, 1, &f->budget, 0)) goto failed; diff --git a/userland/experiments/filesystem-service/fd-fork-stage.h b/userland/experiments/filesystem-service/fd-fork-stage.h index cc1b4a8..8629647 100644 --- a/userland/experiments/filesystem-service/fd-fork-stage.h +++ b/userland/experiments/filesystem-service/fd-fork-stage.h @@ -33,6 +33,13 @@ typedef int (*aefd_fork_rebind)(void *, struct aefd_startup_auxv parent, /* Parent: mask whole snapshot/send; leave guard held on success. The caller * must perform exactly one actual native park next. FAILED requires direct * fail-stop; never return it as a known no-child errno or call libc finish. */ +#ifdef AEFD_ZSH_PROCESS_BINDINGS +/* Caller holds the whole context mask; no guard, transport or admission here. + * Validation failure clears the local snapshot; precondition failure leaves it + * untouched. Storage may be the boot scratch body. */ +int64_t aefd_zsh_capture(struct aefd_startup *, struct aefd_startup_auxv, long, + long, unsigned char[336]); +#endif int64_t aefd_fork_arm(struct aefd_fork *, struct aefd_startup_auxv); /* Actual positive native child result or raw thaw-11; consumes one correlated * owner disposition. IDLE result may return upstream; FAILED must fail-stop. */ diff --git a/userland/experiments/filesystem-service/fd-libc-bindings.c b/userland/experiments/filesystem-service/fd-libc-bindings.c index 5a5dc55..2391c72 100644 --- a/userland/experiments/filesystem-service/fd-libc-bindings.c +++ b/userland/experiments/filesystem-service/fd-libc-bindings.c @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: BSD-2-Clause */ #include "fd-libc.h" -#ifdef AEFD_ZSH_DIAGNOSTIC +#if defined(AEFD_ZSH_DIAGNOSTIC) || defined(AEFD_ZSH_PROCESS_BINDINGS) #include "fd-zsh-external.h" #endif #include @@ -31,6 +31,10 @@ void *aefd_test_malloc(size_t); int chdir(const char *); char *getcwd(char *, size_t); #endif +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#define waitpid aefd_test_waitpid +pid_t waitpid(pid_t, int *, int); +#endif #define lseek aefd_test_lseek #define __aether_io aefd_test_io int open(const char *, int, ...); @@ -45,7 +49,8 @@ long __aether_io(unsigned long, unsigned long, unsigned long, unsigned long); _Static_assert(sizeof(long) == 8 && sizeof(off_t) == 8, "selected64-bit libc ABI"); /* One concrete stable off-stack owner. Never reset/copy after admission. */ -#if defined(AEFD_EXTERNAL_STARTUP) && defined(AEFD_FORK_BINDINGS) +#if defined(AEFD_EXTERNAL_STARTUP) && defined(AEFD_FORK_BINDINGS) && \ + !defined(AEFD_ZSH_PROCESS_BINDINGS) #error "external startup cannot select fork-owned storage" #endif #ifdef AEFD_EXTERNAL_STARTUP @@ -70,6 +75,9 @@ static long checked(int64_t result, int saved) int64_t aefd_shell_start(void) { int saved = errno; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + int64_t result = aefd_zsh_admit(); +#else #ifdef AEFD_FORK_BINDINGS if (!aefd_fork_tls_valid()) { errno = saved; @@ -82,6 +90,7 @@ int64_t aefd_shell_start(void) int64_t result = shell.phase == 2 ? -16 : -5; #else int64_t result = aefd_startup_native(&shell); +#endif #endif errno = saved; return result; @@ -243,7 +252,12 @@ long __aether_waitpid(int pid, int *status, int options) long result = -EINVAL; if (shell.phase != 2 || shell.context.state != 1 || shell.identity.slot != 1 || (pid != -1 && pid <= 0) || +#ifdef AEFD_ZSH_PROCESS_BINDINGS + (options & ~11) || + (pid != -1 && (pid < 10 || pid > 108 || (pid - 3) % 7 != 0))) +#else (options != 0 && options != WNOHANG)) +#endif return result; if (shell.context.native.active || shell.context.native.busy) return -EBUSY; @@ -267,7 +281,18 @@ long __aether_waitpid(int pid, int *status, int options) : (int64_t)raw - INT64_C(4294967296); int valid = (value > 0 && aether_fs_session_child_pid((long)value) && (pid == -1 || value == pid)) || +#ifdef AEFD_ZSH_PROCESS_BINDINGS + (value == 0 && (options & WNOHANG)) || value == -ECHILD; + if (value > 0) { + uint32_t packed = aether_fs_session_u32(frame + 4); + valid = valid && value >= 10 && value <= 108 && + (value - 3) % 7 == 0 && + ((packed <= 0xff00 && !(packed & 0xff)) || + (packed > 0 && packed < 127)); + } +#else (value == 0 && options == WNOHANG) || value == -ECHILD; +#endif if (value <= 0 && aether_fs_session_u32(frame + 4)) valid = 0; for (unsigned i = 8; i < 32; ++i) @@ -290,6 +315,13 @@ long __aether_waitpid(int pid, int *status, int options) return result; } +#ifdef AEFD_ZSH_PROCESS_BINDINGS +pid_t waitpid(pid_t pid, int *status, int options) +{ + int saved = errno; + return (pid_t)checked(__aether_waitpid((int)pid, status, options), saved); +} +#endif #ifdef AEFD_CWD_WIRE int chdir(const char *path) { diff --git a/userland/experiments/filesystem-service/fd-protocol.h b/userland/experiments/filesystem-service/fd-protocol.h index acf9828..ee183f2 100644 --- a/userland/experiments/filesystem-service/fd-protocol.h +++ b/userland/experiments/filesystem-service/fd-protocol.h @@ -5,11 +5,23 @@ #include #include +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#ifndef AEFD_FORK_BINDINGS +#define AEFD_FORK_BINDINGS +#endif +#ifndef AEFD_EXEC_BINDINGS +#define AEFD_EXEC_BINDINGS +#endif +#ifndef AEFD_ZSH_FILESYSTEM_BINDINGS +#define AEFD_ZSH_FILESYSTEM_BINDINGS +#endif +#endif #ifdef AEFD_ZSH_FILESYSTEM_BINDINGS #if defined(AEFD_PATH_WIRE) || defined(AEFD_DUP2_WIRE) || \ defined(AEFD_CWD_WIRE) || defined(AEFD_WRITE_WIRE) || \ - defined(AEFD_FORK_BINDINGS) || defined(AEFD_EXEC_BINDINGS) || \ - defined(AEFD_EXEC_WIRE) + defined(AEFD_EXEC_WIRE) || \ + (!defined(AEFD_ZSH_PROCESS_BINDINGS) && \ + (defined(AEFD_FORK_BINDINGS) || defined(AEFD_EXEC_BINDINGS))) #error "Zsh filesystem selection excludes historical authority profiles" #endif #ifndef AEFD_TERMINAL_CANONICAL_BINDINGS @@ -95,7 +107,8 @@ #ifndef AEFD_TERMINAL_BINDINGS #define AEFD_TERMINAL_BINDINGS #endif -#if defined(AEFD_FORK_BINDINGS) || defined(AEFD_EXEC_WIRE) +#if !defined(AEFD_ZSH_PROCESS_BINDINGS) && \ + (defined(AEFD_FORK_BINDINGS) || defined(AEFD_EXEC_WIRE)) #error "duplex qualification excludes fork and exec" #endif #endif @@ -153,6 +166,12 @@ #define AEFD_MAP_MAGIC "AEFDT008" #define AEFD_STARTUP_MAGIC "AEFDB008" #endif +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#undef AEFD_MAP_MAGIC +#undef AEFD_STARTUP_MAGIC +#define AEFD_MAP_MAGIC "AEFDT009" +#define AEFD_STARTUP_MAGIC "AEFDB009" +#endif #define AEFD_FRAME 64U #define AEFD_READ_MAX 4096U diff --git a/userland/experiments/filesystem-service/fd-startup.c b/userland/experiments/filesystem-service/fd-startup.c index b64f46c..6e87347 100644 --- a/userland/experiments/filesystem-service/fd-startup.c +++ b/userland/experiments/filesystem-service/fd-startup.c @@ -64,7 +64,9 @@ int64_t aefd_startup_start(struct aefd_startup *b, aefd_native_call call, if (!aether_fs_session_decode(raw + 16, 32, observed.memory, (long)pid, (long)parent, &identity) || aether_fs_session_u32(raw + 300) != -#ifdef AEFD_TERMINAL_MODE_BINDINGS +#ifdef AEFD_ZSH_PROCESS_BINDINGS + (identity.slot == 1 ? AEFD_MODE_CONTROL : 0) +#elif defined(AEFD_TERMINAL_MODE_BINDINGS) AEFD_MODE_CONTROL #else 0 @@ -73,7 +75,13 @@ int64_t aefd_startup_start(struct aefd_startup *b, aefd_native_call call, goto cleanup; uint32_t factory = aether_fs_session_u32(raw + 296); #ifdef AEFD_ZSH_FILESYSTEM_BINDINGS - if (identity.slot != 1 || factory || + if ( +#ifdef AEFD_ZSH_PROCESS_BINDINGS + (identity.slot != 1 && identity.slot != 2) || + factory != (identity.slot == 1 ? UINT32_C(0x76050001) : 0) || +#else + identity.slot != 1 || factory || +#endif aether_fs_session_u32(raw + 320) != 1 || aether_fs_session_u32(raw + 324) || aefd_word(raw + 328, 0)) goto cleanup; diff --git a/userland/experiments/filesystem-service/fd-startup.h b/userland/experiments/filesystem-service/fd-startup.h index dd34fa3..d924c42 100644 --- a/userland/experiments/filesystem-service/fd-startup.h +++ b/userland/experiments/filesystem-service/fd-startup.h @@ -20,7 +20,12 @@ struct aefd_startup_auxv { struct aefd_startup { struct aefd_context context; struct aether_fs_session_identity identity; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + unsigned char + scratch[368]; /* Selected fork prefix plus complete startup. */ +#else unsigned char scratch[AEFD_STARTUP_BYTES]; +#endif #ifdef AEFD_ZSH_FILESYSTEM_BINDINGS uint32_t credentials[4]; /* Advisory AECAT002 values, never authority. */ #endif diff --git a/userland/experiments/filesystem-service/fd-zsh-external.c b/userland/experiments/filesystem-service/fd-zsh-external.c index cda58c3..69b29be 100644 --- a/userland/experiments/filesystem-service/fd-zsh-external.c +++ b/userland/experiments/filesystem-service/fd-zsh-external.c @@ -2,6 +2,9 @@ #define _POSIX_C_SOURCE 200809L #include "fd-zsh-external.h" #include "fd-libc.h" +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#include "fd-fork-stage.h" +#endif #include #include #include @@ -14,7 +17,8 @@ !defined(AEFD_EXTERNAL_STARTUP) #error "current Zsh entry requires canonical external startup" #endif -#if defined(AEFD_FORK_BINDINGS) || defined(AEFD_EXEC_BINDINGS) || \ +#if (!defined(AEFD_ZSH_PROCESS_BINDINGS) && \ + (defined(AEFD_FORK_BINDINGS) || defined(AEFD_EXEC_BINDINGS))) || \ defined(AEFD_EXEC_WIRE) || defined(AEFD_DUP2_WIRE) || \ defined(AEFD_WRITE_WIRE) || defined(AEFD_CWD_WIRE) #error "current Zsh entry excludes historical process/provider selections" @@ -24,9 +28,25 @@ _Noreturn void __real__exit(int); _Noreturn void __wrap__Exit(int); _Noreturn void __wrap__Exit(int status) { __wrap__exit(status); } /* Internal borrows point into this object. Never move/reset/reinitialize it. */ +#ifdef AEFD_ZSH_PROCESS_BINDINGS +static struct aefd_fork process; +#define startup process.boot +struct aefd_fork *aefd_shell_fork_context(void) { return &process; } +#else static struct aefd_startup startup; +#endif static _Thread_local unsigned entry_fresh = 1; struct aefd_startup *aefd_shell_external_startup(void) { return &startup; } +#ifdef AEFD_ZSH_PROCESS_BINDINGS +int aefd_zsh_tls_valid(void) { return entry_fresh == 0; } +int64_t aefd_zsh_admit(void) +{ + if (entry_fresh != 1) + return -16; + entry_fresh = 0; + return aefd_startup_native(&startup); +} +#endif #ifdef AEFD_ZSH_FILESYSTEM_BINDINGS static size_t path_length(const char *); static int entry_path(const char *); @@ -346,6 +366,7 @@ int aether_zsh_directory_probe(const char *path, return (int)libc_result(result, saved); } #endif +#ifndef AEFD_ZSH_HELPER_ENTRY static int bootstrap(void) { struct sigaction action = {0}; @@ -388,10 +409,14 @@ int __wrap_main(int argc, char **argv) { (void)argc; (void)argv; +#ifdef AEFD_ZSH_PROCESS_BINDINGS + if (aefd_zsh_admit() || !live() || +#else if (entry_fresh != 1) failed(); entry_fresh = 0; if (aefd_startup_native(&startup) || !live() || +#endif startup.identity.slot != 1 || startup.identity.epoch != startup.identity.shell_generation || #ifdef AEFD_ZSH_FILESYSTEM_BINDINGS @@ -409,3 +434,4 @@ int __wrap_main(int argc, char **argv) char *arguments[] = {program, no_rcs, interactive, no_monitor, NULL}; __wrap__exit(__real_main(4, arguments)); } +#endif /* !AEFD_ZSH_HELPER_ENTRY */ diff --git a/userland/experiments/filesystem-service/fd-zsh-external.h b/userland/experiments/filesystem-service/fd-zsh-external.h index 9edbae0..ef5042b 100644 --- a/userland/experiments/filesystem-service/fd-zsh-external.h +++ b/userland/experiments/filesystem-service/fd-zsh-external.h @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: BSD-2-Clause */ #ifndef AETHER_FD_ZSH_EXTERNAL_H #define AETHER_FD_ZSH_EXTERNAL_H +#ifdef AEFD_ZSH_PROCESS_BINDINGS +#include "fd-protocol.h" +#endif #include #include /* Private current-Zsh entry/scoped stdio adapter. At most two readable iovecs; @@ -38,5 +41,10 @@ char *__wrap_getcwd(char *, size_t); int __wrap_tgetent(char *, const char *); int aether_zsh_diagnostic_status(int); #endif +#ifdef AEFD_ZSH_PROCESS_BINDINGS +/* Single selected aggregate supplies parent, copied child and fresh helper. */ +int64_t aefd_zsh_admit(void); +int aefd_zsh_tls_valid(void); +#endif int __wrap_main(int, char **); #endif diff --git a/userland/experiments/filesystem-service/fd-zsh-helper-entry.c b/userland/experiments/filesystem-service/fd-zsh-helper-entry.c new file mode 100644 index 0000000..d484e66 --- /dev/null +++ b/userland/experiments/filesystem-service/fd-zsh-helper-entry.c @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +#include "fd-zsh-external.h" +#include "fd-libc.h" +#include "fd-exec.h" +#if !defined(AEFD_ZSH_PROCESS_BINDINGS) || !defined(AEFD_ZSH_HELPER_ENTRY) || \ + defined(AEFD_ZSH_DIAGNOSTIC) +#error \ + "helper requires its fresh selected process entry without parent diagnostic" +#endif +extern int __real_main(int, char **); +_Noreturn void __real__exit(int); +int __wrap_main(int argc, char **argv) +{ + if (aefd_shell_start() || aefd_shell_role() != 2 || aefd_shell_ready()) + __real__exit(143); + /* CRT arguments/environment come from the exact committed exec record. */ + __wrap__exit(__real_main(argc, argv)); +} diff --git a/userland/experiments/filesystem-service/fd-zsh-process-tests.c b/userland/experiments/filesystem-service/fd-zsh-process-tests.c new file mode 100644 index 0000000..f038577 --- /dev/null +++ b/userland/experiments/filesystem-service/fd-zsh-process-tests.c @@ -0,0 +1,135 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +#include "fd-zsh-process.h" +#include +#include +static struct aefd_startup_auxv parent = {0x40000102, 71, 72}; +static struct aefd_startup_auxv child = {0x40000103, 81, 82}; +static void put32(unsigned char *raw, unsigned at, uint32_t value) +{ + for (unsigned i = 0; i < 4; ++i) + raw[at + i] = (unsigned char)(value >> (i * 8)); +} +static void snapshot(unsigned char raw[336], int copied) +{ + memset(raw, 0, 336); + memcpy(raw, "AEFDB009", 8); + put32(raw, 8, 1); + put32(raw, 12, 336); + memcpy(raw + 16, "AEFSH001", 8); + put32(raw, 24, 29); + put32(raw, 28, 7); + put32(raw, 32, copied ? 2 : 1); + put32(raw, 36, 1); + put32(raw, 40, copied ? 9 : 7); + put32(raw, 44, 1); + memcpy(raw + 48, "AEFDT009", 8); + aefd_put(raw + 48, 1, copied ? 32 : 31); + aefd_put(raw + 48, 2, 16); + for (unsigned i = 0; i < 3; ++i) { + raw[112 + i * 4] = 4; + raw[113 + i * 4] = (unsigned char)i; + put32(raw, 176 + i * 16, 0x7a000001 | i << 16); + put32(raw, 180 + i * 16, i); + put32(raw, 184 + i * 16, i ? 2 : 1); + put32(raw, 188 + i * 16, 16); + } + raw[124] = raw[128] = 2; + raw[125] = 3; + raw[129] = 4; + raw[132] = 3; + raw[133] = 3; + raw[152] = 5; + raw[155] = 1; + for (unsigned i = 3; i < 5; ++i) { + put32(raw, 176 + i * 16, 0x79000100 | (i - 2)); + put32(raw, 180 + i * 16, i); + put32(raw, 184 + i * 16, i == 3 ? 1 : 2); + put32(raw, 188 + i * 16, 16); + } + put32(raw, 288, 1); + put32(raw, 296, copied ? 0 : 0x76050001); + put32(raw, 300, copied ? 0 : 0x7d000001); + for (unsigned i = 0; i < 4; ++i) + put32(raw, 304 + i * 4, 1000); + put32(raw, 320, 1); +} +int main(void) +{ + unsigned char original[336], copied[336], frame[672], saved[672], + arguments[272] = {0}; + snapshot(original, 0); + snapshot(copied, 1); + struct aefd_zsh_snapshot view = {0}; + assert(aefd_zsh_snapshot_decode(original, 336, parent, 9, 7, &view)); + assert(view.identity.slot == 1 && view.generation == 31); + assert(aefd_zsh_snapshot_decode(copied, 336, child, 10, 9, &view)); + assert(view.identity.slot == 2 && view.generation == 32); + assert(aefd_zsh_fork_encode(frame, 368, original, parent, 9, 7, 41)); + assert(!memcmp(frame + 32, original, 336)); + uint64_t sequence = 0; + assert( + aefd_zsh_fork_decode(frame, 368, parent, 9, 7, 31, &view, &sequence) && + sequence == 41); + assert( + aefd_zsh_child_match(frame, 368, parent, 9, copied, child, 10, &view)); + aefd_put(arguments, 0, 2); + memcpy(arguments + 16, "/inputs/helper", 14); + memcpy(arguments + 48, "status", 6); + assert(aefd_zsh_exec_encode(frame, 672, copied, child, 10, 9, 42, + "/inputs/helper", arguments)); + assert(!memcmp(frame + 32, copied, 336) && + !memcmp(frame + 400, arguments, 272)); + assert( + aefd_zsh_exec_decode(frame, 672, child, 10, 9, 32, &view, &sequence) && + sequence == 42); + memcpy(saved, frame, sizeof(frame)); + for (size_t n = 0; n < sizeof(frame); ++n) + assert(!aefd_zsh_exec_decode(frame, n, child, 10, 9, 32, &view, + &sequence)); + for (unsigned at = 0; at < 32; ++at) { + memcpy(frame, saved, sizeof(frame)); + frame[at] ^= 0x80; + if (at >= 16 && at < 24) + continue; /* Other nonzero sequence is valid data. */ + assert(!aefd_zsh_exec_decode(frame, 672, child, 10, 9, 32, &view, + &sequence)); + } + memcpy(frame, saved, sizeof(frame)); + frame[39] = '8'; + assert( + !aefd_zsh_exec_decode(frame, 672, child, 10, 9, 32, &view, &sequence)); + memcpy(frame, saved, sizeof(frame)); + frame[400 + 271] = 1; + assert( + !aefd_zsh_exec_decode(frame, 672, child, 10, 9, 32, &view, &sequence)); + assert( + !aefd_zsh_exec_decode(saved, 672, child, 10, 9, 33, &view, &sequence)); + assert(!aefd_zsh_exec_encode(frame, 672, original, parent, 9, 7, 44, + "/inputs/helper", arguments)); + snapshot(copied, 1); + copied[300] = 1; + assert(!aefd_zsh_snapshot_decode(copied, 336, child, 10, 9, &view)); + snapshot(copied, 1); + copied[155] = 0; + assert(!aefd_zsh_snapshot_decode(copied, 336, child, 10, 9, &view)); + snapshot(copied, 1); + copied[324] = 1; + assert(!aefd_zsh_snapshot_decode(copied, 336, child, 10, 9, &view)); + snapshot(copied, 1); + put32(copied, 32, 3); + assert(!aefd_zsh_snapshot_decode(copied, 336, child, 10, 9, &view)); + unsigned char receipt[32]; + assert(aefd_zsh_receipt_encode(receipt, "AEZFR001", 41, 10)); + int64_t result = 0; + assert(aefd_zsh_receipt_decode(receipt, "AEZFR001", 41, &result) && + result == 10); + assert(!aefd_zsh_receipt_decode(receipt, "AEZER001", 41, &result)); + assert(!aefd_zsh_receipt_encode(receipt, "AEZER001", 41, 7)); + assert(!aefd_zsh_receipt_encode(receipt, "AEZFC001", 41, -13)); + assert(!aefd_zsh_receipt_encode(receipt, "AEZFR001", 41, 11)); + assert(!aefd_zsh_receipt_encode(receipt, "AEZFR001", UINT64_MAX, 10)); + receipt[31] = 1; + assert(!aefd_zsh_receipt_decode(receipt, "AEZFR001", 41, &result)); + puts("ZSH_PROCESS_CODEC_PASS"); + return 0; +} diff --git a/userland/experiments/filesystem-service/fd-zsh-process.c b/userland/experiments/filesystem-service/fd-zsh-process.c new file mode 100644 index 0000000..a539ac7 --- /dev/null +++ b/userland/experiments/filesystem-service/fd-zsh-process.c @@ -0,0 +1,256 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +#include "fd-zsh-process.h" +static int64_t no_transport(void *owner, uint64_t op, uint64_t a, uint64_t b, + uint64_t c, uint64_t d) +{ + (void)owner; + (void)op; + (void)a; + (void)b; + (void)c; + (void)d; + return -5; +} +static int sequence_ok(uint64_t sequence) +{ + return sequence && sequence != UINT64_MAX; +} +int aefd_zsh_snapshot_decode(const unsigned char *raw, size_t size, + struct aefd_startup_auxv observed, long pid, + long parent, struct aefd_zsh_snapshot *out) +{ + struct aether_fs_session_identity identity; + struct aefd_bindings map; + if (!raw || !out || size != 336 || memcmp(raw, "AEFDB009", 8) || + aether_fs_session_u32(raw + 8) != 1 || + aether_fs_session_u32(raw + 12) != 336 || !observed.input || + !observed.output || observed.input == observed.output || + !aether_fs_session_decode(raw + 16, 32, observed.memory, pid, parent, + &identity) || + (identity.slot != 1 && identity.slot != 2) || + !aefd_bindings_decode(raw + 48, 128, aefd_word(raw + 48, 1), &map) || + aether_fs_session_u32(raw + 296) != + (identity.slot == 1 ? UINT32_C(0x76050001) : 0) || + aether_fs_session_u32(raw + 300) != + (identity.slot == 1 ? AEFD_MODE_CONTROL : 0) || + aether_fs_session_u32(raw + 320) != 1 || + aether_fs_session_u32(raw + 324) || aefd_word(raw + 328, 0)) + return 0; + for (unsigned i = 0; i < 4; ++i) + if (aether_fs_session_u32(raw + 304 + i * 4) != 1000) + return 0; + unsigned seen = 0; + for (unsigned fd = 0; fd < 16; ++fd) { + struct aefd_binding slot = map.slots[fd]; + if (slot.kind == AEFD_BIND_CONSOLE) + return 0; + if (slot.kind == AEFD_BIND_DUPLEX_TERMINAL) + seen |= 3U; + else if (slot.kind == AEFD_BIND_TERMINAL || slot.kind == AEFD_BIND_PIPE) + seen |= 1U << slot.selector; + } + struct aefd_native_source sources[7]; + for (unsigned i = 0; i < 7; ++i) { + const unsigned char *s = raw + 176 + i * 16; + sources[i] = (struct aefd_native_source){ + aether_fs_session_u32(s), aether_fs_session_u32(s + 4), + aether_fs_session_u32(s + 8), aether_fs_session_u32(s + 12)}; + if (!!sources[i].reference != !!(seen & (1U << i)) || + (i < 3 && sources[i].reference && + sources[i].reference != (UINT32_C(0x7a000001) | i << 16))) + return 0; + } + unsigned history[2] = {aether_fs_session_u32(raw + 288), + aether_fs_session_u32(raw + 292)}; + struct aefd_native native = {0}; + if (!aefd_native_init(&native, no_transport, NULL, observed.input, + observed.output, sources, history, + aether_fs_session_u32(raw + 296))) + return 0; + *out = (struct aefd_zsh_snapshot){raw, identity, map.generation}; + return 1; +} +static int header(const unsigned char *raw, size_t size, const char magic[8], + size_t expected, uint64_t *sequence) +{ + if (!raw || size != expected || memcmp(raw, magic, 8) || + aether_fs_session_u32(raw + 8) != 1 || + aether_fs_session_u32(raw + 12) != expected || + !sequence_ok(aefd_word(raw, 2)) || aefd_word(raw, 3)) + return 0; + *sequence = aefd_word(raw, 2); + return 1; +} +static void prefix(unsigned char *out, const char magic[8], size_t size, + uint64_t sequence) +{ + memset(out, 0, 32); + memcpy(out, magic, 8); + aefd_put(out, 1, 1 | (uint64_t)size << 32); + aefd_put(out, 2, sequence); +} +int aefd_zsh_fork_encode(unsigned char *out, size_t size, + const unsigned char snapshot[336], + struct aefd_startup_auxv observed, long pid, + long parent, uint64_t sequence) +{ + struct aefd_zsh_snapshot view; + if (!out || size != 368 || !sequence_ok(sequence) || + !aefd_zsh_snapshot_decode(snapshot, 336, observed, pid, parent, + &view) || + view.identity.slot != 1) + return 0; + memmove(out + 32, snapshot, 336); + prefix(out, "AEZFK001", size, sequence); + return 1; +} +int aefd_zsh_fork_decode(const unsigned char *raw, size_t size, + struct aefd_startup_auxv observed, long pid, + long parent, uint64_t generation, + struct aefd_zsh_snapshot *out, uint64_t *sequence) +{ + struct aefd_zsh_snapshot view; + uint64_t value; + if (!out || !sequence || !header(raw, size, "AEZFK001", 368, &value) || + !aefd_zsh_snapshot_decode(raw + 32, 336, observed, pid, parent, + &view) || + view.identity.slot != 1 || view.generation != generation) + return 0; + *out = view; + *sequence = value; + return 1; +} +int aefd_zsh_child_match(const unsigned char *request, size_t size, + struct aefd_startup_auxv parent, long parent_pid, + const unsigned char startup[336], + struct aefd_startup_auxv child, long child_pid, + struct aefd_zsh_snapshot *out) +{ + struct aefd_zsh_snapshot before, after; + uint64_t sequence; + if (!out || !request || size != 368 || + !aefd_zsh_fork_decode(request, size, parent, parent_pid, 7, + aefd_word(request + 80, 1), &before, &sequence) || + !aefd_zsh_snapshot_decode(startup, 336, child, child_pid, parent_pid, + &after) || + after.identity.slot != 2 || before.generation == after.generation || + parent.memory == child.memory || child.input == parent.input || + child.input == parent.output || child.output == parent.input || + child.output == parent.output || + memcmp(before.raw + 112, startup + 112, 184) || + memcmp(before.raw + 304, startup + 304, 32)) + return 0; + *out = after; + return 1; +} +static int arguments_ok(const unsigned char raw[272]) +{ + if (!raw || aefd_word(raw, 0) < 1 || aefd_word(raw, 0) > 4 || + aefd_word(raw, 1) > 4) + return 0; + for (unsigned i = 0; i < 8; ++i) { + const unsigned char *s = raw + 16 + i * 32; + unsigned count = (unsigned)aefd_word(raw, i < 4 ? 0 : 1); + int used = (i % 4) < count; + size_t end = 0; + while (end < 32 && used && s[end]) + ++end; + if (end == 32) + return 0; + for (size_t j = end; j < 32; ++j) + if (s[j]) + return 0; + } + return 1; +} +int aefd_zsh_exec_encode(unsigned char *out, size_t size, + const unsigned char snapshot[336], + struct aefd_startup_auxv observed, long pid, + long parent, uint64_t sequence, const char *path, + const unsigned char arguments[272]) +{ + struct aefd_zsh_snapshot view; + if (!out || size != 672 || !path || !sequence_ok(sequence) || + !arguments_ok(arguments) || + !aefd_zsh_snapshot_decode(snapshot, 336, observed, pid, parent, + &view) || + view.identity.slot != 2) + return 0; + size_t length = 0; + while (length < 32 && path[length]) + ++length; + if (!length || length == 32) + return 0; + memmove(out + 32, snapshot, 336); + prefix(out, "AEZEX001", size, sequence); + memset(out + 368, 0, 32); + memcpy(out + 368, path, length); + memmove(out + 400, arguments, 272); + return 1; +} +int aefd_zsh_exec_decode(const unsigned char *raw, size_t size, + struct aefd_startup_auxv observed, long pid, + long parent, uint64_t generation, + struct aefd_zsh_snapshot *out, uint64_t *sequence) +{ + struct aefd_zsh_snapshot view; + uint64_t value; + if (!out || !sequence || !header(raw, size, "AEZEX001", 672, &value) || + !aefd_zsh_snapshot_decode(raw + 32, 336, observed, pid, parent, + &view) || + view.identity.slot != 2 || view.generation != generation || + !arguments_ok(raw + 400)) + return 0; + size_t end = 0; + while (end < 32 && raw[368 + end]) + ++end; + if (!end || end == 32) + return 0; + for (size_t j = end; j < 32; ++j) + if (raw[368 + j]) + return 0; + *out = view; + *sequence = value; + return 1; +} +static int receipt_kind(const char magic[8]) +{ + return magic && + (!memcmp(magic, "AEZFR001", 8) || !memcmp(magic, "AEZFC001", 8) || + !memcmp(magic, "AEZER001", 8)); +} +static int receipt_result(const char magic[8], int64_t result) +{ + int child_pid = result >= 10 && result <= 108 && (result - 3) % 7 == 0; + int error = result >= -4095 && result <= -1; + return !memcmp(magic, "AEZFR001", 8) ? child_pid || error + : !memcmp(magic, "AEZFC001", 8) ? child_pid + : error; +} +int aefd_zsh_receipt_encode(unsigned char out[32], const char magic[8], + uint64_t sequence, int64_t result) +{ + if (!out || !receipt_kind(magic) || !sequence_ok(sequence) || + !receipt_result(magic, result)) + return 0; + memset(out, 0, 32); + memcpy(out, magic, 8); + aefd_put(out, 1, sequence); + aefd_put(out, 2, (uint64_t)result); + return 1; +} +int aefd_zsh_receipt_decode(const unsigned char raw[32], const char magic[8], + uint64_t sequence, int64_t *out) +{ + if (!raw || !out || !receipt_kind(magic) || !sequence_ok(sequence) || + memcmp(raw, magic, 8) || aefd_word(raw, 1) != sequence || + aefd_word(raw, 3)) + return 0; + uint64_t result = aefd_word(raw, 2); + int64_t value = result <= INT64_MAX ? (int64_t)result + : -1 - (int64_t)(UINT64_MAX - result); + if (!receipt_result(magic, value)) + return 0; + *out = value; + return 1; +} diff --git a/userland/experiments/filesystem-service/fd-zsh-process.h b/userland/experiments/filesystem-service/fd-zsh-process.h new file mode 100644 index 0000000..42a95d0 --- /dev/null +++ b/userland/experiments/filesystem-service/fd-zsh-process.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +#ifndef AETHER_FD_ZSH_PROCESS_H +#define AETHER_FD_ZSH_PROCESS_H +#include "fd-startup.h" +#ifndef AEFD_ZSH_PROCESS_BINDINGS +#error "Zsh process codec requires the selected process profile" +#endif +#define AEFD_ZSH_FORK_BYTES 368U +#define AEFD_ZSH_EXEC_BYTES 672U +/* Inert checked view. Endpoint/native identity authentication is the caller's + * responsibility. Output publishes only after complete validation; no I/O, + * pointer rebind, resource admission or authority follows from these bytes. */ +struct aefd_zsh_snapshot { + const unsigned char *raw; + struct aether_fs_session_identity identity; + uint64_t generation; +}; +int aefd_zsh_snapshot_decode(const unsigned char *, size_t, + struct aefd_startup_auxv, long, long, + struct aefd_zsh_snapshot *); +/* Encoders permit disjoint input/output or exact in-place snapshot expansion. + * Arguments may also already occupy their exact output field. Path inputs + * and all decoded output records must be disjoint from + * their output/input frame respectively. Invalid input leaves output untouched. + * Fixed authenticated observations and exclusive valid C buffers are required; + * these functions do not park/replay. + */ +int aefd_zsh_fork_encode(unsigned char *, size_t, const unsigned char[336], + struct aefd_startup_auxv, long, long, uint64_t); +int aefd_zsh_fork_decode(const unsigned char *, size_t, + struct aefd_startup_auxv, long, long, uint64_t, + struct aefd_zsh_snapshot *, uint64_t *); +int aefd_zsh_child_match(const unsigned char *, size_t, + struct aefd_startup_auxv, long, + const unsigned char[336], struct aefd_startup_auxv, + long, struct aefd_zsh_snapshot *); +int aefd_zsh_exec_encode(unsigned char *, size_t, const unsigned char[336], + struct aefd_startup_auxv, long, long, uint64_t, + const char *, const unsigned char[272]); +int aefd_zsh_exec_decode(const unsigned char *, size_t, + struct aefd_startup_auxv, long, long, uint64_t, + struct aefd_zsh_snapshot *, uint64_t *); +/* Only the three selected receipt kinds are admitted. Result policy (actual + * copy PID, ready PID or negative refusal) remains with the typed caller. */ +int aefd_zsh_receipt_encode(unsigned char[32], const char[8], uint64_t, + int64_t); +int aefd_zsh_receipt_decode(const unsigned char[32], const char[8], uint64_t, + int64_t *); +#endif diff --git a/userland/experiments/filesystem-service/fd-zsh-runtime-tests.c b/userland/experiments/filesystem-service/fd-zsh-runtime-tests.c new file mode 100644 index 0000000..0991eb6 --- /dev/null +++ b/userland/experiments/filesystem-service/fd-zsh-runtime-tests.c @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +#define _POSIX_C_SOURCE 200809L +#include "fd-zsh-process.h" +#include "fd-exec.h" +#include "fd-zsh-external.h" +#include "fd-libc.h" +#include +#include +#include +#include +#include +/* A fresh fixture is a new isolated synthetic native world. No fixture grants + * authority; actual provider bodies perform all snapshot/rebind validation. */ +struct fixture { + struct aefd_fork *f; + struct aefd_startup_auxv aux; + unsigned char startup[336], child[336], sent[672], reply[32]; + size_t offset, written, limit, write_limit; + long pid, parent; + uint64_t mask; + unsigned calls, closes, rebinds, parks, ready, transactions; + int mode, reenter, rebind_error, park_result; +}; +static struct fixture x; +static struct aefd_fork local; +static unsigned cases; +static int main_calls; +static int helper_check, helper_mode; +static char **expected_argv; +static void put32(unsigned char *raw, unsigned at, uint32_t value) +{ + for (unsigned i = 0; i < 4; ++i) + raw[at + i] = (unsigned char)(value >> (i * 8)); +} +static void snapshot(unsigned char raw[336], int child) +{ + memset(raw, 0, 336); + memcpy(raw, "AEFDB009", 8); + put32(raw, 8, 1); + put32(raw, 12, 336); + memcpy(raw + 16, "AEFSH001", 8); + put32(raw, 24, 29); + put32(raw, 28, 7); + put32(raw, 32, child ? 2 : 1); + put32(raw, 36, 1); + put32(raw, 40, child ? 9 : 7); + put32(raw, 44, 1); + memcpy(raw + 48, "AEFDT009", 8); + aefd_put(raw + 48, 1, child ? 32 : 31); + aefd_put(raw + 48, 2, 16); + for (unsigned i = 0; i < 3; ++i) { + raw[112 + i * 4] = 4; + raw[113 + i * 4] = (unsigned char)i; + put32(raw, 176 + i * 16, 0x7a000001 | i << 16); + put32(raw, 180 + i * 16, i); + put32(raw, 184 + i * 16, i ? 2 : 1); + put32(raw, 188 + i * 16, 16); + } + put32(raw, 296, child ? 0 : 0x76050001); + put32(raw, 300, child ? 0 : 0x7d000001); + for (unsigned i = 0; i < 4; ++i) + put32(raw, 304 + i * 4, 1000); + put32(raw, 320, 1); +} +static int64_t call(void *unused, uint64_t op, uint64_t cap, uint64_t a, + uint64_t b, uint64_t c) +{ + (void)unused; + ++x.calls; + if (op == 0x1000e) { + assert(c == 8 && a); + if (!cap) { + assert(b && *(uint64_t *)(uintptr_t)a == UINT64_MAX); + *(uint64_t *)(uintptr_t)b = x.mask; + x.mask = UINT64_MAX; + } else { + assert(cap == 2 && !b); + x.mask = *(uint64_t *)(uintptr_t)a; + } + return 0; + } + if (op == 0x10027 || op == 0x1006e) + return op == 0x10027 ? x.pid : x.parent; + if (op == 0x24003) { + assert(cap == x.aux.input || cap == x.aux.output); + ++x.closes; + assert(x.closes <= 2); + return 0; + } + if (op == 0x24004) + return 0; + if (op == 0x23003) { + assert(x.mode == 3 && x.written == 672); + ++x.parks; + return x.park_result; + } + if (op == 0x20004) { + ++x.ready; + x.mode = 5; + x.written = x.offset = 0; + return 0; + } + assert(op == 0x24001 || op == 0x24002); + assert(cap == (op == 0x24001 ? x.aux.input : x.aux.output)); + assert(a && b && b <= 16 && !c && x.mask == UINT64_MAX); + if (x.reenter) { + unsigned before = x.calls; + assert(aefd_fork_arm(x.f, x.aux) == -16); + assert(__aether_waitpid(-1, NULL, 11) == -EBUSY); + assert(x.calls == before); + } + size_t count = b > 7 ? 7 : (size_t)b; /* Actual short transport. */ + if (op == 0x24002) { + if (count > x.write_limit - x.written) + count = x.write_limit - x.written; + memcpy(x.sent + x.written, (void *)(uintptr_t)a, count); + x.written += count; + if (x.mode == 5 && x.written == 32) { + assert(aether_fs_session_u32(x.sent) == 3); + memset(x.reply, 0, 32); + x.offset = 0; + } + } else { + const unsigned char *raw = x.mode == 0 ? x.startup + : x.mode == 2 ? x.child + : x.reply; + if (count > x.limit - x.offset) + count = x.limit - x.offset; + memcpy((void *)(uintptr_t)a, raw + x.offset, count); + x.offset += count; + if (x.mode == 5 && x.offset == 32) { + ++x.transactions; + x.written = 0; + } + } + return (int64_t)count; +} +static void setup(int global) +{ + memset(&x, 0, sizeof x); + x.f = global ? aefd_shell_fork_context() : &local; + memset(x.f, 0, sizeof *x.f); + assert(!global || &x.f->boot == aefd_shell_external_startup()); + snapshot(x.startup, 0); + snapshot(x.child, 1); + x.aux = (struct aefd_startup_auxv){0x40000102, 71, 72}; + x.pid = 9; + x.parent = 7; + x.mask = 0x1234; + x.limit = 336; + x.write_limit = 672; + assert(aefd_startup_start(&x.f->boot, call, NULL, x.aux) == 0); + assert(x.mask == 0x1234 && !x.closes); + x.mode = 1; + x.offset = x.written = 0; + x.limit = 32; + x.park_result = -11; +} +static void arm(void) +{ + assert(aefd_fork_arm(x.f, x.aux) == 0); + assert(x.written == 368 && x.f->phase == AEFD_FORK_ARMED); + struct aefd_zsh_snapshot view; + uint64_t sequence; + assert( + aefd_zsh_fork_decode(x.sent, 368, x.aux, 9, 7, 31, &view, &sequence)); + assert(sequence == x.f->sequence && !memcmp(x.sent + 32, x.startup, 336)); +} +static int rebind(void *unused, struct aefd_startup_auxv parent, + struct aefd_startup_auxv child) +{ + (void)unused; + ++x.rebinds; + assert(parent.memory == 0x40000102 && x.f->boot.phase == 4); + unsigned calls = x.calls; + assert(aefd_context_flags(&x.f->boot.context, 0) == -5); + assert(calls == x.calls); + if (x.rebind_error) + return 0; + x.aux = child; + return 1; +} +static int64_t become_child(void) +{ + arm(); + x.mode = 2; + x.pid = 10; + x.parent = 9; + x.offset = x.written = 0; + x.limit = 336; + return aefd_fork_child(x.f, (struct aefd_startup_auxv){0x40000103, 81, 82}, + rebind, NULL); +} +static void fork_cases(void) +{ + for (unsigned which = 0; which < 5; ++which) { + setup(1); + arm(); + assert(aefd_zsh_receipt_encode(x.reply, "AEZFR001", 1, + which == 1 ? -12 : 10)); + if (which == 2) + x.reply[8] ^= 1; + if (which == 3) + x.reply[24] = 1; + if (which == 4) + x.limit = 31; + int64_t result = aefd_fork_parent(x.f, which == 1 ? -11 : 10); + assert(result == (which >= 2 ? -5 : which == 1 ? -12 : 10)); + assert(x.closes == (which >= 2 ? 2U : 0U)); + ++cases; + } + for (unsigned which = 0; which < 5; ++which) { + setup(0); + if (which == 1) + x.child[39] = '8'; + if (which == 2) + x.child[304] ^= 1; + if (which == 3) + x.child[56] = 31; /* stale table generation */ + if (which == 4) + x.rebind_error = 1; + assert(become_child() == (which ? -5 : 0)); + assert(!x.closes || which); + if (!which) { + int64_t result; + assert(aefd_zsh_receipt_decode(x.sent, "AEZFC001", 1, &result)); + assert(result == 10 && x.rebinds == 1); + struct aefd_context *c = &x.f->boot.context; + assert(c->session.map == &c->map && + c->session.client == &c->client); + assert(c->client.workspace == c->workspace && + c->client.io.context == &c->native); + assert(c->native.factory == 0 && c->mode_control == 0); + assert(x.f->boot.credentials[3] == 1000 && c->map.generation == 32); + } + unsigned before = x.calls; + assert(aefd_fork_child(x.f, x.aux, rebind, NULL) == -16 && + before == x.calls); + ++cases; + } + setup(0); + x.f->counter = UINT64_MAX - 1; + unsigned before = x.calls; + assert(aefd_fork_arm(x.f, x.aux) == -75 && before == x.calls && !x.closes); + ++cases; + setup(0); + x.write_limit = 367; + assert(aefd_fork_arm(x.f, x.aux) == -5 && x.closes == 2); + ++cases; +} +static void pipe_alias_case(void) +{ + setup(0); + x.mode = 6; + memset(x.reply, 0, 32); + put32(x.reply, 4, 1); + put32(x.reply, 8, 0x79000101); + put32(x.reply, 12, 0x79000102); + put32(x.reply, 16, 3); + put32(x.reply, 20, 4); + put32(x.reply, 24, 256); + put32(x.reply, 28, 16); + int pair[2] = {-1, -1}; + assert(aefd_context_pipe(&x.f->boot.context, pair, 1) == 0); + assert(pair[0] == 3 && pair[1] == 4 && x.sent[0] == 82); + assert(aefd_context_open_duplex(&x.f->boot.context, 0) == 5); + for (unsigned which = 0; which < 2; ++which) { + unsigned char *raw = which ? x.child : x.startup; + raw[124] = raw[128] = 2; + raw[125] = 3; + raw[129] = 4; + raw[126] = raw[130] = 1; + raw[132] = 5; + raw[135] = 1; + for (unsigned i = 3; i < 5; ++i) { + put32(raw, 176 + i * 16, 0x79000100 | (i - 2)); + put32(raw, 180 + i * 16, i); + put32(raw, 184 + i * 16, i == 3 ? 1 : 2); + put32(raw, 188 + i * 16, 16); + } + put32(raw, 288, 1); + } + x.mode = 1; + x.offset = x.written = 0; + x.limit = 32; + assert(become_child() == 0); + struct aefd_context *c = &x.f->boot.context; + assert(c->map.slots[3].cloexec == 1 && c->map.slots[4].cloexec == 1); + assert(c->map.slots[5].reserved == 1 && c->native.generations[0] == 1); + assert(c->native.sources[3].reference == 0x79000101 && + c->native.factory == 0); + ++cases; +} +static void exec_cases(void) +{ + char arg0[] = "/inputs/helper", arg1[] = "status", env0[] = "TERM=vt100"; + char *argv[] = {arg0, arg1, NULL}, *envp[] = {env0, NULL}; + for (unsigned which = 0; which < 5; ++which) { + setup(0); + assert(become_child() == 0); + x.mode = 3; + x.limit = 32; + x.offset = x.written = 0; + assert(aefd_zsh_receipt_encode(x.reply, "AEZER001", 2, -13)); + if (which == 1) + x.reply[8] ^= 1; + if (which == 2) + x.limit = 31; + if (which == 3) + x.park_result = 0; + if (which == 4) + x.f->boot.context.map.slots[0].kind = 255; + errno = 71; + assert(aefd_exec(x.f, x.aux, arg0, argv, envp) == (which ? -5 : -13)); + if (which == 4) { + assert(x.f->phase == AEFD_FORK_FAILED && x.closes == 2); + assert(errno == 71 && !x.parks && !x.written && x.f->counter == 1); + ++cases; + continue; + } + assert(errno == 71 && x.parks == 1 && x.written == 672); + struct aefd_zsh_snapshot view; + uint64_t sequence; + assert(aefd_zsh_exec_decode(x.sent, 672, x.aux, 10, 9, 32, &view, + &sequence)); + assert(sequence == 2 && aefd_word(x.sent + 400, 0) == 2); + assert(aefd_word(x.sent + 400, 1) == 1 && + !strcmp((char *)x.sent + 448, "status")); + assert(x.closes == (which ? 2U : 0U)); + ++cases; + } +} +pid_t aefd_test_waitpid(pid_t, int *, int); +static void wait_cases(void) +{ + for (unsigned which = 0; which < 8; ++which) { + setup(1); + memset(x.reply, 0, 32); + int result = which == 1 ? 0 + : which == 2 ? -ECHILD + : which == 3 ? 11 + : 10; + put32(x.reply, 0, (uint32_t)result); + if (result > 0) + put32(x.reply, 4, 7U << 8); + if (which == 4) + x.reply[8] = 1; + if (which == 5) + put32(x.reply, 4, 0x7f); + if (which == 6) + x.limit = 31; + int status = 0x12345678; + x.reenter = 1; + errno = 71; + pid_t observed = aefd_test_waitpid(-1, &status, 11); + assert(observed == (which >= 2 && which <= 6 ? -1 : result)); + assert(errno == (which >= 3 && which <= 6 ? EIO + : which == 2 ? ECHILD + : 71)); + assert(status == (which == 0 || which == 7 ? 7 << 8 : 0x12345678)); + assert(x.closes == (which >= 3 && which <= 6 ? 2U : 0U)); + assert(x.sent[0] == 68 && aether_fs_session_u32(x.sent + 8) == 11); + ++cases; + } + setup(1); + unsigned before = x.calls; + int status = 123; + assert(__aether_waitpid(-1, &status, 4) == -EINVAL); + assert(__aether_waitpid(11, &status, 11) == -EINVAL); + assert(status == 123 && before == x.calls); + ++cases; +} +int64_t aefd_startup_native(struct aefd_startup *boot) +{ + assert(boot == aefd_shell_external_startup()); + return aefd_startup_start(boot, call, NULL, x.aux); +} +int __real_main(int argc, char **argv) +{ + ++main_calls; + assert(argc == 2 && argv == expected_argv && !argv[2]); + assert(!strcmp(argv[0], "/inputs/helper") && !strcmp(argv[1], "status")); + assert(aefd_shell_role() == 2 && aefd_zsh_tls_valid()); + assert(aefd_shell_start() == -16); + return 7; +} +_Noreturn void __real__exit(int status) +{ + if (helper_mode) { + assert(helper_check && status == 143 && !main_calls && !x.ready); + assert(x.closes == (helper_mode == 2 ? 0U : 2U)); + printf("selected helper guard %d passed\n", helper_mode); + fflush(NULL); + _Exit(0); + } + assert(helper_check && status == 7 && main_calls == 1 && x.ready == 1); + assert(x.transactions == 3 && x.closes == 2 && x.mask == 0x1234); + assert(aefd_zsh_admit() == -16); + printf("selected C runtime %u cases passed\n", cases + 1); + fflush(NULL); + _Exit(0); +} +int __wrap_main(int, char **); +static void helper_case(void) +{ + memset(&x, 0, sizeof x); + x.f = aefd_shell_fork_context(); + memset(x.f, 0, sizeof *x.f); + snapshot(x.startup, 1); + x.aux = (struct aefd_startup_auxv){0x40000103, 81, 82}; + x.pid = 10; + x.parent = 9; + x.limit = 336; + x.write_limit = 672; + x.mask = 0x1234; + if (helper_mode == 1) + x.startup[7] = '8'; + if (helper_mode == 2) { + snapshot(x.startup, 0); + x.aux = (struct aefd_startup_auxv){0x40000102, 71, 72}; + x.pid = 9; + x.parent = 7; + } + if (helper_mode == 3) + x.limit = 335; + assert(!aefd_zsh_tls_valid()); + char a[] = "/inputs/helper", b[] = "status"; + char *args[] = {a, b, NULL}; + expected_argv = args; + helper_check = 1; + (void)__wrap_main(2, args); + abort(); +} +_Noreturn void __assert_fail(const char *expression, const char *file, int line, + const char *function) +{ + printf("assertion %s at %s:%d (%s)\n", expression, file, line, function); + fflush(NULL); + _Exit(99); +} +int main(int argc, char **argv) +{ + if (argc == 2) { + helper_mode = atoi(argv[1]); + assert(helper_mode >= 1 && helper_mode <= 3); + helper_case(); + } + fork_cases(); + pipe_alias_case(); + exec_cases(); + wait_cases(); + helper_case(); + printf("selected C runtime %u cases passed\n", cases); + return 0; +} +unsigned long getauxval(unsigned long type) +{ + assert(type >= 0xa000 && type <= 0xa002); + return type == 0xa000 ? x.aux.memory + : type == 0xa001 ? x.aux.input + : x.aux.output; +} diff --git a/userland/experiments/filesystem-service/file-descriptions.rs b/userland/experiments/filesystem-service/file-descriptions.rs index ed0c21e..9a8f087 100644 --- a/userland/experiments/filesystem-service/file-descriptions.rs +++ b/userland/experiments/filesystem-service/file-descriptions.rs @@ -119,7 +119,7 @@ impl Descriptions { Ok((index, self.descriptors[view][index].ok_or(Failure::BadFd)?)) } - #[cfg(fs_process_views)] + #[cfg(any(fs_process_views, native_zsh_process))] /// Exact remote FD presence/flags, computed from the sole authoritative view. pub(crate) fn file_view_in(&self, view: usize) -> Result<(u16, u16), S::Error> { self.view(view)?; @@ -389,7 +389,7 @@ impl Descriptions { } Ok(()) } - #[cfg(fs_process_views)] + #[cfg(any(fs_process_views, native_zsh_process))] pub(crate) fn inherit_view(&mut self, parent: usize, child: usize) -> Result<(), S::Error> { self.view(parent)?; self.view(child)?; @@ -399,7 +399,7 @@ impl Descriptions { self.descriptors[child] = self.descriptors[parent]; Ok(()) } - #[cfg(fs_process_views)] + #[cfg(any(fs_process_views, native_zsh_process))] pub(crate) fn settle_view(&mut self, view: usize) -> Result<(), S::Error> { self.view(view)?; for index in 0..SLOTS { diff --git a/userland/experiments/filesystem-service/readonly-files-tests.rs b/userland/experiments/filesystem-service/readonly-files-tests.rs index 0fd9eaa..ee9e960 100644 --- a/userland/experiments/filesystem-service/readonly-files-tests.rs +++ b/userland/experiments/filesystem-service/readonly-files-tests.rs @@ -421,3 +421,90 @@ fn zsh_bad_body_padding_retires_before_lookup() { assert!(h.files().prepare(binding(), &raw, 64, &mut w).is_err()); assert_eq!(h.requests(), before); } + +#[cfg(native_zsh_process)] +#[test] +fn selected_child_binding_has_its_actual_native_and_session_identity() { + let child = Binding { slot: 2, epoch: 1, handle: 0xb0000103, generation: 47 }; + assert!(child.valid(), "SELECTED_CHILD_BINDING"); + assert!(!Binding { handle: binding().handle, ..child }.valid()); + assert!(!Binding { generation: 0, ..child }.valid()); +} + +#[cfg(native_zsh_process)] +fn child_binding() -> Binding { + Binding { slot: 2, epoch: 1, handle: 0xb0000103, generation: 47 } +} +#[cfg(native_zsh_process)] +fn rpc_as(h: &mut Harness, binding: Binding, op: u64, args: [u64; 4]) -> ([u8; 64], Vec) { + let files = h.files(); + let view = files.view(binding).unwrap(); + let mut raw = [0; 64]; raw[..8].copy_from_slice(MAGIC); + put(&mut raw, 1, op); put(&mut raw, 2, binding.generation); put(&mut raw, 3, files.sequence(view)); + for (i, value) in args.iter().enumerate() { put(&mut raw, 4+i, *value); } + let mut workspace = [0; 4096]; + let mut reply = files.prepare(binding, &raw, 0, &mut workspace).unwrap(); + let header = *reply.header(); let body = reply.body_mut().to_vec(); reply.delivered(); + (header, body) +} +#[cfg(native_zsh_process)] +#[test] +fn selected_two_views_share_real_offsets_closes_and_commit_only_cloexec() { + let mut h = Harness::new(); + let (r, _) = h.rpc(1, [0, ENTRY.len() as u64, 0, 0], ENTRY); let first = word(&r, 4); + let (r, _) = h.rpc(1, [1, ALIAS.len() as u64, 0, 0], ALIAS); let second = word(&r, 4); + let before = h.requests(); let child = child_binding(); + h.files().inherit_child(binding(), child).unwrap(); assert_eq!(h.requests(), before); + assert_eq!(h.files().file_view(child).unwrap(), ((1< bool { - self.slot == 1 + (self.slot == 1 || (cfg!(native_zsh_process) && self.slot == 2)) && (1..=15).contains(&self.epoch) - && self.handle == 0xb0000000 | u64::from(self.epoch) << 8 | 2 + && self.handle == 0xb0000000 | u64::from(self.epoch) << 8 | u64::from(self.slot + 1) && self.generation != 0 } } @@ -76,8 +77,16 @@ impl descriptions::Store for Remote { /// Runtime must authenticate the actual endpoint before passing its binding here. #[must_use] pub struct Files { - descriptions: descriptions::Descriptions, 1>, + descriptions: descriptions::Descriptions, VIEWS>, binding: Binding, + #[cfg(native_zsh_process)] + child: Option, + #[cfg(native_zsh_process)] + child_next: u64, + #[cfg(native_zsh_process)] + child_pending: bool, + #[cfg(native_zsh_process)] + child_epoch: u8, namespace_epoch: u64, next: u64, pending: bool, @@ -94,7 +103,7 @@ impl Files { binding: Binding, ) -> Result<&mut Self, ConnectFailure> { let pointer = slot.as_mut_ptr(); - if !binding.valid() || epoch == 0 { + if !binding.valid() || binding.slot != 1 || epoch == 0 { let mut channel = channel; return Err(ConnectFailure { error: Error::Invalid, @@ -105,13 +114,20 @@ impl Files { // all non-Client fields are initialized before any Self reference exists. let client_slot = unsafe { core::ptr::addr_of_mut!((*pointer).binding).write(binding); + #[cfg(native_zsh_process)] + { + core::ptr::addr_of_mut!((*pointer).child).write(None); + core::ptr::addr_of_mut!((*pointer).child_next).write(0); + core::ptr::addr_of_mut!((*pointer).child_pending).write(false); + core::ptr::addr_of_mut!((*pointer).child_epoch).write(0); + } core::ptr::addr_of_mut!((*pointer).namespace_epoch).write(epoch); core::ptr::addr_of_mut!((*pointer).next).write(1); core::ptr::addr_of_mut!((*pointer).pending).write(false); core::ptr::addr_of_mut!((*pointer).retired).write(false); core::ptr::addr_of_mut!((*pointer).mutation_failure).write(None); let desc = &mut *core::ptr::addr_of_mut!((*pointer).descriptions) - .cast::, 1>>>(); + .cast::, VIEWS>>>(); let source = descriptions::Descriptions::prepare_in_place(desc).as_mut_ptr(); core::ptr::addr_of_mut!((*source).held).write(None); &mut *core::ptr::addr_of_mut!((*source).client).cast::>>() @@ -133,6 +149,7 @@ impl Files { pub fn abort(&mut self) -> [i64; 2] { self.retired = true; self.pending = false; + #[cfg(native_zsh_process)] { self.child_pending = false; } self.descriptions.retire(); self.cleanup().unwrap_or([-5; 2]) } @@ -212,12 +229,65 @@ impl Files { } result } + fn view(&self, binding: Binding) -> Option { + if self.binding == binding { return Some(0); } + #[cfg(native_zsh_process)] + if self.child == Some(binding) { return Some(1); } + None + } + fn sequence(&self, view: usize) -> u64 { + #[cfg(native_zsh_process)] + if view == 1 { return self.child_next; } + let _ = view; + self.next + } + fn pending_in(&self, view: usize) -> bool { + #[cfg(native_zsh_process)] + if view == 1 { return self.child_pending; } + let _ = view; + self.pending + } fn live(&self, binding: Binding) -> bool { - !self.retired - && !self.pending - && self.binding == binding - && self.cleanup().is_none() - && self.descriptions.source.held.is_some() + !self.retired && self.view(binding).is_some_and(|view| !self.pending_in(view)) + && self.cleanup().is_none() && self.descriptions.source.held.is_some() + } + /// Owner-authenticated snapshot check; no namespace operation or new handle. + #[cfg(native_zsh_process)] + pub fn file_view(&self, binding: Binding) -> Result<(u16, u16), Error> { + if !self.live(binding) { return Err(Error::Protocol); } + self.descriptions.file_view_in(self.view(binding).unwrap()).map_err(|_| Error::Protocol) + } + /// Only after exact native copy admission. Parent remains frozen/awaiting + /// its correlated reply; real descriptions/positions, not handles, are shared. + #[cfg(native_zsh_process)] + pub fn inherit_child(&mut self, parent: Binding, child: Binding) -> Result<(), Error> { + if !self.live(parent) || parent != self.binding || self.child.is_some() + || !child.valid() || child.slot != 2 || child.epoch != self.child_epoch + 1 + || child.generation == parent.generation { return Err(Error::Protocol); } + self.descriptions.inherit_view(0, 1).map_err(|_| Error::Protocol)?; + self.child = Some(child); self.child_epoch = child.epoch; + self.child_next = 1; self.child_pending = false; + Ok(()) + } + /// Native C003 receipt must already prove commit. Refusals never call this. + #[cfg(native_zsh_process)] + pub fn exec_child(&mut self, child: Binding, generation: u64) -> Result { + if self.child != Some(child) || !self.live(child) + || child.generation.checked_add(1) != Some(generation) || generation == u64::MAX { + return Err(Error::Protocol); + } + self.descriptions.commit_exec_close_in(1).map_err(|_| Error::Closed)?; + let next = Binding { generation, ..child }; + self.child = Some(next); self.child_next = 1; + Ok(next) + } + /// Caller proved native terminal and known wait delivery before settlement. + #[cfg(native_zsh_process)] + pub fn settle_child(&mut self, child: Binding) -> Result<(), Error> { + if self.child != Some(child) || !self.live(child) { return Err(Error::Protocol); } + self.descriptions.settle_view(1).map_err(|_| Error::Closed)?; + self.child = None; self.child_next = 0; + Ok(()) } /// Validate complete fixed header before body assembly or source effects. pub fn body_length(&mut self, binding: Binding, raw: &[u8; FRAME]) -> Result { @@ -228,8 +298,8 @@ impl Files { let valid = self.live(binding) && raw[..8] == MAGIC[..] && word(raw, 2) == binding.generation - && word(raw, 3) == self.next - && self.next != u64::MAX + && word(raw, 3) == self.sequence(self.view(binding).unwrap()) + && self.sequence(self.view(binding).unwrap()) != u64::MAX && match word(raw, 1) { 1 => a <= 1 && (1..=255).contains(&b) && c == 0 && d == 0, 2 => fd(a) && b <= PAYLOAD as u64 && c == 0 && d == 0, @@ -267,7 +337,8 @@ impl Files { self.abort(); return Err(Error::Protocol); } - let (result, body) = self.execute(raw, workspace); + let view = self.view(binding).ok_or(Error::Protocol)?; + let (result, body) = self.execute(view, raw, workspace); if self.cleanup().is_some() { workspace.fill(0); self.abort(); @@ -276,7 +347,7 @@ impl Files { let mut reply = [0; FRAME]; reply[..8].copy_from_slice(MAGIC); put(&mut reply, 2, binding.generation); - put(&mut reply, 3, self.next); + put(&mut reply, 3, self.sequence(view)); let body = match result { Ok(value) => { put(&mut reply, 4, value); @@ -289,8 +360,13 @@ impl Files { 0 } }; - self.pending = true; + #[cfg(native_zsh_process)] + if view == 1 { self.child_pending = true; } else { self.pending = true; } + #[cfg(not(native_zsh_process))] + { self.pending = true; } Ok(Reply { + #[cfg(native_zsh_process)] + view, files: self, workspace, header: reply, @@ -300,6 +376,7 @@ impl Files { } fn execute( &mut self, + view: usize, raw: &[u8; FRAME], workspace: &mut [u8; PAYLOAD], ) -> (Result>, usize) { @@ -311,14 +388,14 @@ impl Files { 1 => { let value = self .descriptions - .open_read(&workspace[..b as usize], a != 0) + .open_read_in(view, &workspace[..b as usize], a != 0) .map(|fd| fd as u64); workspace.fill(0); value } 2 => self .descriptions - .read(a as i32, b as usize, workspace) + .read_in(view, a as i32, b as usize, workspace) .map(|count| { body = count; count as u64 @@ -370,8 +447,8 @@ impl Files { opcode => { workspace.fill(0); match opcode { - 3 => self.descriptions.seek( - a as i32, + 3 => self.descriptions.seek_in( + view, a as i32, match c { 0 => Seek::Set(b as i64), 1 => Seek::Current(b as i64), @@ -380,12 +457,12 @@ impl Files { ), 4 => self .descriptions - .duplicate(a as i32, b as i32, c != 0) + .duplicate_in(view, a as i32, b as i32, c != 0) .map(|fd| fd as u64), - 5 => self.descriptions.close(a as i32).map(|()| 0), + 5 => self.descriptions.close_in(view, a as i32).map(|()| 0), 6 => self .descriptions - .set_close_on_exec(a as i32, b != 0) + .set_close_on_exec_in(view, a as i32, b != 0) .map(|()| 0), _ => Err(Failure::Invalid), } @@ -398,6 +475,8 @@ impl Files { &mut self, verify: impl FnOnce(&mut T) -> Result<(), Error>, ) -> Result<[i64; 2], Error> { + #[cfg(native_zsh_process)] + if self.child.is_some() || self.child_pending { return Err(Error::Closed); } if self.retired || self.pending || self.cleanup().is_some() { return Err(Error::Closed); } @@ -417,6 +496,8 @@ impl Files { } } pub struct Reply<'a, T: Channel> { + #[cfg(native_zsh_process)] + view: usize, files: &'a mut Files, workspace: &'a mut [u8; PAYLOAD], header: [u8; FRAME], @@ -438,8 +519,11 @@ impl Reply<'_, T> { &mut self.workspace[..self.body.div_ceil(FRAME) * FRAME] } pub fn delivered(mut self) { - self.files.next += 1; - self.files.pending = false; + #[cfg(native_zsh_process)] + if self.view == 1 { self.files.child_next += 1; self.files.child_pending = false; } + else { self.files.next += 1; self.files.pending = false; } + #[cfg(not(native_zsh_process))] + { self.files.next += 1; self.files.pending = false; } self.workspace.fill(0); self.delivered = true; }