/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com Project: zyginit Filename: contract.reef Authors: Chris Tusa License: Description: Hammerhead process contract FFI bindings (libcontract) ******************************************************************************/ // Hammerhead process contracts are the kernel mechanism for tracking service // processes. This module provides Reef wrappers around libcontract. // // This module ONLY works on Hammerhead. On Linux, the extern "C" // functions will fail at link time unless stub implementations are provided. // // Reference: Hammerhead libcontract(3LIB), contract(5), process(5) // Source: hammerhead/usr/src/lib/libcontract/common/ module contract import sys.fd as fd import core.str export type Contract // Contract event flags (from sys/contract/process.h) fn CT_PR_EV_EMPTY(): int fn CT_PR_EV_FORK(): int fn CT_PR_EV_EXIT(): int fn CT_PR_EV_CORE(): int fn CT_PR_EV_SIGNAL(): int fn CT_PR_EV_HWERR(): int fn CT_PR_ALLEVENT(): int fn CT_PR_ALLFATAL(): int // Contract parameter flags fn CT_PR_INHERIT(): int fn CT_PR_NOORPHAN(): int fn CT_PR_PGRPONLY(): int fn CT_PR_REGENT(): int // Status detail levels (from sys/contract.h) fn CTD_COMMON(): int fn CTD_FIXED(): int fn CTD_ALL(): int // sigsend id type fn P_CTID(): int // CTFS paths fn CTFS_ROOT(): string fn CTFS_PROCESS_TEMPLATE(): string fn CTFS_PROCESS_LATEST(): string fn CTFS_PROCESS_BUNDLE(): string // Contract handle constructor fn new_contract(id: int, service_name: string): Contract // Contract accessors fn contract_id(ct: Contract): int fn contract_service(ct: Contract): string fn contract_bundle_fd(ct: Contract): int // Contract lifecycle fn setup_template(): int fn get_latest_contract(): int fn clear_template(tmpl_fd: int): int fn open_bundle(): int fn kill_contract(contract_id: int, sig: int): int fn abandon_contract(contract_id: int): int fn adopt_contract(contract_id: int): int fn is_contract_empty(contract_id: int): bool // Event reading fn read_event(bundle_fd: int, out_ctid: [int], out_type: [int]): bool fn ack_event(contract_id: int, event_id: int): int end export // ============================================================================ // FFI declarations — libcontract // ============================================================================ // Template management extern "C" fn ct_tmpl_activate(fd: int): int extern "C" fn ct_tmpl_clear(fd: int): int extern "C" fn ct_tmpl_set_informative(fd: int, events: int): int extern "C" fn ct_tmpl_set_critical(fd: int, events: int): int // Process contract template extern "C" fn ct_pr_tmpl_set_fatal(fd: int, events: int): int extern "C" fn ct_pr_tmpl_set_param(fd: int, params: int): int // Contract status — ct_stathdl_t is an opaque void* extern "C" fn ct_status_read(fd: int, detail: int, statp: pointer): int extern "C" proc ct_status_free(stathdl: pointer) extern "C" fn ct_status_get_id(stathdl: pointer): int // Process-contract member list — fills *members and *n. Memory is // owned by the stathdl, freed by ct_status_free. Requires the status // to have been read at CTD_FIXED detail or higher (CTD_COMMON does // not include the member list). extern "C" fn ct_pr_status_get_members(stathdl: pointer, members: [pointer], n: [int]): int // Contract control extern "C" fn ct_ctl_abandon(fd: int): int extern "C" fn ct_ctl_adopt(fd: int): int // Contract events — ct_evthdl_t is an opaque void* extern "C" fn ct_event_read(fd: int, evtp: pointer): int extern "C" proc ct_event_free(evthdl: pointer) extern "C" fn ct_event_get_ctid(evthdl: pointer): int extern "C" fn ct_event_get_type(evthdl: pointer): int extern "C" fn ct_event_get_flags(evthdl: pointer): int // Process contract event details extern "C" fn ct_pr_event_get_exitstatus(evthdl: pointer, status: pointer): int extern "C" fn ct_pr_event_get_pid(evthdl: pointer, pid: pointer): int // POSIX — sigsend for contract-wide signaling extern "C" fn sigsend(idtype: int, id: int, sig: int): int // POSIX — open/close for ctfs paths extern "C" fn open(path: string, flags: int): int extern "C" fn close(fd: int): int // ============================================================================ // Constants — values from Hammerhead kernel headers // ============================================================================ // Contract event flags (sys/contract/process.h) fn CT_PR_EV_EMPTY(): int return 1 end CT_PR_EV_EMPTY fn CT_PR_EV_FORK(): int return 2 end CT_PR_EV_FORK fn CT_PR_EV_EXIT(): int return 4 end CT_PR_EV_EXIT fn CT_PR_EV_CORE(): int return 8 end CT_PR_EV_CORE fn CT_PR_EV_SIGNAL(): int return 16 end CT_PR_EV_SIGNAL fn CT_PR_EV_HWERR(): int return 32 end CT_PR_EV_HWERR fn CT_PR_ALLEVENT(): int return 63 end CT_PR_ALLEVENT fn CT_PR_ALLFATAL(): int return 56 end CT_PR_ALLFATAL // Contract parameter flags (sys/contract/process.h) fn CT_PR_INHERIT(): int return 1 end CT_PR_INHERIT fn CT_PR_NOORPHAN(): int return 2 end CT_PR_NOORPHAN fn CT_PR_PGRPONLY(): int return 4 end CT_PR_PGRPONLY fn CT_PR_REGENT(): int return 8 end CT_PR_REGENT // Status detail levels (sys/contract.h) fn CTD_COMMON(): int return 0 end CTD_COMMON fn CTD_FIXED(): int return 1 end CTD_FIXED fn CTD_ALL(): int return 2 end CTD_ALL // sigsend id type for contract (sys/procset.h enum position 13, // between P_ZONEID=12 and P_CPUID=14). Verified on Hammerhead via // utils/contract_probe. fn P_CTID(): int return 13 end P_CTID // CTFS paths fn CTFS_ROOT(): string return "/system/contract" end CTFS_ROOT fn CTFS_PROCESS_TEMPLATE(): string return "/system/contract/process/template" end CTFS_PROCESS_TEMPLATE fn CTFS_PROCESS_LATEST(): string return "/system/contract/process/latest" end CTFS_PROCESS_LATEST fn CTFS_PROCESS_BUNDLE(): string return "/system/contract/process/bundle" end CTFS_PROCESS_BUNDLE // Open flags fn O_RDONLY(): int return 0 end O_RDONLY fn O_WRONLY(): int return 1 end O_WRONLY fn O_RDWR(): int return 2 end O_RDWR // ============================================================================ // Contract type // ============================================================================ type Contract = struct id: int service_name: string bundle_fd: int end Contract fn new_contract(id: int, service_name: string): Contract return Contract{ id: id, service_name: service_name, bundle_fd: 0 - 1 } end new_contract fn contract_id(ct: Contract): int return ct.id end contract_id fn contract_service(ct: Contract): string return ct.service_name end contract_service fn contract_bundle_fd(ct: Contract): int return ct.bundle_fd end contract_bundle_fd // ============================================================================ // Contract lifecycle functions // ============================================================================ // Set up a contract template for forking a service. // Configures: informative=EMPTY, critical=EMPTY|HWERR, fatal=HWERR, // params=INHERIT|NOORPHAN. Returns template fd on success, -1 on error. fn setup_template(): int let tmpl_fd = open(CTFS_PROCESS_TEMPLATE(), O_RDWR()) if tmpl_fd < 0 return 0 - 1 end if // Want informative notification when contract becomes empty let rc1 = ct_tmpl_set_informative(tmpl_fd, CT_PR_EV_EMPTY()) // Critical events: empty (all exited) and hardware error let rc2 = ct_tmpl_set_critical(tmpl_fd, CT_PR_EV_EMPTY() + CT_PR_EV_HWERR()) // Fatal events: hardware error causes contract death let rc3 = ct_pr_tmpl_set_fatal(tmpl_fd, CT_PR_EV_HWERR()) // Parameters: inherit contracts on fork, kill orphans on abandon let rc4 = ct_pr_tmpl_set_param(tmpl_fd, CT_PR_INHERIT() + CT_PR_NOORPHAN()) if rc1 != 0 or rc2 != 0 or rc3 != 0 or rc4 != 0 close(tmpl_fd) return 0 - 1 end if let rc5 = ct_tmpl_activate(tmpl_fd) if rc5 != 0 close(tmpl_fd) return 0 - 1 end if return tmpl_fd end setup_template // After fork(), retrieve the contract ID for the new child process. // The kernel writes the latest contract to /system/contract/process/latest. // Returns contract ID on success, -1 on error. fn get_latest_contract(): int let latest_fd = open(CTFS_PROCESS_LATEST(), O_RDONLY()) if latest_fd < 0 return 0 - 1 end if // ct_status_read writes a handle (void*) to an output parameter. // We pass a [pointer] array — in C, this is void**, exactly what ct_status_read expects. unsafe let stathdl_buf = new [pointer](1) let rc = ct_status_read(latest_fd, CTD_COMMON(), stathdl_buf) close(latest_fd) if rc != 0 return 0 - 1 end if let stathdl = stathdl_buf[0] let ctid = ct_status_get_id(stathdl) ct_status_free(stathdl) return ctid end unsafe end get_latest_contract // Clear (deactivate) a contract template after fork. // Returns 0 on success. fn clear_template(tmpl_fd: int): int let rc = ct_tmpl_clear(tmpl_fd) close(tmpl_fd) return rc end clear_template // Open the contract bundle fd for poll()-based event monitoring. // Returns bundle fd on success, -1 on error. fn open_bundle(): int // O_NONBLOCK (0x80 on Hammerhead): ct_event_read on a blocking fd // BLOCKS waiting for events. Our main loop only enters // handle_contract_events when poll says the fd is readable, but // after we drain the events present at that moment, the next // ct_event_read call sees no event and would block indefinitely // — wedging the entire main loop, including reap_children for // SIGCHLD. With O_NONBLOCK, ct_event_read returns EAGAIN once // we've drained, read_event returns false, the while loop in // handle_contract_events exits cleanly. let bundle_fd = open(CTFS_PROCESS_BUNDLE(), O_RDONLY() + 0x80) return bundle_fd end open_bundle // Kill all processes in a contract with the given signal. // Uses sigsend(P_CTID, contract_id, signal). // Returns 0 on success, -1 on error. fn kill_contract(contract_id: int, sig: int): int return sigsend(P_CTID(), contract_id, sig) end kill_contract // Abandon a contract (release ownership, orphan handling applies). // Opens the contract's ctl fd and issues abandon. // Returns 0 on success, -1 on error. fn abandon_contract(contract_id: int): int // Build path: /system/contract/process//ctl // ctl file mode is --w--w--w-, so open with O_WRONLY (O_RDWR fails). let ctl_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/ctl") let ctl_fd = open(ctl_path, O_WRONLY()) if ctl_fd < 0 return 0 - 1 end if let rc = ct_ctl_abandon(ctl_fd) close(ctl_fd) return rc end abandon_contract // Adopt an existing contract (for zyginit restart recovery). // Opens the contract's ctl fd and issues adopt. // Returns 0 on success, -1 on error. fn adopt_contract(contract_id: int): int let ctl_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/ctl") let ctl_fd = open(ctl_path, O_WRONLY()) if ctl_fd < 0 return 0 - 1 end if let rc = ct_ctl_adopt(ctl_fd) close(ctl_fd) return rc end adopt_contract // Check whether a contract has zero member processes. Used by the // post-recovery idempotency step in main.apply_recovered_state — if a // contract was reported in state.toml but the kernel says it's empty, // the service exited during the exec gap and we should apply restart // policy as if a contract-empty event arrived. // // Returns true if the contract has zero members OR if the status // read fails (treat unknown as empty so the recovery path applies // restart policy rather than leaving a phantom service in RUNNING). fn is_contract_empty(contract_id: int): bool let status_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/status") let st_fd = open(status_path, O_RDONLY()) if st_fd < 0 return true end if unsafe let stathdl_buf = new [pointer](1) // CTD_FIXED includes process-contract-specific fields like the // member-PID list, which CTD_COMMON omits. let rc = ct_status_read(st_fd, CTD_FIXED(), stathdl_buf) close(st_fd) if rc != 0 return true end if let stathdl = stathdl_buf[0] let members_buf = new [pointer](1) let n_buf = new [int](1) let mrc = ct_pr_status_get_members(stathdl, members_buf, n_buf) let n = n_buf[0] ct_status_free(stathdl) if mrc != 0 return true end if return n == 0 end unsafe end is_contract_empty // ============================================================================ // Event reading // ============================================================================ // Read an event from the contract bundle fd. // Writes the contract ID to out_ctid[0] and event type to out_type[0]. // Returns true on success, false on error or no event. fn read_event(bundle_fd: int, out_ctid: [int], out_type: [int]): bool unsafe let evthdl_buf = new [pointer](1) let rc = ct_event_read(bundle_fd, evthdl_buf) if rc != 0 return false end if let evthdl = evthdl_buf[0] out_ctid[0] = ct_event_get_ctid(evthdl) out_type[0] = ct_event_get_type(evthdl) ct_event_free(evthdl) return true end unsafe end read_event // Acknowledge a contract event. // Opens the contract's event fd and issues ack. fn ack_event(contract_id: int, event_id: int): int let ev_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/events") let ev_fd = open(ev_path, O_RDONLY()) if ev_fd < 0 return 0 - 1 end if // For now, we just close — full ack requires ct_ctl_ack on the ctl fd close(ev_fd) return 0 end ack_event // ============================================================================ // Helpers // ============================================================================ // Convert int to string (for building ctfs paths) fn int_to_str(n: int): string if n == 0 return "0" end if mut value = n if n < 0 value = 0 - n end if mut result = "" while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while if n < 0 result = str.concat("-", result) end if return result end int_to_str end module