Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.
include/platx/abi.h
/* platx/abi.h — Core primitives only. Domain APIs come from require().
*
* EMBEDDED: this pointer is the real Core.
* CHILD: the same layout, implemented by a serializing proxy.
*/
typedef struct plat_module_ctx plat_module_ctx_t;
typedef struct plat_abi {
uint32_t abi_version; /* PLATX_CORE_ABI */
uint32_t struct_size; /* sizeof(plat_abi_t) */
void *(*require)(plat_module_ctx_t *m, const char *capability,
uint32_t version);
int (*provide)(plat_module_ctx_t *m, const char *capability,
uint32_t version, const void *vtable);
int (*revoke)(plat_module_ctx_t *m, const char *capability);
const plat_resource_api_t *res;
const plat_task_api_t *task;
const plat_event_api_t *event;
const plat_xio_api_t *xio;
const plat_diag_api_t *diag;
} plat_abi_t;
/* Fill Core primitive pointers. Domain vtables stay on require(). */
int plat_abi_init(plat_abi_t *abi);
/* Worker boot: res + task tables, then a process-wide EMBEDDED ABI. */
int plat_abi_core_init(void);
const plat_abi_t *plat_abi_core(void);
/* EMBEDDED modules without create_args: same table as abi->require.
* CHILD must use the proxy on args->abi, not these. Fail closed if
* Core ABI is down — no second registry. */
void *plat_abi_require(const char *capability, uint32_t version); /* inline interface */
int plat_abi_provide(const char *capability, uint32_t version,
const void *vtable); /* inline interface */
int plat_abi_revoke(const char *capability); /* inline interface */
/* Worker-side registry behind abi->require/provide/revoke. */
void *plat_cap_require(plat_module_ctx_t *m, const char *capability,
uint32_t version);
int plat_cap_provide(plat_module_ctx_t *m, const char *capability,
uint32_t version, const void *vtable);
/* Revoke the first named slot owned by this exact context; NULL is the trusted
* Core override. Returns -1 if no eligible slot exists, without side effects. */
int plat_cap_revoke(plat_module_ctx_t *m, const char *capability);
int plat_cap_init(void);
void plat_cap_fini(void);
int plat_cap_has(const char *capability, uint32_t version);
int plat_cap_revoke_ctx(plat_module_ctx_t *m);
include/platx/abi_contract.h
/* platx/abi_contract.h — A1-CONTRACT: early public compatibility contract.
*
* A1-P01-012/013/014/015. This header is the one place a consumer may look
* BEFORE it dereferences anything a producer handed it.
*
* Three properties are deliberate and load-bearing:
*
* 1. It is header-only and pure. No Core symbol is referenced, nothing is
* linked, no allocation, no syscall. A consumer that only wants to say
* "is this table shaped the way I was compiled against" must not have to
* link Core to ask.
*
* 2. It is portable. Only <stdint.h> and <stddef.h>. It does NOT include
* platx/abi.h, platx/xio_api.h or anything that reaches <sys/socket.h>,
* because A3 (platx-windows) has to compile the same fixtures without a
* Linux tree and without a Windows provider being ready (A1-P01-014).
*
* 3. It carries no domain tables. Only the two header words every PLATX
* versioned table starts with, and the classification of an owner-gated
* result. Capabilities stay on require() (invariants.h #3, #4).
*
* THE HEADER-WORD CONTRACT
* ------------------------
* Every versioned PLATX table — plat_abi_t today, and any Core/XIO/profile
* table that wants this negotiation — begins with exactly:
*
* uint32_t <something>_version; / * major<<16 | minor * /
* uint32_t struct_size; / * sizeof(that struct) * /
*
* plat_abi_t already does (include/platx/abi.h). That prefix is what makes a
* safe peek possible: reading the first eight bytes of a table is defined
* even when the rest of the layout is a version the reader has never seen.
*
* RULE. Compatibility is decided by three questions, in this order:
* - major equal? no -> PLAT_ABI_ERR_MAJOR (never call)
* - producer minor >= mine? no -> PLAT_ABI_ERR_MINOR (never call)
* - producer size >= mine? no -> PLAT_ABI_ERR_SIZE (never call)
* A larger producer struct is accepted: the consumer's fields are a prefix.
* A smaller one is refused even at the same version, because the trailing
* slots the consumer was compiled to read are not there.
*/
/* Contract revision of THIS header. Bumped when the rules below change,
* independently of PLATX_CORE_ABI which versions the Core table itself. */
#define PLATX_ABI_CONTRACT_REV 1u
#define PLAT_ABI_MAJOR_OF(v) ((uint32_t)(((uint32_t)(v)) >> 16))
#define PLAT_ABI_MINOR_OF(v) ((uint32_t)(((uint32_t)(v)) & 0xFFFFu))
#define PLAT_ABI_MAKE(ma, mi) ((uint32_t)((((uint32_t)(ma)) << 16) | \
(((uint32_t)(mi)) & 0xFFFFu)))
/* The safe-peek prefix. Layout-compatible with the first two members of
* plat_abi_t and of every table that opts into this contract. */
typedef struct plat_abi_header {
uint32_t abi_version;
uint32_t struct_size;
} plat_abi_header_t;
/* Negotiation verdicts. Distinct values: a caller must be able to log which
* of the three refusals happened without re-deriving it. */
typedef enum plat_abi_verdict {
PLAT_ABI_OK = 0,
PLAT_ABI_ERR_NULL = -1, /* no table at all */
PLAT_ABI_ERR_MAJOR = -2, /* different major: layout is not comparable */
PLAT_ABI_ERR_MINOR = -3, /* producer older than the consumer needs */
PLAT_ABI_ERR_SIZE = -4 /* producer struct shorter than consumer's */
} plat_abi_verdict_t;
/* The whole negotiation, on the two header words only.
*
* want_version / want_size are what the CONSUMER was compiled against —
* normally PLATX_CORE_ABI and sizeof(plat_abi_t) at the consumer's own
* compile time. Nothing beyond the first eight bytes of `hdr` is touched, so
* this is defined even when the producer is a future layout. */
plat_abi_verdict_t
plat_abi_check_header(const plat_abi_header_t *hdr,
uint32_t want_version, size_t want_size); /* inline interface */
/* Same check from an opaque pointer. A consumer holding only `void *` — a
* CHILD proxy, a loader, a Windows fixture with no plat_abi_t in scope —
* still reads a defined prefix. */
plat_abi_verdict_t
plat_abi_check_opaque(const void *table, uint32_t want_version, size_t want_size); /* inline interface */
const char *plat_abi_verdict_str(plat_abi_verdict_t v); /* inline interface */
/* ── A1-P01-013: one owner-result vocabulary ──────────────────────────────
*
* The tree already refuses in several dialects: XIO returns named negatives
* (platx/xio_err.h), plat_task_api_t returns -1 and names the state in errno
* (platx/task.h), plat_resource_api_t returns 0/-1. A consumer that has to
* decide "retry, give up, or clean up" must not need three switch statements
* that each know one dialect.
*
* These are the five outcomes that decision actually has. They are declared
* here, once, and the classifiers below are the only mapping. Adding a sixth
* outcome is an ABI change, not a local convenience.
*/
typedef enum plat_owner_result {
/* The call did what it said. */
PLAT_OWNER_OK = 0,
/* The request was malformed, or the epoch/handle is not a thing:
* retrying with the same arguments changes nothing. */
PLAT_OWNER_ARGS = 1,
/* The object exists but is owned by someone else, or by an older
* generation of this owner. NOTHING was read, written or closed.
* generation 0 is never an owner: it is refused here, not swept. */
PLAT_OWNER_STALE = 2,
/* Someone else already holds the operation (a concurrent join, a
* claimed slot). Retry may succeed once they are done. */
PLAT_OWNER_BUSY = 3,
/* Requested, not finished. The slot is deliberately kept: a cancel that
* was delivered, a bounded wait that expired, an inflight submit.
* This is NOT a terminal state and must never be reported as success. */
PLAT_OWNER_PENDING = 4,
/* The subject is gone for good — exited, released, closed. Its slot is
* cleared. Cleanup is complete; there is nothing left to reap. */
PLAT_OWNER_TERMINAL = 5
} plat_owner_result_t;
const char *plat_owner_result_str(plat_owner_result_t r); /* inline interface */
/* XIO dialect -> vocabulary. `rc` is what xio_submit() returned: >= 0 is a
* byte count, negatives are platx/xio_err.h codes. The numeric codes are
* repeated here rather than included, so that this header stays free of
* <sys/socket.h> via xio_api.h; the contract test asserts they still agree
* with platx/xio_err.h (tests/contract/platx_abi_contract_test.c). */
#define PLATX_CONTRACT_XIO_ERR_SUBMIT (-1001)
#define PLATX_CONTRACT_XIO_ERR_COMPLETE (-1002)
#define PLATX_CONTRACT_XIO_ERR_UNAVAIL (-1003)
#define PLATX_CONTRACT_XIO_ERR_CANCEL (-1004)
#define PLATX_CONTRACT_XIO_ERR_OWNER_STALE (-1005)
#define PLATX_CONTRACT_XIO_ERR_TIMEOUT (-1006)
#define PLATX_CONTRACT_XIO_ERR_ARGS (-1007)
plat_owner_result_t plat_owner_from_xio(long rc); /* inline interface */
/* Task dialect -> vocabulary, for plat_task_api_t::join and ::cancel.
* `rc` is the return value, `err` the errno it named (platx/task.h).
* errno numbers are passed in by the caller so this header needs no <errno.h>
* dependency on any particular platform's numbering; the contract test pins
* them against the host <errno.h>. */
plat_owner_result_t
plat_owner_from_task(int rc, int err,
int e_eagain, int e_esrch, int e_ebusy, int e_edeadlk); /* inline interface */
/* Generation 0 is not an owner. Every own/claim path must refuse it up front
* and every sweep path must treat it as a no-op — platx/xio_api.h says so for
* own_fd and release_owner, platx/resource.h for release_owner. One predicate
* so the rule is not re-implemented per call site. */
int plat_owner_gen_valid(uint32_t generation); /* inline interface */
include/platx/abi_proxy.h
/* platx/abi_proxy.h — child-side plat_abi_t. Same layout, IPC behind it.
* Compile with -I include only.
*
* Proxied this cut: require/provide/revoke, res (own/release/check/
* release_owner), event->emit. Host applies, then ACK; short payload /
* unknown method / dead fd → error, no half-applied resource.
*
* NULL and why (do not fake success):
* task — entry is a function pointer; it cannot cross the socket.
* A host trampoline CALL deadlocks if spawn runs before serve()
* and would serialize child work on the serve thread. Proxy
* must not invent pthread.
* event->subscribe / unsubscribe — fn pointer, same reason as task.
* The event slot is live because emit is real; these methods
* fail closed (id 0 / -1), they do not publish a host slot.
* xio — plat_xio_api.get() would return a worker xio_t*. Methods
* live in src/xio/; CHILD compiles with -I include only.
* Handing out host pointers is a half-state. Framing already
* uses XIO on the socket; that is not abi->xio.
* diag — host-local bootstrap log. Child has no subscriber.
*/
int plat_abi_proxy_init(plat_abi_t *abi, int ipc_fd);
int plat_abi_proxy_handshake(plat_abi_t *abi, int timeout_ms);
int plat_abi_proxy_ready(plat_abi_t *abi);
/* Serve worker CALLs until the channel closes. */
int plat_abi_proxy_serve(plat_abi_t *abi);
void plat_abi_proxy_fini(plat_abi_t *abi);
include/platx/action_provider.h
/* platx/action_provider.h — action_provider_v1 (D054).
*
* The only ABI in the fabric that changes state. Everything here exists to
* make one thing impossible: an action that happened but cannot be described,
* undone or attributed.
*
* The transaction is six-legged, and the legs are not optional:
* preview what would change, and how far it reaches — without changing it
* prepare reserve, fence and stage; nothing is visible yet
* commit make it real, exactly once for a given idempotency key
* verify read the world back and confirm it matches what was promised
* rollback undo a prepared or committed action to its recorded prior state
* reconcile after a restart, decide what a half-finished transaction was
*
* Fencing: an action carries a lease. A lease that has been superseded may not
* commit, even if it prepared successfully — that is the whole point of a
* lease, and it is the difference between "the old owner is slow" and "there
* are two owners".
*
* AI seams AI016-AI033 are contract-only here: the descriptor carries
* `descriptor_hash`, a dry-run enum and `no_self_trigger` so that an advisory
* plane can be attached later without reopening this ABI. No model runtime,
* no inference and no self-triggered action may be implemented in this wave.
*
* ABI freeze: ACTION_PROVIDER_ABI 1 (D054 sealed 2026-09-01)
*/
#define ACTION_PROVIDER_ABI 1u
#define ACTION_TARGET_MAX 256u
#define ACTION_KEY_MAX 64u /* idempotency key, printable */
#define ACTION_BLAST_SLOTS 16u /* enumerated affected classes */
/* ── What an action does ─────────────────────────────────────────────────── */
typedef enum action_verb {
ACTION_VERB_NONE = 0,
ACTION_VERB_QUARANTINE = 1, /* move a file out of reach, reversibly */
ACTION_VERB_FREEZE = 2, /* stop a process without killing it */
ACTION_VERB_ISOLATE = 3, /* cut network reachability */
ACTION_VERB_REVOKE = 4, /* withdraw a credential or lease */
ACTION_VERB_COLLECT = 5, /* seal evidence; changes only the store */
ACTION_VERB_MAX
} action_verb_t;
/* ── Dry-run ladder (AI seam AI017) ──────────────────────────────────────
* PLAN never touches the system. SHADOW performs every check and reservation
* that COMMIT would, then releases them. LIVE is the only value that changes
* anything, and it is the only one an audit record may call an action.
*/
typedef enum action_mode {
ACTION_MODE_PLAN = 0,
ACTION_MODE_SHADOW = 1,
ACTION_MODE_LIVE = 2,
ACTION_MODE_MAX
} action_mode_t;
/* ── Transaction state (D097) ────────────────────────────────────────────── */
typedef enum action_state {
ACTION_ST_NONE = 0,
ACTION_ST_PREVIEWED = 1,
ACTION_ST_PREPARED = 2,
ACTION_ST_COMMITTED = 3,
ACTION_ST_VERIFIED = 4,
ACTION_ST_ROLLED = 5,
ACTION_ST_FAILED = 6,
ACTION_ST_MAX
} action_state_t;
/* ── Blast radius (D093) ─────────────────────────────────────────────────── */
typedef enum action_blast_class {
ACTION_BLAST_NONE = 0,
ACTION_BLAST_FILE = 1,
ACTION_BLAST_DIRECTORY = 2,
ACTION_BLAST_PROCESS = 3,
ACTION_BLAST_PROC_TREE = 4,
ACTION_BLAST_SOCKET = 5,
ACTION_BLAST_NETNS = 6,
ACTION_BLAST_HOST = 7, /* host-wide effect; requires explicit grant */
ACTION_BLAST_MAX
} action_blast_class_t;
typedef struct action_blast_entry {
uint32_t klass; /* action_blast_class_t */
uint32_t count; /* how many of that class are touched */
uint32_t reversible; /* 1 = rollback restores this class exactly */
uint32_t _pad;
} action_blast_entry_t;
/* ── The action descriptor ───────────────────────────────────────────────
* `descriptor_hash` pins the descriptor a decision was made against: commit
* refuses a descriptor whose hash differs from the previewed one, so a plan
* approved for one target cannot be executed against another (AI018).
*/
typedef struct action_descriptor {
uint32_t abi_version;
uint32_t struct_size;
uint32_t verb; /* action_verb_t */
uint32_t mode; /* action_mode_t */
char target[ACTION_TARGET_MAX]; /* canonical, provider-specific */
char idempotency_key[ACTION_KEY_MAX];
uint64_t lease_id; /* fencing token; 0 is refused */
uint64_t lease_generation; /* superseded generation cannot commit */
uint64_t authority_id; /* who authorised; 0 is refused */
uint64_t evidence_ref; /* the finding this answers; 0 refused */
uint32_t no_self_trigger; /* 1 = must not be raised by the plane
* that would execute it (AI021) */
uint32_t require_reversible; /* 1 = refuse if any class is not */
uint8_t descriptor_hash[32]; /* SHA-256 over the fields above */
} action_descriptor_t;
/* ── Preview ─────────────────────────────────────────────────────────────── */
typedef struct action_preview {
uint32_t abi_version;
uint32_t struct_size;
uint32_t n_blast;
uint32_t reversible_all; /* 1 = every entry is reversible */
uint64_t estimated_ns;
action_blast_entry_t blast[ACTION_BLAST_SLOTS];
uint8_t descriptor_hash[32]; /* what commit will be pinned to */
char refusal[PROV_REASON_MAX];/* empty when the action is admissible */
} action_preview_t;
/* ── Receipt ─────────────────────────────────────────────────────────────── */
typedef struct action_receipt {
uint32_t abi_version;
uint32_t struct_size;
uint32_t state; /* action_state_t */
uint32_t applied; /* 1 = the world changed */
uint64_t txn_id;
uint64_t committed_at_ns;
uint64_t lease_id;
uint64_t lease_generation;
uint8_t descriptor_hash[32];
uint8_t prior_digest[32]; /* state before, for rollback and verify */
uint8_t after_digest[32]; /* state after commit */
char detail[PROV_REASON_MAX];
} action_receipt_t;
/* ── The vtable ──────────────────────────────────────────────────────────── */
typedef struct action_provider_v1 {
uint32_t abi_version; /* ACTION_PROVIDER_ABI */
uint32_t struct_size;
void *ctx;
int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);
int (*negotiate)(void *ctx, const prov_negotiate_req_t *req,
prov_negotiate_result_t *out);
/* Never changes state, in any mode. */
int (*preview)(void *ctx, const action_descriptor_t *d,
action_preview_t *out, prov_status_t *st);
/* Reserve and fence. Fails REFUSAL when the lease is stale, the authority
* is absent, the evidence reference is empty, or the descriptor hash does
* not match the preview it claims to follow. */
int (*prepare)(void *ctx, const action_descriptor_t *d,
uint64_t *txn_id_out, prov_status_t *st);
/* Exactly once per idempotency key. A repeat returns the original receipt
* with applied == 0 and PROV_OK — a second commit is not an error, but it
* is also not a second change. */
int (*commit)(void *ctx, uint64_t txn_id, action_receipt_t *out,
prov_status_t *st);
/* Read the world back. A verify that only re-reads the provider's own
* bookkeeping proves nothing and is a conformance failure (D098). */
int (*verify)(void *ctx, uint64_t txn_id, action_receipt_t *out,
prov_status_t *st);
/* Undo a prepared or committed transaction. */
int (*rollback)(void *ctx, uint64_t txn_id, action_receipt_t *out,
prov_status_t *st);
/* After a provider restart: report every transaction that was prepared or
* committed but not verified, so the coordinator can finish or undo it
* rather than forget it (D097). */
int (*reconcile)(void *ctx, action_receipt_t *out, uint32_t max,
uint32_t *n_out, prov_status_t *st);
int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);
void *reserved[8];
} action_provider_v1_t;
const char *action_verb_name(uint32_t verb);
const char *action_state_name(uint32_t state);
const char *action_blast_name(uint32_t klass);
include/platx/agent_inventory.h
/* platx/agent_inventory.h — INT-143: central agent inventory.
*
* The server maintains a bounded table of connected agents. Each agent
* entry is keyed by its RA2C identity and populated from its capability
* snapshot at handshake time. Entries expire when the RA2C session drops
* or is explicitly evicted.
*/
#define PLAT_INV_AGENT_MAX 32
#define PLAT_INV_ID_MAX 64
#define PLAT_INV_CAP_MAX 16
#define PLAT_INV_REASON_MAX 128
typedef struct plat_inv_cap_snap {
char name[PLAT_INV_ID_MAX];
uint32_t version;
} plat_inv_cap_snap_t;
typedef struct plat_inv_agent {
char agent_id[PLAT_INV_ID_MAX]; /* RA2C identity */
uint32_t abi_version;
uint64_t connected_at_ms; /* monotonic */
plat_inv_cap_snap_t caps[PLAT_INV_CAP_MAX];
int n_caps;
int live; /* 0 after eviction */
} plat_inv_agent_t;
int plat_inv_init(void);
void plat_inv_fini(void);
/* Add or update an agent entry. Returns 0 on success, -1 if table full. */
int plat_inv_upsert(const plat_inv_agent_t *agent);
/* Mark agent as disconnected (live=0). Returns 0 or -1 (not found). */
int plat_inv_evict(const char *agent_id);
/* Fill *out with the current entry. Returns 0 or -1 (not found / evicted). */
int plat_inv_get(const char *agent_id, plat_inv_agent_t *out);
/* Count live agents. */
int plat_inv_count(void);
include/platx/alert.h
/* alert.h — S542/S543: деградация, о которой можно что-то сделать.
*
* S542 У КАЖДОЙ деградации обязаны быть пять вещей: причинная цепочка,
* время первого появления, текущее поколение, на что это влияет и что
* делать. Сообщение «health: DEGRADED» не содержит ни одной из них и
* потому не является сообщением: прочитавший его знает ровно столько
* же, сколько до чтения.
*
* Поэтому запись БЕЗ цепочки, влияния или действия отвергается на
* входе. Это неудобно тому, кто её заводит, — и в этом весь смысл:
* неудобство ложится на автора один раз, а не на дежурного каждый раз.
*
* S543 Повторы склеиваются, но НИЧЕГО не теряют. Склейка нужна затем, что
* одна и та же деградация, повторённая тысячу раз, вытесняет из поля
* зрения девятьсот других. Но склеить — не значит забыть: сохраняются
* число повторов, длительность, ХУДШАЯ виденная тяжесть и переход в
* восстановление.
*
* Худшая тяжесть отдельно важна. Деградация, побывавшая КРИТИЧЕСКОЙ и
* осевшая на «предупреждении», в сводке по текущей тяжести выглядит
* безобидно — и именно так пропускают то, что уже один раз падало.
*
* И вторая ловушка склейки: эпизоды. Деградация, восстановившаяся и
* случившаяся снова, — это ДВА эпизода, а не один длинный. Склеить их
* значит соврать о длительности: «шло восемь часов» вместо «дважды по
* пять минут с перерывом».
*/
#define PLAT_ALERT_MAX 32
#define PLAT_ALERT_KEY_MAX 64
#define PLAT_ALERT_CAUSE_MAX 4
#define PLAT_ALERT_TEXT_MAX 96
#define PLAT_ALERT_ACTION_MAX 128
typedef enum {
PLAT_SEV_INFO = 0,
PLAT_SEV_DEGRADED = 1,
PLAT_SEV_CRITICAL = 2
} plat_sev_t;
typedef struct {
char key[PLAT_ALERT_KEY_MAX];
char cause[PLAT_ALERT_CAUSE_MAX][PLAT_ALERT_TEXT_MAX];
int cause_n;
char impact[PLAT_ALERT_TEXT_MAX];
char action[PLAT_ALERT_ACTION_MAX];
uint64_t first_ms; /* начало ТЕКУЩЕГО эпизода */
uint64_t last_ms;
uint32_t generation;
uint32_t count; /* повторов в текущем эпизоде */
uint32_t episodes; /* сколько раз начиналось заново */
plat_sev_t current;
plat_sev_t worst; /* худшая за ВСЮ жизнь записи */
plat_sev_t worst_episode; /* худшая в текущем эпизоде */
int recovered;
uint64_t recovered_ms;
uint64_t total_down_ms; /* сумма по всем закрытым эпизодам */
int used;
} plat_alert_t;
typedef struct {
plat_alert_t a[PLAT_ALERT_MAX];
int n;
} plat_alert_book_t;
typedef enum {
PLAT_ALERT_OK = 0,
PLAT_ALERT_NO_CAUSE = 1, /* нет причинной цепочки */
PLAT_ALERT_NO_IMPACT = 2,
PLAT_ALERT_NO_ACTION = 3,
PLAT_ALERT_FULL = 4,
PLAT_ALERT_BAD_ARG = 5,
PLAT_ALERT_UNKNOWN = 6 /* нет такой записи */
} plat_alert_verdict_t;
void plat_alert_init(plat_alert_book_t *b);
/* Поднять или продлить. cause — массив из cause_n звеньев цепочки.
* Отвергает запись без цепочки, влияния или действия (S542). */
plat_alert_verdict_t plat_alert_raise(plat_alert_book_t *b,
const char *key, plat_sev_t sev,
const char *const *cause, int cause_n,
const char *impact, const char *action,
uint32_t generation, uint64_t now_ms);
/* Восстановление — ПЕРЕХОД, а не удаление: запись остаётся. */
plat_alert_verdict_t plat_alert_recover(plat_alert_book_t *b,
const char *key, uint64_t now_ms);
const plat_alert_t *plat_alert_find(const plat_alert_book_t *b, const char *key);
/* Сколько сейчас НЕ восстановленных. */
int plat_alert_active(const plat_alert_book_t *b);
/* Человеку: по строке на запись, с цепочкой и действием. */
int plat_alert_render(const plat_alert_book_t *b, char *buf, size_t cap);
const char *plat_sev_str(plat_sev_t s);
const char *plat_alert_verdict_name(plat_alert_verdict_t v);
include/platx/atomic_file.h
/* atomic_file.h — S403: замена файла целиком или никак.
*
* ПРОТОКОЛ
* ────────
* 1. временный файл В ТОМ ЖЕ КАТАЛОГЕ, что и цель;
* 2. запись целиком;
* 3. fsync временного — данные на диске;
* 4. close с проверкой кода — отложенная ошибка записи приходит именно сюда;
* 5. rename — подмена атомарна для читателя;
* 6. fsync КАТАЛОГА — запись каталога о подмене на диске.
*
* Шаг 1 не косметика: rename атомарен только внутри одной файловой системы, а
* /tmp сплошь и рядом отдельная. Шаг 6 — тот, которого в дереве не было нигде:
* fsync файла делает долговечными данные, но не запись каталога, указывающую
* на них. Без него вызов возвращает успех, а после потери питания цель может
* остаться прежней — и это ещё хороший исход.
*
* ОТКАЗ ВМЕСТО МОЛЧАНИЯ
* ─────────────────────
* Если у переданной таблицы I/O нет fsync, функция ОТКАЗЫВАЕТ (-ENOTSUP), а не
* пропускает шаг. Обещание долговечности без механизма — это не долговечность,
* а её вид; молчаливый пропуск здесь стоил бы ровно того, ради чего эта
* функция написана.
*
* ЗАВИСИМОСТЬ
* ───────────
* На каждой границе объявлена точка краха (S408), поэтому линковка требует
* src/core/plat_crashpoint.c. Невзведённая точка — сравнение строки с пустой,
* то есть цена нулевая; отказаться от неё значило бы отказаться от единственного
* способа проверить, что именно попадает на диск при смерти процесса.
*
* ЧЕСТНОСТЬ ПОСЛЕДНЕГО ШАГА
* ─────────────────────────
* Отказ на шаге 6 возвращается вызывающему, но rename к этому моменту УЖЕ
* произошёл и отменён быть не может: цель подменена, долговечность подмены не
* подтверждена. Возврат ошибки здесь означает «состояние на диске не
* гарантировано», а не «ничего не изменилось».
*/
struct xio_t;
/* 0 — успех. Отрицательный -errno — отказ.
*
* До шага 5 (rename) любой отказ оставляет ЦЕЛЬ НЕТРОНУТОЙ: временный файл
* удаляется, прежнее содержимое остаётся последним известным хорошим.
* -ENOTSUP означает, что таблица I/O не умеет чего-то из протокола. */
int plat_atomic_write(const struct xio_t *io, const char *path,
const void *data, size_t len, int mode);
/* Долговечное УДАЛЕНИЕ (S404).
*
* unlink убирает запись каталога, но долговечной её отсутствие делает только
* fsync каталога — ровно как и при подмене. Без него после потери питания файл
* может вернуться, и «удалён» окажется временным состоянием. Асимметрия здесь
* ничем не оправдана: удаление — такое же изменение каталога, как и создание.
*
* 0 / -errno. Отсутствие файла (-ENOENT) — успех: цель достигнута. */
int plat_atomic_remove(const struct xio_t *io, const char *path);
/* Убрать СВОИ временные файлы, оставшиеся от прежних падений (S431).
*
* Имя временного файла несёт pid создателя, и уборка идёт только по нему:
* удаляется лишь то, чей владелец ДОКАЗАННО мёртв (kill(pid,0) → ESRCH).
* Живой владелец, чужой формат имени, отказ в проверке — файл остаётся.
*
* Направление отказа выбрано намеренно: переиспользованный pid выглядит живым,
* и мы не удалим мусор. Обратная ошибка — удалить файл работающего процесса —
* стоит данных, а эта стоит места на диске.
*
* Возвращает число убранных файлов, либо -errno. */
int plat_atomic_sweep(const struct xio_t *io, const char *dir,
const char *base);
include/platx/bundle.h
/* bundle.h — S468/S485/S483: версия пакета настроек и её перевод.
*
* S468 У пакета CFG/MSX есть версия, и перевод между версиями обязан быть
* ДЕТЕРМИНИРОВАННЫМ, АВТОНОМНЫМ и ОБРАТИМЫМ.
*
* детерминированным — один вход даёт один и тот же байт-в-байт выход,
* сколько бы раз его ни прогнали. Иначе «мигрировали» и «мигрировали
* ещё раз» — разные файлы, и сравнить их нельзя.
*
* автономным — без сети, без часов, без случайности. Перевод, которому
* нужен внешний источник, нельзя выполнить там, где чинят упавший узел.
*
* обратимым — и это ЖЁСТЧЕ, чем кажется. Обратим не тот перевод, после
* которого можно что-то собрать обратно, а тот, после которого обратно
* собирается ИСХОДНЫЙ ФАЙЛ. Если операция потеряла бы значение, которое
* поставил человек, обратный перевод обязан ОТКАЗАТЬСЯ, а не подставить
* умолчание: молча вернуть «почти то же самое» хуже, чем не вернуть.
*
* S485 Сухой прогон: те же диагностики, ноль побочных действий. Проверять
* переводом «на живую» — значит проверять на том, что ломаешь.
*
* S483 Матрица понижений строится из ЭТИХ вердиктов, а не из обещаний:
* см. tests/cli/compat_matrix.sh.
*
* ФОРМАТ (намеренно скучный)
* ──────────────────────────
* bundle_version=<N>
* ключ=значение
* Строки с '#' и пустые сохраняются дословно: комментарий человека — это
* тоже содержимое.
*/
#define PLAT_BUNDLE_V_MIN 1u
#define PLAT_BUNDLE_V_CURRENT 2u
#define PLAT_BUNDLE_MAX_BYTES 8192u
#define PLAT_BUNDLE_MAX_NOTES 16
typedef enum {
PLAT_BUNDLE_OK = 0,
PLAT_BUNDLE_MALFORMED = 1,
PLAT_BUNDLE_NO_VERSION = 2,
PLAT_BUNDLE_TOO_NEW = 3,
PLAT_BUNDLE_TOO_OLD = 4,
PLAT_BUNDLE_IRREVERSIBLE = 5, /* обратный перевод потерял бы данные */
PLAT_BUNDLE_NOOP = 6, /* уже нужной версии */
PLAT_BUNDLE_OVERFLOW = 7
} plat_bundle_verdict_t;
typedef struct {
char key[64];
char what[96]; /* что именно произойдёт с этим ключом */
} plat_bundle_note_t;
typedef struct {
plat_bundle_note_t note[PLAT_BUNDLE_MAX_NOTES];
int n;
uint32_t from, to;
plat_bundle_verdict_t verdict;
} plat_bundle_plan_t;
/* Версия пакета из текста. 0 и NO_VERSION, если заголовка нет. */
plat_bundle_verdict_t plat_bundle_version(const char *text, uint32_t *out);
/* S485: сухой прогон. Ничего не пишет — ни файла, ни глобального состояния.
* Возвращает ПЛАН: по строке на каждый ключ, которого перевод коснётся. */
plat_bundle_verdict_t plat_bundle_plan(const char *text, uint32_t to,
plat_bundle_plan_t *plan);
/* Перевод. Выход детерминирован: те же байты при каждом прогоне.
* Порядок ключей исходника сохраняется; добавленные ключи идут в конец. */
plat_bundle_verdict_t plat_bundle_migrate(const char *text, uint32_t to,
char *out, size_t cap);
const char *plat_bundle_verdict_name(plat_bundle_verdict_t v);
include/platx/capreq.h
/* capreq.h — S481/S482: незнакомая возможность как решение, а не как молчание.
*
* S481 Незнакомая НЕОБЯЗАТЕЛЬНАЯ возможность = недоступна. Незнакомая
* ОБЯЗАТЕЛЬНАЯ = отказ на старте.
*
* Разница здесь не в строгости, а в том, кто отвечает за последствия.
* Необязательная возможность на то и необязательная, что её отсутствие
* предусмотрено: работаем без неё. Обязательную же кто-то потребовал,
* потому что без неё поведение узла НЕ ТО, за которое он отвечает.
* Запуститься, не поняв такого требования, значит начать работу под
* чужим именем: снаружи узел выглядит настроенным, а внутри — нет.
* Поэтому отказ на СТАРТЕ, а не первая ошибка через час.
*
* S482 Снятая возможность не исчезает молча: у неё есть состояние RETIRED,
* по которому её убирают из перечней команд и статуса. Возможность,
* которую сняли, но которая осталась в списке, — это обещание, за
* которым ничего нет.
*/
typedef enum {
PLAT_CAP_ST_ABSENT = 0, /* нам не известна вовсе */
PLAT_CAP_ST_PRESENT = 1,
PLAT_CAP_ST_RETIRED = 2 /* была, снята по пути устаревания */
} plat_cap_state_t;
typedef struct {
const char *name;
int mandatory;
} plat_cap_req_t;
typedef enum {
PLAT_CAP_OK = 0,
PLAT_CAP_REFUSE = 1, /* обязательная недоступна — старт отменён */
PLAT_CAP_BAD_ARG = 2
} plat_cap_verdict_t;
plat_cap_state_t plat_cap_state(const char *name);
/* Разобрать требования. out_mask — биты доступных известных возможностей.
* why — человекочитаемая причина отказа (имя возможности внутри).
* При отказе out_mask НЕ выставляется: полумаска хуже отсутствия маски. */
plat_cap_verdict_t plat_cap_resolve(const plat_cap_req_t *reqs, size_t n,
uint32_t *out_mask,
char *why, size_t whycap);
/* Перечень для статуса/команд: снятые сюда не попадают (S482). */
size_t plat_cap_list_visible(const char **out, size_t cap);
include/platx/cdata2.h
/* Options for cdata2_save(). All pointer fields may be NULL for defaults. */
typedef struct {
const char *encoding; /* "hex" | "base64" | NULL → "base64" */
const char *compression; /* "gzip" | NULL → none */
const char *encryption; /* "rc4" | "chacha20" | "aes256gcm" | NULL → none */
const char *key; /* secret supplied via --key=SECRET */
const char *keyring; /* keyring entry name (--keyring=NAME) */
int embed_key; /* 1: store literal key hex in container field */
const char *hash_algo; /* "sha256" | "crc32" | NULL → "sha256" */
} cdata2_opts_t;
/* Pack raw bytes into a CDATA v2 container string.
* Returns malloc'd NUL-terminated string, or NULL on error.
* opts may be NULL for all defaults. Caller frees. */
char *cdata2_save(const unsigned char *raw, size_t rawlen,
const cdata2_opts_t *opts);
/* Unpack a CDATA v2 container string.
* override_key: secret passed on command line when key_ref=="0".
* Returns malloc'd raw bytes; *outlen = byte count. NULL on error. */
unsigned char *cdata2_load(const char *container, const char *override_key,
size_t *outlen);
/* Print container metadata to stdout.
* Returns 0 if the container is well-formed, -1 otherwise. */
int cdata2_info(const char *container);
/* Transparent dispatch helper.
* If s begins with a valid CDATA v2 header "[...]", decode and return the
* raw payload (malloc'd, *outlen set). Returns NULL if s is not a CDATA
* container or if decoding fails. Never calls die(). */
unsigned char *mfp_maybe_cdata(const char *s, size_t *outlen);
unsigned char *hex_decode(const char *, size_t *);
char *hex_encode(const unsigned char *, size_t);
unsigned char *b64_decode(const char *, size_t *);
char *b64_encode(const unsigned char *, size_t);
unsigned char *cdata_literal_decode(const char *, size_t *);
include/platx/cfgkey.h
/* cfgkey.h — S462: четыре разных ответа вместо одного «unknown key».
*
* Сейчас конфигуратор на любой непринятый ключ отвечает одинаково: «unknown
* key 'x'» и отказ. Под этим ответом лежат четыре РАЗНЫХ положения, и
* оператору нужно разное действие в каждом:
*
* ИЗВЕСТНЫЙ — принимаем.
* УСТАРЕВШИЙ — принимаем, но говорим, чем заменить. Отказ здесь ломал бы
* работающие конфиги при обновлении.
* УДАЛЁННЫЙ — отказ с указанием, ЧТО делать: ключ был, его убрали, и
* молчаливое «unknown» заставляет искать опечатку там, где
* её нет.
* ОПЕЧАТКА — отказ, но с подсказкой. «protect.module» вместо
* «protect.modules» — самая частая причина непринятого
* конфига, и она решается одной строкой вывода.
* НЕИЗВЕСТНЫЙ — отказ без подсказки: похожего нет.
*
* Расстояние правки считается до 2 включительно. Дальше подсказка становится
* вредной: «вы имели в виду совсем другое?» хуже, чем её отсутствие.
*/
typedef enum {
PLAT_CFGKEY_KNOWN = 0,
PLAT_CFGKEY_DEPRECATED = 1,
PLAT_CFGKEY_REMOVED = 2,
PLAT_CFGKEY_TYPO = 3,
PLAT_CFGKEY_UNKNOWN = 4
} plat_cfgkey_class_t;
typedef struct {
const char *key;
const char *note; /* чем заменить / когда убран */
} plat_cfgkey_entry_t;
typedef struct {
const plat_cfgkey_entry_t *known; size_t n_known;
const plat_cfgkey_entry_t *deprecated; size_t n_deprecated;
const plat_cfgkey_entry_t *removed; size_t n_removed;
} plat_cfgkey_table_t;
/* Классифицировать ключ. suggest получает подсказку (имя похожего ключа или
* note), если она есть; иначе пустую строку. */
plat_cfgkey_class_t plat_cfgkey_classify(const plat_cfgkey_table_t *t,
const char *key,
char *suggest, size_t suggest_sz);
const char *plat_cfgkey_class_name(plat_cfgkey_class_t c);
/* Признак «расстояние неприменимо»: NULL-аргумент или ключ длиннее предела.
* B-024: раньше в этих случаях возвращалось 64 — то же число, каким могло
* оказаться настоящее расстояние между двумя 64-символьными ключами, так что
* отказ был неотличим от ответа. Настоящее расстояние никогда не превышает
* предела длины, поэтому SIZE_MAX не может быть спутан с ним. */
#define PLAT_CFGKEY_DISTANCE_NA ((size_t)-1)
/* Расстояние правки, вынесено ради проверок. Возвращает
* PLAT_CFGKEY_DISTANCE_NA, если хоть один аргумент NULL или длиннее предела. */
size_t plat_cfgkey_distance(const char *a, const char *b);
include/platx/chain.h
/* chain.h — S446/S419: пропажа, перестановка и подмена члена обязаны быть видны.
*
* Манифест доказательств — это список членов с их хешами. Список сам по себе
* защищает от подмены СОДЕРЖИМОГО и ни от чего больше: удалить строку или
* поменять две местами он не мешает. Поэтому строки связаны в цепочку:
*
* link[i] = SHA256( link[i-1] || имя || размер || хеш(содержимое) )
*
* Каждое звено зависит от всех предыдущих, поэтому пропажа, перестановка и
* подмена одинаково рвут цепочку. Хеш берётся существующий (crypto_sha256);
* ничего нового здесь не заводится.
*
* ЗАВЕРШЁН ИЛИ НЕТ (S419)
* ───────────────────────
* Собранный набор отличается от недособранного НАЛИЧИЕМ замыкающей записи.
* Без неё набор — INCOMPLETE, и это не ошибка чтения: сборка могла оборваться
* крахом, и выдать её за готовую значило бы подписать неизвестно что.
*/
typedef enum {
PLAT_CHAIN_SEALED = 0, /* цепочка сошлась и замкнута */
PLAT_CHAIN_INCOMPLETE = 1, /* сошлась, но не замкнута — сборка оборвана */
PLAT_CHAIN_MISSING = 2, /* члена нет на диске */
PLAT_CHAIN_REPLACED = 3, /* содержимое члена иное */
PLAT_CHAIN_BROKEN = 4 /* звено не сходится: порядок или сам
* манифест изменены */
} plat_chain_state_t;
typedef struct {
plat_chain_state_t state;
size_t members; /* сколько записей в манифесте */
size_t at; /* индекс первой расходящейся записи */
char name[128]; /* её имя */
/* При BROKEN: целы ли САМИ ЧЛЕНЫ на диске.
*
* Цепочка доказывает, что манифест не тот, но НЕ различает перестановку
* записей и правку манифеста — по одному разорванному звену это не
* восстановить, и выдавать догадку за вывод здесь нельзя. Зато проверяемо
* другое: сходится ли каждый файл со СВОИМ записанным хешем. Сходится —
* значит тронут манифест, а не содержимое; не сходится — тронуто и оно.
* Это оператору и нужно знать в первую очередь. */
int members_intact;
} plat_chain_report_t;
struct xio_t;
/* Создать пустой манифест. */
int plat_chain_create(const struct xio_t *io, const char *manifest);
/* Добавить члена: содержимое читается из dir/name. */
int plat_chain_add(const struct xio_t *io, const char *manifest,
const char *dir, const char *name);
/* Замкнуть набор. После замыкания добавлять нельзя. */
int plat_chain_seal(const struct xio_t *io, const char *manifest);
/* Проверить манифест против того, что реально лежит в dir. */
int plat_chain_verify(const struct xio_t *io, const char *manifest,
const char *dir, plat_chain_report_t *out);
include/platx/child_ipc.h
/* platx/child_ipc.h — wire ABI between worker child_host and child proxy.
* Length-prefixed frames over a connected SOCK_STREAM. Not a domain API.
*/
#define PLAT_IPC_MAGIC 0x31584C50u /* 'PLX1' */
#define PLAT_IPC_MAX 4096u
#define PLAT_IPC_NAME 64u
#define PLAT_CHILD_IPC_FD 3
enum plat_ipc_type {
PLAT_IPC_HELLO = 1,
PLAT_IPC_HELLO_ACK,
PLAT_IPC_REQUIRE,
PLAT_IPC_REQUIRE_OK,
PLAT_IPC_REQUIRE_ERR,
PLAT_IPC_PROVIDE,
PLAT_IPC_PROVIDE_OK,
PLAT_IPC_PROVIDE_ERR,
PLAT_IPC_REVOKE,
PLAT_IPC_REVOKE_OK,
PLAT_IPC_REVOKE_ERR,
PLAT_IPC_CALL,
PLAT_IPC_CALL_OK,
PLAT_IPC_CALL_ERR,
PLAT_IPC_READY,
PLAT_IPC_BYE
};
/* Reserved CALL handles for Core primitives. Capability handles are 1..0x7fffffff.
* Unknown handle / method / short payload → CALL_ERR, no half-apply. */
#define PLAT_IPC_H_RES 0x80000001u
#define PLAT_IPC_H_EVENT 0x80000002u
#define PLAT_IPC_RES_OWN 1u
#define PLAT_IPC_RES_RELEASE 2u
#define PLAT_IPC_RES_RELEASE_OWNER 3u
#define PLAT_IPC_RES_CHECK 4u
#define PLAT_IPC_EV_EMIT 1u
#define PLAT_IPC_OWNER_LEN 12u
#define PLAT_IPC_RES_NAME 48u
#define PLAT_IPC_RES_OWN_LEN (PLAT_IPC_OWNER_LEN + 4u + 8u + PLAT_IPC_RES_NAME)
typedef struct plat_ipc_hdr {
uint32_t magic;
uint32_t type;
uint32_t seq;
uint32_t len;
} plat_ipc_hdr_t;
typedef struct plat_ipc_msg {
uint32_t type;
uint32_t seq;
uint32_t len;
uint8_t data[PLAT_IPC_MAX];
} plat_ipc_msg_t;
int plat_ipc_send(int fd, uint32_t type, uint32_t seq,
const void *payload, uint32_t len);
/* timeout_ms < 0 = block. 0 = poll once. >0 = bounded wait. */
int plat_ipc_recv(int fd, plat_ipc_msg_t *msg, int timeout_ms);
int plat_ipc_rpc(int fd, uint32_t *seq, uint32_t type,
const void *req, uint32_t reqlen,
uint32_t ok_type, uint32_t err_type,
void *okbuf, uint32_t okcap, uint32_t *oklen,
int timeout_ms);
uint32_t plat_ipc_put_cap(uint8_t *dst, const char *name, uint32_t version);
int plat_ipc_get_cap(const uint8_t *src, uint32_t len,
char *name, uint32_t name_cap, uint32_t *version);
uint32_t plat_ipc_put_call(uint8_t *dst, uint32_t handle, uint32_t method,
const void *in, uint32_t in_len);
int plat_ipc_get_call(const uint8_t *src, uint32_t len,
uint32_t *handle, uint32_t *method,
const uint8_t **data, uint32_t *data_len);
include/platx/childrec.h
/* childrec.h — S421/S422: запись о ребёнке не переживает переиспользование pid.
*
* PID НЕ ЯВЛЯЕТСЯ ИДЕНТИФИКАТОРОМ
* ───────────────────────────────
* Номер процесса переиспользуется, и после перезапуска (а на загруженной
* машине и без него) под тем же номером живёт кто-то другой. Сохранённая
* запись «мой ребёнок — pid 4242» после этого указывает на чужой процесс, и
* всё, что по ней делают — сигнал, привязка, снятие ограничений, — делают с
* ним. Поэтому запись хранит ещё и ВРЕМЯ РОЖДЕНИЯ процесса (поле 22
* /proc/<pid>/stat) и поколение владельца. Совпасть должны все три.
*
* Поле 22 читается от ПОСЛЕДНЕЙ ')' в строке: имя процесса заключено в скобки
* и может содержать и пробелы, и сами скобки, поэтому разбор по пробелам с
* начала строки ломается на процессе с именем "x) 1 2 3 4 5".
*
* ТРИ СОСТОЯНИЯ (S421)
* ────────────────────
* PENDING — ребёнок запущен, о готовности не сообщал. Это НЕ «не запустился»:
* крах родителя между запуском и READY оставляет именно такую запись, и
* достроить её в любую сторону значит либо бросить работающего ребёнка, либо
* ждать несуществующего.
*/
struct xio_t;
typedef enum {
PLAT_CHILD_PENDING = 0, /* запущен, готовности не подтверждал */
PLAT_CHILD_READY = 1,
PLAT_CHILD_GONE = 2
} plat_child_state_t;
typedef struct {
uint32_t pid;
uint32_t generation;
uint64_t birth; /* starttime, поле 22 /proc/<pid>/stat */
plat_child_state_t state;
} plat_childrec_t;
typedef enum {
PLAT_CHILD_VALID = 0,
PLAT_CHILD_DEAD = 1, /* процесса нет */
PLAT_CHILD_REUSED = 2, /* pid тот же, процесс другой */
PLAT_CHILD_STALE_GEN = 3 /* владелец пересоздан */
} plat_child_verdict_t;
/* Снять запись о живом процессе. -ESRCH, если его нет. */
int plat_child_capture(plat_childrec_t *out, uint32_t pid, uint32_t generation);
/* Сверить запись с тем, что живёт под этим pid ПРЯМО СЕЙЧАС. */
plat_child_verdict_t plat_child_verify(const plat_childrec_t *r,
uint32_t current_generation);
int plat_child_save(const struct xio_t *io, const char *path,
const plat_childrec_t *r);
int plat_child_load(const struct xio_t *io, const char *path,
plat_childrec_t *out);
include/platx/cmd_authz.h
/* platx/cmd_authz.h — INT-146: central command authorization.
*
* Every command dispatched from the central node must carry:
* scope — what it may affect (capability namespace)
* target — which agent/context identity
* expiry — absolute monotonic ms; 0 = no expiry (admin only)
* nonce — replay protection: each nonce accepted exactly once
* audit_out — filled on both ALLOW and DENY with the outcome
*
* A command missing any required field is DENIED immediately.
* A replayed nonce is DENIED even if all other fields are valid.
* An expired command is DENIED even if the nonce is fresh.
*
* This module is stateful: it maintains the seen-nonce ring.
* Call plat_cmd_authz_init() once before use; plat_cmd_authz_fini() on
* shutdown to release the ring.
*/
#define PLAT_AUTHZ_SCOPE_MAX 64
#define PLAT_AUTHZ_TARGET_MAX 64
#define PLAT_AUTHZ_REASON_MAX 128
#define PLAT_AUTHZ_NONCE_RING 256 /* seen-nonce ring capacity */
/* ── Command envelope ────────────────────────────────────────────────── */
typedef struct plat_cmd_envelope {
char scope[PLAT_AUTHZ_SCOPE_MAX]; /* capability being exercised */
char target[PLAT_AUTHZ_TARGET_MAX]; /* agent id or context id str */
uint64_t expiry_ms; /* monotonic ms; 0 = no expiry (requires admin) */
uint64_t nonce; /* replay token — must be unique per scope+target */
int admin; /* non-zero: allows expiry_ms == 0 */
} plat_cmd_envelope_t;
/* ── Authorization result ────────────────────────────────────────────── */
typedef enum plat_authz_verdict {
PLAT_AUTHZ_ALLOW = 0,
PLAT_AUTHZ_DENY = 1
} plat_authz_verdict_t;
typedef struct plat_authz_result {
plat_authz_verdict_t verdict;
char reason[PLAT_AUTHZ_REASON_MAX];
} plat_authz_result_t;
/* ── API ─────────────────────────────────────────────────────────────── */
int plat_cmd_authz_init(void);
void plat_cmd_authz_fini(void);
/* Authorize one command envelope.
* Returns 0 (ALLOW) or -1 (DENY).
* result is always populated.
* Side effect on ALLOW: nonce is recorded as seen.
*/
int plat_cmd_authz(const plat_cmd_envelope_t *env,
plat_authz_result_t *result);
/* Read the monotonic clock in ms (same basis as expiry_ms). */
int plat_authz_clock_ms(uint64_t *out);
include/platx/cmd_dispatch.h
/* platx/cmd_dispatch.h — INT-148: typed request dispatcher.
* Authorization via cmd_authz before dispatch; result via incident_env. */
#define PLAT_DISPATCH_MAX_HANDLERS 32
#define PLAT_DISPATCH_CMD_MAX 64
typedef int (*plat_cmd_handler_fn)(const char *cmd_type,
const void *payload, size_t payloadsz,
plat_incident_env_t *env);
int plat_dispatch_init(void);
void plat_dispatch_fini(void);
/* Register handler for cmd_type. -1 on duplicate or table full. */
int plat_dispatch_register(const char *cmd_type, plat_cmd_handler_fn fn);
/* Remove handler. Returns 0 if found, -1 if not. */
int plat_dispatch_deregister(const char *cmd_type);
/* Authorize via auth_env, find handler for cmd_type, invoke.
* Returns 0 on success, -1 on authz failure or missing handler. */
int plat_dispatch(const plat_cmd_envelope_t *auth_env,
const char *cmd_type,
const void *payload, size_t payloadsz,
plat_incident_env_t *out_env);
include/platx/compat_handshake.h
/* platx/compat_handshake.h — INT-142: agent↔server compatibility handshake.
*
* Before any capability exchange the agent presents its ABI version,
* capability versions, policy schema digest, and feature bits. The server
* checks these against its own table and replies: accepted / downgraded /
* rejected. A rejected handshake must not proceed to capability exchange.
*
* Invariant: a newer server MUST accept an older agent with a lower ABI
* version, provided it can supply the downgraded interface. An agent that
* demands features the server does not have is rejected, not silently
* degraded.
*/
#define PLAT_COMPAT_CAP_MAX 16
#define PLAT_COMPAT_NAME_MAX 32
#define PLAT_COMPAT_REASON_MAX 128
/* ── Agent advertises ────────────────────────────────────────────────── */
typedef struct plat_compat_cap {
char name[PLAT_COMPAT_NAME_MAX];
uint32_t version; /* agent's own version of this capability */
uint32_t min_version; /* minimum it can tolerate from the server */
} plat_compat_cap_t;
typedef struct plat_compat_offer {
uint32_t abi_version; /* PLATX_CORE_ABI of the agent */
uint32_t policy_digest; /* CRC32 of current policy schema */
uint64_t feature_bits; /* optional feature flags */
plat_compat_cap_t caps[PLAT_COMPAT_CAP_MAX];
int n_caps;
} plat_compat_offer_t;
/* ── Server replies ──────────────────────────────────────────────────── */
typedef enum plat_compat_verdict {
PLAT_COMPAT_ACCEPTED = 0, /* full match or server provides downgrade */
PLAT_COMPAT_DOWNGRADED = 1, /* accepted with capability version floor lowered */
PLAT_COMPAT_REJECTED = 2 /* incompatible; must not proceed */
} plat_compat_verdict_t;
typedef struct plat_compat_reply {
plat_compat_verdict_t verdict;
uint32_t server_abi; /* server's own ABI version */
uint64_t accepted_features; /* server-supported subset */
/* Per-capability negotiated versions (same order as offer.caps). */
uint32_t negotiated[PLAT_COMPAT_CAP_MAX];
char reason[PLAT_COMPAT_REASON_MAX];
} plat_compat_reply_t;
/* ── Server-side check ───────────────────────────────────────────────── */
/* plat_compat_check: validate an agent's offer against this server's table.
* Returns 0 (ACCEPTED or DOWNGRADED) or -1 (REJECTED).
* reply is always populated regardless of verdict.
*/
int plat_compat_check(const plat_compat_offer_t *offer,
plat_compat_reply_t *reply);
/* plat_compat_check_strict: like check, but treats DOWNGRADED as REJECTED.
* Use when the server cannot tolerate version gaps.
*/
int plat_compat_check_strict(const plat_compat_offer_t *offer,
plat_compat_reply_t *reply);
/* ── Agent-side validation ───────────────────────────────────────────── */
/* plat_compat_accept_reply: the agent inspects the server's reply and
* decides whether to proceed. Returns 0 (proceed) or -1 (abort).
* Aborts if verdict is REJECTED, or if any negotiated version is below
* the agent's own min_version for that capability.
*/
int plat_compat_accept_reply(const plat_compat_offer_t *offer,
const plat_compat_reply_t *reply);
include/platx/compstat.h
/* compstat.h — S505/S537/S538/S539: состояние пути так, чтобы им можно было
* пользоваться, не читая отладочный журнал.
*
* S505 У каждой составляющей пути шесть полей, и ни одно не лишнее:
* чего добивались, что есть, поколение, владелец, последний переход,
* причина расхождения.
* Пять первых отвечают на «что происходит», шестое — на «почему не то,
* что заказано». Строка, где желаемое не равно фактическому, а причина
* не названа, ОТВЕРГАЕТСЯ на входе: это и есть тот самый тихий отказ,
* ради которого статус вообще существует.
*
* S537 Человеку — коротко, по этапам, без секретов, с указанием следующего
* шага. Отказ без следующего шага — половина отказа.
*
* S538 Машине — JSON с номером схемы. И ГЛАВНОЕ: в человеческом выводе не
* должно быть ни одного смысла, которого нет в машинном. Иначе
* автоматизация читает не то же самое, что человек, и расхождение
* обнаружится в тот единственный раз, когда оно дорого.
*
* S539 Затирание секретов — ОДНА функция на оба вывода. Два места затирания
* расходятся: сначала чинят одно, потом полгода не замечают второго.
* Поэтому и консоль, и JSON, и журнал берут текст только отсюда.
*/
#define PLAT_CS_SCHEMA 1
#define PLAT_CS_NAME_MAX 48
#define PLAT_CS_STATE_MAX 24
#define PLAT_CS_TRANS_MAX 40
#define PLAT_CS_DETAIL_MAX 192
#define PLAT_CS_MAX 16
typedef struct {
char name[PLAT_CS_NAME_MAX];
char desired[PLAT_CS_STATE_MAX];
char actual[PLAT_CS_STATE_MAX];
uint64_t generation;
char owner[PLAT_CS_NAME_MAX];
char last_transition[PLAT_CS_TRANS_MAX];
uint64_t last_transition_ms; /* монотонные мс от старта пути */
plat_reason_t reason;
char detail[PLAT_CS_DETAIL_MAX]; /* хранится УЖЕ затёртым */
} plat_cs_row_t;
typedef struct {
plat_cs_row_t row[PLAT_CS_MAX];
int n;
} plat_cs_t;
void plat_cs_init(plat_cs_t *s);
/* Добавить строку. detail затирается ЗДЕСЬ, один раз, до любого вывода.
* Возвращает -1, если desired != actual, а причина PLAT_R_OK: состояние не
* достигнуто и не объяснено — такую строку показывать нельзя. */
int plat_cs_add(plat_cs_t *s, const char *name,
const char *desired, const char *actual,
uint64_t generation, const char *owner,
const char *last_transition, uint64_t transition_ms,
plat_reason_t reason, const char *detail);
/* S539: единственное место затирания. Возвращает длину результата.
* Затирает значение после ключевых слов (key/token/secret/password/psk/
* authorization) и длинные шестнадцатеричные/base64-подобные строки. */
size_t plat_cs_redact(char *dst, size_t cap, const char *src);
/* S537: человеку. Одна строка на составляющую + следующий шаг у каждой,
* которая не OK. */
int plat_cs_render_human(const plat_cs_t *s, char *buf, size_t cap);
/* S538: машине. Один объект со схемой и массивом составляющих. */
int plat_cs_render_json(const plat_cs_t *s, char *buf, size_t cap);
/* Всё ли достигнуто. 0 — да. Иначе индекс первой недостигнутой + 1. */
int plat_cs_first_bad(const plat_cs_t *s);
include/platx/core.h
/* platx/core.h — SDK umbrella. Modules compile with -I include only.
* Static boot core that memfd links: platx/kernel.h + libplatx.a. */
include/platx/corr_id.h
/* platx/corr_id.h — Correlation ID across all subsystems (INT-111). */
#define PLAT_CORR_ID_BYTES 16
#define PLAT_CORR_ID_HEXLEN 33
typedef struct { uint8_t b[PLAT_CORR_ID_BYTES]; } plat_corr_id_t;
extern const plat_corr_id_t PLAT_CORR_ID_ZERO;
/* Generate a fresh correlation ID from entropy. Returns 0/-1. */
int plat_corr_id_new(plat_corr_id_t *out);
/* Пишет 32 hex-символа и терминатор. NULL в любом аргументе — тихий отказ
* без записи: тип возврата не позволяет сообщить об этом иначе. */
void plat_corr_id_hex(const plat_corr_id_t *id, char out[PLAT_CORR_ID_HEXLEN]);
int plat_corr_id_parse(const char *hex, plat_corr_id_t *out);
int plat_corr_id_is_zero(const plat_corr_id_t *id);
/* Thread-local propagation — matches trace_ctx pattern. */
void plat_corr_id_set_current(const plat_corr_id_t *id);
int plat_corr_id_get_current(plat_corr_id_t *out); /* 0 if set, -1 if none */
void plat_corr_id_clear(void);
include/platx/crashpoint.h
/* crashpoint.h — S408: детерминированные точки краха на границах долговечности.
*
* ЗАЧЕМ
* ─────
* Пункты «крах во время X» (S409–S422) без этого механизма не тесты, а
* пожелания: убить процесс ровно между fsync и rename нельзя ни таймером, ни
* сигналом извне — окно измеряется микросекундами и не воспроизводится. Точка
* краха переворачивает задачу: не «поймать момент», а «объявить его заранее и
* прийти в него детерминированно».
*
* КАК
* ───
* Код расставляет именованные точки на границах долговечности. Прогон
* вооружается переменными окружения — потому что взводить надо ДОЧЕРНИЙ
* процесс, а окружение переживает fork и exec, в отличие от любого вызова API:
*
* PLATX_CRASH_AT=<имя> в какой точке умереть
* PLATX_CRASH_NTH=<k> на k-м проходе через неё (по умолчанию 1)
* PLATX_CRASH_ID_FILE=<путь> куда записать имя точки ПЕРЕД смертью
* PLATX_CRASH_TRACE=<путь> дописывать каждую пройденную точку (разведка)
*
* ПОЧЕМУ _exit, А НЕ abort И НЕ exit
* ──────────────────────────────────
* exit() выполняет atexit-обработчики и сбрасывает буферы stdio; abort() тоже
* может дойти до сброса. Крах, который сбрасывает буферы, — не крах: он
* доносит до диска ровно то, чего настоящая потеря питания не донесла бы, и
* тест после него проверял бы состояние, которого в жизни не бывает. Здесь
* только _exit(PLATX_CRASH_EXIT): ни обработчиков, ни сброса, ни закрытия
* потоков.
*
* По той же причине идентификатор точки пишется через write+fsync напрямую, а
* не через stdio: он обязан пережить смерть, которая ничего не сбрасывает.
*/
/* Код выхода «умер в объявленной точке». Отличается от 0/1/77, чтобы не
* сливаться с PASS/FAIL/SKIP тестового договора. */
#define PLATX_CRASH_EXIT 99
/* Объявить границу. Если прогон вооружён на эту точку и это её k-й проход —
* процесс умирает здесь и не возвращается. Иначе возврат немедленный. */
void plat_crash_point(const char *name);
/* Вооружить/разоружить программно (для случая, когда окружение недоступно).
* name == NULL снимает взвод. */
void plat_crash_arm(const char *name, unsigned nth);
/* Сколько раз эта точка была пройдена в текущем процессе. Нужна разведке:
* «сколько границ прошло до отказа» — это факт, а не догадка. */
unsigned plat_crash_hits(const char *name);
include/platx/cryptopol.h
/* cryptopol.h — S484: замороженные имена криптополитик, без псевдонимов.
*
* Имя алгоритма в конфиге и на проводе — это ИДЕНТИФИКАТОР, а не описание.
* «sha256», «SHA-256» и «Sha256» для человека одно и то же; для системы,
* которая по этому имени выбирает поведение и потом ссылается на него в
* доказательствах, — три разных строки. Если принять все три и молча свести
* к одной, то в журнале окажется имя, которого оператор не писал, а при
* следующем разборе никто не докажет, что именно было согласовано.
*
* Отсюда два правила, оба неудобные и оба намеренные:
*
* 1. Точное совпадение или отказ. Псевдоним — это ОТДЕЛЬНЫЙ вердикт, а не
* «почти правильно»: человеку надо сказать, что мы поняли, чего он
* хотел, и всё равно не приняли.
*
* 2. Пустое пересечение при согласовании — отказ. Никогда не «возьмём
* что-нибудь по умолчанию»: подстановка при неудачном согласовании —
* это и есть тихая подмена, ради предотвращения которой всё остальное.
*/
#define PLAT_CP_SHA256 0x0001u
#define PLAT_CP_SHA512 0x0002u
#define PLAT_CP_ED25519 0x0004u
#define PLAT_CP_AES256GCM 0x0008u
#define PLAT_CP_KNOWN 0x000Fu
typedef enum {
PLAT_CP_OK = 0,
PLAT_CP_UNKNOWN = 1, /* такого имени нет вовсе */
PLAT_CP_ALIAS = 2, /* узнали намерение, но имя не то */
PLAT_CP_RETIRED = 3, /* имя было, политика снята */
PLAT_CP_NO_COMMON = 4 /* пересечение пусто — БЕЗ подстановки */
} plat_cp_verdict_t;
/* Точный поиск. При OK кладёт бит в *out; иначе *out не трогает. */
plat_cp_verdict_t plat_cryptopol_lookup(const char *name, uint32_t *out);
/* Согласование: пересечение локальной и удалённой политик.
* Пустое пересечение — отказ, *out не трогается. */
plat_cp_verdict_t plat_cryptopol_negotiate(uint32_t local, uint32_t peer,
uint32_t *out);
const char *plat_cryptopol_name(uint32_t bit);
const char *plat_cp_verdict_name(plat_cp_verdict_t v);
include/platx/ctlmsg.h
/* ctlmsg.h — S472/S473: версия управляющего протокола и порядок сообщений.
*
* S472 У протокола присоединения есть версия, и он обязан внятно вести себя
* при: незнакомом типе сообщения, незнакомом поле, НАРУШЕННОМ ПОРЯДКЕ и
* смене поколения собеседника.
*
* Порядок — самое недооценённое из этого. Управляющий протокол — это
* автомат, а не мешок сообщений. READY до HELLO означает, что говорящий
* либо не тот, за кого себя выдаёт, либо переподключился, а мы этого не
* заметили. Принять такое «потому что поле READY выглядит нормально» —
* значит начать работу с состоянием, которого не согласовывали.
*
* Поколение (peer_gen) отличает «тот же собеседник продолжает» от «он
* перезапустился и начал заново». Сообщение от ПРЕДЫДУЩЕГО поколения,
* пришедшее после нового HELLO, — это эхо мёртвого собеседника. Оно
* обязано быть отвергнуто: иначе ответ уедет тому, кого уже нет, а
* состояние соединения окажется склеенным из двух жизней.
*
* S473 То же для XIM: CHILD READY несёт версию. Родитель принимает ребёнка
* версии не ниже объявленного минимума; иначе — отказ. И отказ значит
* ИМЕННО отказ: ребёнок, чей READY отвергнут, не считается готовым — ни
* в статусе, ни в учёте, ни как «наверное, поднялся».
*
* НЕЗНАКОМЫЕ ТИПЫ: две разные вещи
* ────────────────────────────────
* Тип из ДИАПАЗОНА РАСШИРЕНИЙ (>= PLAT_CTL_T_EXT_BASE) можно пропустить: он
* объявлен необязательным заранее. Любой другой незнакомый тип — отказ:
* молчаливое игнорирование обязательного сообщения выглядит на той стороне
* как согласие.
*/
#define PLAT_CTL_PROTO_MIN 1u
#define PLAT_CTL_PROTO_CURRENT 2u
/* Ниже этой версии ребёнок не принимается (S473). */
#define PLAT_CTL_CHILD_MIN 1u
typedef enum {
PLAT_CTL_T_HELLO = 1,
PLAT_CTL_T_CHILD_READY = 2,
PLAT_CTL_T_CHILD_FAIL = 3,
PLAT_CTL_T_BYE = 4,
PLAT_CTL_T_EXT_BASE = 0x8000 /* и выше — необязательные расширения */
} plat_ctl_type_t;
typedef struct {
uint16_t proto;
uint16_t type;
uint32_t flags; /* незнакомые биты — отказ */
uint64_t peer_gen; /* поколение собеседника; растёт при перезапуске */
uint64_t seq;
} plat_ctl_msg_t;
#define PLAT_CTL_FLAG_KNOWN 0x00000003u
typedef enum {
PLAT_CTL_ST_IDLE = 0, /* ничего не согласовано */
PLAT_CTL_ST_HELLO = 1, /* HELLO принят, версия известна */
PLAT_CTL_ST_READY = 2, /* ребёнок объявлен готовым */
PLAT_CTL_ST_DEAD = 3 /* завершено или отвергнуто */
} plat_ctl_state_t;
typedef struct {
plat_ctl_state_t state;
uint16_t proto; /* согласованная версия */
uint64_t peer_gen; /* поколение текущей жизни */
uint64_t last_seq;
int child_ready;/* 1 только после ПРИНЯТОГО READY */
} plat_ctl_peer_t;
typedef enum {
PLAT_CTL_OK = 0,
PLAT_CTL_UNKNOWN_MSG = 1, /* обязательный тип, которого мы не знаем */
PLAT_CTL_IGNORED_EXT = 2, /* необязательное расширение — пропущено */
PLAT_CTL_BAD_PROTO = 3,
PLAT_CTL_OUT_OF_ORDER = 4,
PLAT_CTL_STALE_GEN = 5, /* эхо предыдущей жизни собеседника */
PLAT_CTL_UNKNOWN_FLAG = 6,
PLAT_CTL_REPLAY = 7, /* seq не растёт */
PLAT_CTL_BAD_ARG = 8
} plat_ctl_verdict_t;
void plat_ctl_peer_init(plat_ctl_peer_t *p);
plat_ctl_verdict_t plat_ctl_accept(plat_ctl_peer_t *p, const plat_ctl_msg_t *m);
const char *plat_ctl_verdict_name(plat_ctl_verdict_t v);
const char *plat_ctl_state_name(plat_ctl_state_t s);
include/platx/desc.h
/* desc.h — S453/S454/S465/S466: согласование размера у расширяемых договоров.
*
* ЗАЧЕМ ЗАГОЛОВОК У КАЖДОГО ПУБЛИЧНОГО ДЕСКРИПТОРА
* ────────────────────────────────────────────────
* Структура, которую заполняет ОДНА сторона, а читает ДРУГАЯ, собранная
* другим заголовком, — это протокол, даже если она объявлена в C. Без размера
* читатель берёт поля по своим смещениям: у более старой стороны за концом
* структуры мусор, у более новой — поля, о которых он не знает.
*
* Поэтому каждый такой дескриптор начинается с plat_desc_hdr_t, и размер в нём
* заполняет ТОТ, КТО ЗАПОЛНЯЕТ СТРУКТУРУ.
*
* ПРАВИЛА (и почему они не симметричны)
* ─────────────────────────────────────
* size < нашего → отказ. За концом чужой структуры наши смещения не
* значат ничего.
* size > нашего → ПРИНЯТЬ. Известные поля на местах, лишние не трогаем.
* Иначе добавление поля в конец ломало бы всех, и
* расширяемость была бы на словах.
* size == 0 → отказ: заголовок не заполнен вовсе.
* size невозможен → отказ: больше объявленного максимума или не кратен
* выравниванию. Это не «новая версия», это мусор.
*
* version → отдельно от размера: версия про СМЫСЛ полей, размер про
* их число. Старшая версия — отказ, а не попытка угадать.
*
* reserved != 0 → отказ (S454). Незнакомое значение в зарезервированном
* поле — это чужое намерение. Принять его молча значит
* согласиться с тем, чего мы не поняли.
*
* unknown flags → отказ по той же причине.
*
* СОХРАНЕНИЕ РАСШИРЕНИЙ (S454)
* ────────────────────────────
* Если сторона получила структуру ДЛИННЕЕ своей и отдаёт её дальше, хвост
* обязан уехать без изменений. Инструмент, переписывающий артефакт, не имеет
* права терять поля, которых не понимает: потеря выглядит как решение автора.
*/
#define PLAT_DESC_MAX_SIZE (64u * 1024u)
typedef struct {
uint32_t size; /* sizeof у ЗАПОЛНИВШЕГО */
uint16_t version; /* смысл полей */
uint16_t flags; /* известные биты; неизвестные — отказ */
uint64_t reserved; /* обязан быть нулём */
} plat_desc_hdr_t;
typedef enum {
PLAT_DESC_OK = 0,
PLAT_DESC_TOO_SMALL = 1,
PLAT_DESC_NO_HEADER = 2, /* size == 0 */
PLAT_DESC_IMPOSSIBLE = 3, /* размер невозможен */
PLAT_DESC_BAD_VERSION = 4,
PLAT_DESC_RESERVED_SET= 5,
PLAT_DESC_UNKNOWN_FLAG= 6
} plat_desc_verdict_t;
/* own_size — sizeof структуры У ЧИТАТЕЛЯ; own_version — его версия;
* known_flags — маска битов, которые он понимает. */
plat_desc_verdict_t plat_desc_check(const plat_desc_hdr_t *h,
size_t own_size, uint16_t own_version,
uint16_t known_flags);
const char *plat_desc_verdict_name(plat_desc_verdict_t v);
/* Заполнить заголовок своей стороной. */
void plat_desc_init(plat_desc_hdr_t *h, size_t own_size, uint16_t version,
uint16_t flags);
/* Скопировать дескриптор, СОХРАНИВ хвост, которого мы не понимаем (S454).
* dst_cap — сколько байт доступно в приёмнике. Возвращает число скопированных
* байт или -errno. */
long plat_desc_copy_preserving(void *dst, size_t dst_cap,
const void *src, size_t own_size);
include/platx/diag.h
/* platx/diag.h — bootstrap log. Full log/ is a subscriber, not Core. */
typedef enum plat_diag_lvl {
PLAT_DIAG_DEBUG = 0,
PLAT_DIAG_INFO,
PLAT_DIAG_WARN,
PLAT_DIAG_ERROR
} plat_diag_lvl_t;
typedef struct plat_diag_api {
void (*write)(plat_diag_lvl_t lvl, const char *comp,
_Printf_format_string_ const char *fmt, ...);
void (*write)(plat_diag_lvl_t lvl, const char *comp, const char *fmt, ...)
__attribute__((format(printf, 3, 4)));
} plat_diag_api_t;
include/platx/diag_endpoint.h
/* platx/diag_endpoint.h — Authenticated read-only diagnostic endpoints (INT-116). */
#define PLAT_DIAG_PATH_MAX 64
#define PLAT_DIAG_BUF_MAX 4096
typedef enum {
PLAT_DIAG_OK = 0,
PLAT_DIAG_NOLINK = 1,
PLAT_DIAG_DENIED = 2,
PLAT_DIAG_ERROR = 3,
} plat_diag_status_t;
/* Read-only diagnostic handler: fills buf, returns bytes written or -1. */
typedef int (*plat_diag_handler_fn)(char *buf, size_t bufsz, void *ud);
/* Register a read-only diagnostic endpoint at path. 0/-1. */
int plat_diag_register(const char *path,
plat_diag_handler_fn fn, void *ud);
/* Read a diagnostic endpoint with capability auth. Returns bytes/-1. */
int plat_diag_read(const char *path, uint64_t ctx_id, uint32_t ctx_gen,
char *buf, size_t bufsz);
include/platx/enrich_provider.h
/* platx/enrich_provider.h — enrich_provider_v1 (D055): DEFERRED ABI.
*
* STATUS: contract only. There is no product implementation of this ABI in
* Wave 2, and adding one is a gate failure, not a feature
* (tests/cli/provider_enrich_deferred_gate.sh).
*
* Why it is written now and implemented later. Enrichment is the step where
* an observation acquires context it did not carry: a hash becomes a
* reputation, a path becomes a package, an address becomes an owner. Every one
* of those lookups is slower than the fact, may fail, may be attacker-visible
* and — this is the part that decides the design — is UNTRUSTED INPUT
* (Constitution C27). Enrichment that writes into the observation record
* silently promotes third-party text to evidence.
*
* So the contract is fixed here while it costs nothing:
* - enrichment never mutates an observation. It emits a separate record
* that references the observation by identity (sense_enrich_ref_t);
* - every enrichment carries `taint` and its source; a consumer that cannot
* handle tainted values must be able to drop it by looking at one field;
* - enrichment is asynchronous and droppable. Nothing in the decision path
* may block on it, and its absence is never an error — it is a field that
* is not there;
* - enrichment has its own budget and its own privacy lease. It is the one
* provider class that talks to things outside the host.
*
* ABI freeze: deferred. The declarations below may still change; anything
* depending on them must be marked deferred as well.
*/
/* The marker the gate greps for. Removing it does not enable the ABI; it just
* makes the gate red, which is the intended relationship. */
#define ENRICH_PROVIDER_ABI_DEFERRED 1
#define ENRICH_PROVIDER_ABI 0u /* 0 = not frozen, not implemented */
/* Taint classes (AI seam AI019): where a value came from decides what may be
* done with it, not how plausible it looks. */
typedef enum enrich_taint {
ENRICH_TAINT_NONE = 0, /* computed locally from trusted input */
ENRICH_TAINT_LOCAL = 1, /* local cache or database */
ENRICH_TAINT_REMOTE = 2, /* fetched from outside the host */
ENRICH_TAINT_ATTACKER = 3, /* derived from data the subject controls */
ENRICH_TAINT_MAX
} enrich_taint_t;
typedef struct enrich_request {
uint32_t abi_version;
uint32_t struct_size;
uint64_t observation_id_hi;
uint64_t observation_id_lo;
uint32_t enrichment_type;
uint32_t _pad;
prov_budget_t budget;
} enrich_request_t;
typedef struct enrich_result {
uint32_t abi_version;
uint32_t struct_size;
uint32_t taint; /* enrich_taint_t; never NONE for remote */
uint32_t data_len;
uint64_t produced_at_ns;
uint8_t data[512];
char source[PROV_REASON_MAX];
} enrich_result_t;
/* Deferred vtable. Declared so that dependants can be written against a stable
* shape; not registered anywhere, and no implementation exists. */
typedef struct enrich_provider_v1 {
uint32_t abi_version;
uint32_t struct_size;
void *ctx;
int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);
int (*enrich)(void *ctx, const enrich_request_t *req,
enrich_result_t *out, prov_status_t *st);
int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);
void *reserved[8];
} enrich_provider_v1_t;
include/platx/epoch.h
/* epoch.h — S424/S425: сроки переживают перезагрузку честно.
*
* ЗАДАЧА
* ──────
* Срок аренды, одноразовый токен и любой дедлайн считаются по МОНОТОННЫМ
* часам — они не скачут от NTP и `date -s`. Но монотонные часы обнуляются при
* перезагрузке: сохранённый дедлайн «через 300 секунд от загрузки» после
* перезагрузки означает совсем другой момент, а выглядит так же.
*
* Поэтому вместе с дедлайном сохраняется ЭПОХА ЗАГРУЗКИ. При чтении:
* эпоха совпала → дедлайн осмыслен, сравниваем;
* эпоха другая → дедлайн БЕССМЫСЛЕН, аренда считается истёкшей.
*
* Истёкшей, а не «ошибкой чтения» и не «ещё живой». Считать её живой значит
* продлевать доступ перезагрузкой; считать ошибкой — терять различие между
* «не знаю» и «сломано». Отказ выбран в сторону, которая не выдаёт прав.
*
* НАСТЕННЫЕ ЧАСЫ (S425)
* ─────────────────────
* Их сдвиг ФИКСИРУЕТСЯ как факт для оператора, но ни одно решение о сроке или
* доступе на них не опирается. Это разные вещи: заметить скачок полезно,
* принимать по нему решения — нет.
*/
#define PLAT_EPOCH_ID_MAX 64
typedef struct {
char id[PLAT_EPOCH_ID_MAX]; /* устойчивый идентификатор загрузки */
uint64_t mono_ns; /* CLOCK_MONOTONIC на момент снимка */
int64_t wall_s; /* CLOCK_REALTIME, только как факт */
} plat_epoch_t;
/* Снять текущую эпоху. 0 / -errno. */
int plat_epoch_now(plat_epoch_t *out);
typedef enum {
PLAT_LEASE_LIVE = 0,
PLAT_LEASE_EXPIRED = 1, /* срок вышел по монотонным часам */
PLAT_LEASE_REBOOTED = 2, /* другая эпоха: дедлайн бессмыслен */
PLAT_LEASE_MALFORMED= 3 /* запись нечитаема */
} plat_lease_state_t;
typedef struct {
char epoch_id[PLAT_EPOCH_ID_MAX];
uint64_t deadline_mono_ns;
uint32_t one_shot; /* 1 — токен сгорает при первом употреблении */
uint32_t used;
} plat_lease_t;
/* Выдать аренду на ttl_ms от текущего момента. */
int plat_lease_issue(plat_lease_t *out, uint64_t ttl_ms, int one_shot);
/* Состояние аренды ПРЯМО СЕЙЧАС. Не меняет её. */
plat_lease_state_t plat_lease_state(const plat_lease_t *l);
/* Употребить одноразовый токен. 0 — употреблён впервые; -EPERM — уже был или
* недействителен. Второе употребление обязано отказать даже в ту же секунду. */
int plat_lease_consume(plat_lease_t *l);
/* Сдвиг настенных часов между двумя снимками эпохи, в секундах.
* Возвращает 0, если эпохи разные (сравнивать нечего) — см. *comparable. */
int64_t plat_wall_drift(const plat_epoch_t *before, const plat_epoch_t *now,
int *comparable);
include/platx/event.h
/* platx/event.h — in-process fan-out. Not a bus, not JSON, not durable. */
#define PLAT_EV_RING_CAP 64u
#define PLAT_EV_PAY_MAX 256u
#define PLAT_EVENT_HAS_STATS 1
typedef uint32_t plat_event_id_t;
typedef uint64_t plat_sub_id_t;
typedef void (*plat_event_fn)(plat_event_id_t id, const void *payload,
size_t len, void *ud);
typedef struct plat_event_stats {
uint64_t emitted; /* accepted emit() calls, including overflow drops */
uint64_t dropped; /* overwrite of oldest + payload > PLAT_EV_PAY_MAX */
uint32_t queued; /* current ring occupancy */
uint32_t capacity; /* PLAT_EV_RING_CAP */
} plat_event_stats_t;
typedef struct plat_event_api {
int (*emit)(plat_event_id_t id, const void *payload, size_t len);
plat_sub_id_t (*subscribe)(plat_owner_t owner, plat_event_id_t id,
plat_event_fn fn, void *ud);
/* Returns after in-flight emit finishes (unless called from that emit). */
int (*unsubscribe)(plat_sub_id_t sub);
} plat_event_api_t;
/*
* emit copies into a bounded ring (no malloc). It never waits for a
* consumer. Full ring: drop oldest, increment dropped, keep the new fact.
* Callback sees a copy valid only for that call. It must not free or retain it.
*/
int plat_event_init(void);
void plat_event_fini(void);
int plat_event_emit(plat_event_id_t id, const void *payload, size_t len);
int plat_event_stats(plat_event_stats_t *out);
/* Snapshot last ids. Does not emit, pop, or change overflow. Newest last. */
int plat_event_recent(uint32_t *ids, uint32_t max);
/* Drop leftover subs on DESTROY. generation 0 is a no-op.
* Waits out a concurrent emit so ud can be freed after return. */
int plat_event_unsub_owner(plat_owner_t owner);
extern const plat_event_api_t plat_event_api;
/* Well-known core events. Domain modules define their own IDs ≥ 0x1000. */
#define PLAT_EV_MODULE_LOAD 1u
#define PLAT_EV_MODULE_START 2u
#define PLAT_EV_MODULE_STOP 3u
#define PLAT_EV_MODULE_FAIL 4u
#define PLAT_EV_RECOVERY 5u
#define PLAT_EV_SERVICE_REG 6u
#define PLAT_EV_SERVICE_REV 7u
#define PLAT_EV_BOOT 8u /* stage name after enter; "ready" only when platform_ready */
/* XIM: the parent answered for one mediated CHILD syscall. Payload is
* plat_xim_fact_t (platx/xim.h): ids and numbers, no pointers. A fact, not a
* command -- nothing downstream may turn it into start/stop/restart. */
/* Hook Engine. Payload is plat_hook_fact_t (platx/hook.h). These describe
* attach, detach and refusal -- lifecycle of the interposition itself. Per
* invocation facts deliberately do not live here: one event per read would
* drown a ring of 64 and starve every other subscriber. Counters and, later,
* a recorder carry those. */
#define PLAT_EV_HOOK_ATTACH 0x1200u
#define PLAT_EV_HOOK_DETACH 0x1201u
#define PLAT_EV_HOOK_ERROR 0x1202u
#define PLAT_EV_HOOK_POLICY 0x1203u /* a decision the runtime applied */
#define PLAT_EV_XIM_DECISION 0x1300u
/* Hades / eBPF (platx/hades_capsule.h). RARE lifecycle facts only — a profile
* committed, dropped, degraded. Payload <= 256 bytes, no pointers: capsule_id,
* profile_id, generation, epoch, quality, lost, reason. The dense per-hit
* observations (an exec, a connect, an open) never live here — they ride the
* Hades DOMAIN ring, exactly as Hook per-invocation facts stay off this ring.
* HADES_ATTACH fires when a profile is committed AFTER shadow verification, not
* after a bare BPF load. */
#define PLAT_EV_HADES_ATTACH 0x1500u /* profile committed after shadow */
#define PLAT_EV_HADES_DETACH 0x1501u /* cleanly removed */
#define PLAT_EV_HADES_ERROR 0x1502u /* load/attach/BTF/shadow refusal */
#define PLAT_EV_HADES_DEGRADED 0x1503u /* loss, adaptive transition, integrity mismatch */
#define PLAT_EV_HADES_DROPPED 0x1504u /* lost threshold crossed (not every drop) */
#define PLAT_EV_HADES_PROFILE 0x1505u /* apply/reload: new profile/epoch generation */
/* 0x1600..0x1601 are reserved for a retired laboratory subsystem. Do not reuse. */
include/platx/event_bound.h
/* platx/event_bound.h — A1-P05. Bounded plat_event: наблюдаемые ёмкости,
* ограниченный unsubscribe, барьер остановки, маркеры для аудита.
*
* Это расширение platx/event.h, а не второй event engine (C20). Кольцо,
* таблица подписчиков и диспетчер — те же самые, один TU src/core/plat_event.c.
* Здесь объявлено то, чего в старом контракте не было и из-за отсутствия чего
* нельзя было ни доказать границу, ни отличить потерю от тишины.
*
* Контракт волны 5 (A1-EVENT-IF). Стабильные точки для A2/A3/A4:
* - счётчик на каждую ёмкость (кольцо, таблица подписчиков, payload, вложенность);
* - токен подписки, привязанный к epoch подсистемы и к слоту таблицы;
* - unsubscribe с внешним deadline, не зависающий на чужом callback;
* - неблокирующий снимок счётчиков (не берёт лок диспетчера вовсе);
* - маркер seq/drop/restart, по которому аудит отличает gap от тишины;
* - owner эмитента в факте (ответ на A2 H12).
*/
/* ── Ёмкости. Каждая имеет отдельный счётчик исчерпания (A1-P05-201) ───── */
#define PLAT_EV_MAX_SUB 64
/* Бюджет вложенного emit. Раньше глубина ограничивалась только числом слотов
* (64 кадра ev_emit ≈ 80 KiB стека) и нигде не была объявлена. Теперь это
* явное число, и превышение считается, а не молчит. */
#define PLAT_EV_MAX_NEST 4
/* Сколько owner'ов одновременно могут держать STOPPING-барьер. */
#define PLAT_EV_MAX_BARRIER 16
/* Порог, после которого доставка одному подписчику объявляется медленной.
* Это ТОЛЬКО наблюдение: диспетчер не отписывает медленного, не перезапускает
* его и не меняет маршрут. Факт задержки передаётся, решение остаётся за
* Recovery (A1-P05-229). */
#define PLAT_EV_SLOW_NS (10ull * 1000000ull) /* 10 мс */
#define PLAT_EVENT_BOUND_ABI 1
/* ── Коды отказа. Отрицательные, не пересекаются с 0/-1 старого API ────── */
#define PLAT_EV_OK 0
#define PLAT_EV_E_ARGS (-2) /* NULL/нулевой id/generation 0 */
#define PLAT_EV_E_TOOBIG (-3) /* payload > PLAT_EV_PAY_MAX (A1-P05-219) */
#define PLAT_EV_E_NOT_READY (-4) /* подсистема не инициализирована */
#define PLAT_EV_E_FULL (-5) /* таблица подписчиков исчерпана */
#define PLAT_EV_E_BARRIER (-6) /* owner в STOPPING (A1-P05-215) */
#define PLAT_EV_E_STALE (-7) /* токен не из этого epoch/слота */
#define PLAT_EV_E_TIMEOUT (-8) /* deadline истёк, detach сохранён */
#define PLAT_EV_E_BUSY (-9) /* снимок не удался без ожидания */
#define PLAT_EV_E_SEQ_EXHAUST (-10) /* пространство токенов исчерпано */
#define PLAT_EV_E_NEST (-11) /* превышен бюджет вложенности */
#define PLAT_EV_E_VERSION (-12) /* неизвестная обязательная версия факта */
/* ── Раскладка токена подписки (A1-P05-208) ────────────────────────────────
*
* Старый токен был счётчиком, обнулявшимся в plat_event_init(). После
* fini/init он выдавался заново с 1, и токен прошлого поколения успешно
* отписывал нового подписчика другого owner. Теперь токен несёт epoch
* подсистемы и индекс слота, поэтому ни перезапуск, ни переиспользование
* слота не делают старый токен снова валидным.
*
* биты 63..48 epoch — инкремент на каждый plat_event_init(), не сбрасывается
* биты 47..40 slot — индекс в таблице, прямой поиск без сканирования
* биты 39..0 seq — монотонный внутри epoch, никогда не 0
*/
#define PLAT_EV_TOK_EPOCH_SHIFT 48
#define PLAT_EV_TOK_SLOT_SHIFT 40
#define PLAT_EV_TOK_SEQ_MASK ((uint64_t)0xFFFFFFFFFFull)
#define PLAT_EV_TOK_SLOT_MASK ((uint64_t)0xFFull)
#define PLAT_EV_TOK_EPOCH_MASK ((uint64_t)0xFFFFull)
/* Предел выдачи токенов внутри epoch. По умолчанию — вся разрядность seq.
* Отдельная константа, а не сама маска: тестовая сборка уменьшает предел,
* не трогая раскладку токена, поэтому исчерпание проверяется на настоящей
* реализации, а не на её изменённой копии (A1-P05-245). */
#define PLAT_EV_SUB_SEQ_LIMIT PLAT_EV_TOK_SEQ_MASK
uint32_t plat_ev_tok_epoch(plat_sub_id_t t); /* inline interface */
uint32_t plat_ev_tok_slot(plat_sub_id_t t); /* inline interface */
uint64_t plat_ev_tok_seq(plat_sub_id_t t); /* inline interface */
/* ── Состояние подписки: логический detach отделён от освобождения ─────────
* (A1-P05-213). DETACHED слот больше не выбирается новыми emit, но его
* callback storage жив, пока держится хоть одна inflight-ссылка. */
typedef enum plat_ev_sub_state {
PLAT_EV_SUB_FREE = 0,
PLAT_EV_SUB_LIVE = 1,
PLAT_EV_SUB_DETACHED = 2
} plat_ev_sub_state_t;
typedef struct plat_ev_sub_info {
plat_ev_sub_state_t state;
int inflight; /* удерживаемые снимки этого слота */
plat_owner_t owner;
plat_event_id_t ev;
} plat_ev_sub_info_t;
/* ── Расширенные счётчики (A1-P05-201/222/226/230) ─────────────────────────
*
* Читаются без лока диспетчера: все поля — атомики, снимок не блокируется
* зависшим callback'ом. Поэтому снимок не мгновенный срез одного момента;
* инварианты, которые обязаны держаться, перечислены в комментариях. */
typedef struct plat_event_stats2 {
uint32_t version; /* PLAT_EVENT_BOUND_ABI */
uint32_t epoch; /* поколение подсистемы (init) */
uint64_t restarts; /* сколько раз выполнялся init после fini */
/* Кольцо фактов. */
uint64_t emitted; /* принятые факты (без отказов) */
uint64_t seq_last; /* seq последнего принятого факта */
uint64_t seq_first_live; /* seq самого старого живого слота кольца */
uint32_t queued; /* занятость кольца, <= ring_capacity всегда */
uint32_t ring_capacity; /* PLAT_EV_RING_CAP */
uint64_t dropped_ring; /* вытеснено переполнением кольца */
/* Отказы до резервирования — кольцо не тронуто. */
uint64_t rejected_oversize; /* payload > PLAT_EV_PAY_MAX */
uint64_t rejected_args; /* id==0 / payload==NULL при len>0 */
uint64_t rejected_not_ready;
/* Доставка. Факт принят, но конкретный подписчик его не увидел. */
uint64_t undelivered_reentry;/* слот уже в стеке этого потока */
uint64_t undelivered_nest; /* исчерпан PLAT_EV_MAX_NEST */
uint64_t undelivered_detach; /* слот отсоединён между снимком и вызовом */
uint32_t nest_depth_max; /* наибольшая наблюдённая глубина */
/* Таблица подписчиков. */
uint32_t sub_active; /* LIVE слоты */
uint32_t sub_detached; /* DETACHED, ещё не освобождённые */
uint32_t sub_capacity; /* PLAT_EV_MAX_SUB */
uint64_t sub_reject_full; /* отказ из-за исчерпания таблицы */
uint64_t sub_reject_args; /* generation 0, NULL fn, id 0 */
uint64_t sub_reject_barrier; /* owner в STOPPING */
uint64_t sub_reject_seq; /* исчерпано пространство токенов */
/* Ожидания. */
uint64_t unsub_timeouts; /* deadline истёк, detach сохранён */
uint32_t barriers_active;
/* Медленный подписчик (A1-P05-229). Факт, не действие: ни одна из этих
* величин не приводит к автоматической отписке или перезапуску. */
uint64_t slow_dispatches; /* доставок дольше PLAT_EV_SLOW_NS */
uint64_t slow_ns_max; /* наибольшая наблюдённая доставка, нс */
uint32_t slow_slot_last; /* слот последней медленной доставки */
uint32_t slow_threshold_ms; /* объявленный порог, чтобы читатель знал его */
} plat_event_stats2_t;
/* Неблокирующий снимок. Не берёт лок диспетчера; возвращает PLAT_EV_E_ARGS
* при out==NULL и PLAT_EV_E_NOT_READY до init. Никогда не PLAT_EV_E_BUSY —
* оставлено в кодах для реализаций, которым понадобится trylock. */
int plat_event_stats2(plat_event_stats2_t *out);
/* ── Маркер для аудита (A1-P05-231) ────────────────────────────────────────
* Позволяет отличить «событий не было» от «события были и потеряны».
* Правило для потребителя: если seq_first_live > last_seen_seq + 1, между
* ними подтверждённый gap ровно (seq_first_live - last_seen_seq - 1) фактов.
* Если epoch отличается от прошлого чтения — был restart, и непрерывность
* seq через границу epoch не заявляется. */
typedef struct plat_event_marker {
uint32_t version;
uint32_t epoch;
uint64_t restarts;
uint64_t seq_last;
uint64_t seq_first_live;
uint64_t dropped_total; /* dropped_ring + rejected_oversize */
} plat_event_marker_t;
int plat_event_marker(plat_event_marker_t *out);
/* ── Owner эмитента в факте — ответ на A2 H12 (A1-P05-232) ────────────────
* Старый plat_event_fn не отдавал owner того, кто выпустил факт, поэтому мост
* plat_event→mbus мог проставить только свой. Расширенный callback получает
* метаданные; флаг отличает «эмитент неизвестен» от «эмитент — owner 0/0/0».
* Обратный мост из этого контракта не включается: emit_owner ничего не
* подписывает и никуда не публикует. */
#define PLAT_EVENT_META_V1 1u
#define PLAT_EV_F_EMITTER_KNOWN 0x1u
typedef struct plat_event_meta {
uint32_t version; /* PLAT_EVENT_META_V1 */
uint32_t flags;
plat_event_id_t id;
uint32_t epoch;
uint64_t seq;
plat_owner_t emitter; /* валиден только при PLAT_EV_F_EMITTER_KNOWN */
} plat_event_meta_t;
typedef void (*plat_event_fn2)(const plat_event_meta_t *meta, const void *payload,
size_t len, void *ud);
plat_sub_id_t plat_event_subscribe_ex(plat_owner_t owner, plat_event_id_t id,
plat_event_fn2 fn, void *ud);
int plat_event_emit_owner(plat_owner_t emitter, plat_event_id_t id,
const void *payload, size_t len);
/* ── Ограниченный по времени unsubscribe (A1-P05-214) ──────────────────────
* timeout_ns == 0 — только попытка без ожидания. При истечении возвращает
* PLAT_EV_E_TIMEOUT, слот остаётся DETACHED: новые emit его не выбирают,
* доставка прекращена, освобождение произойдёт, когда уйдёт последняя
* inflight-ссылка. Состояние достоверно и читается plat_event_sub_state(). */
int plat_event_unsub_deadline(plat_sub_id_t sub, uint64_t timeout_ns);
int plat_event_unsub_owner_deadline(plat_owner_t owner, uint64_t timeout_ns,
int *detached_out);
int plat_event_sub_state(plat_sub_id_t sub, plat_ev_sub_info_t *out);
/* ── Барьер остановки owner (A1-P05-215) ──────────────────────────────────
* После begin новые подписки этого owner отвергаются с PLAT_EV_E_BARRIER,
* поэтому подписка либо уже учтена в drain, либо не создана. begin не
* отписывает существующие: это делает unsub_owner_deadline. */
int plat_event_barrier_begin(plat_owner_t owner);
int plat_event_barrier_end(plat_owner_t owner);
int plat_event_barrier_active(plat_owner_t owner);
/* ── Типизированный payload и его версия (A1-P05-218/220) ─────────────────
* Заголовок факта. Диспетчер его не интерпретирует — он копирует байты.
* Проверка версии живёт в consumer adapter, и неизвестная обязательная
* версия обязана стать отказом, а не «декодируем как сможем». */
typedef struct plat_event_fact_hdr {
uint16_t version;
uint16_t body_len;
} plat_event_fact_hdr_t;
/* Возвращает PLAT_EV_OK, PLAT_EV_E_ARGS или PLAT_EV_E_VERSION. */
int plat_event_fact_accept(const void *payload, size_t len,
uint16_t min_ver, uint16_t max_ver,
plat_event_fact_hdr_t *hdr_out);
include/platx/fleet_plan.h
/* platx/fleet_plan.h -- immutable Central/Node planning and config CAS.
*
* This layer contains no transport and performs no node-side effect. It is
* the stable boundary shared by a central coordinator, mesh fan-out, DSL/MSX
* dry-run and the evidence writer. Execution adapters may consume a plan,
* but cannot make an incomplete plan look globally successful. */
#define PLAT_FLEET_PLAN_VERSION 1u
#define PLAT_FLEET_MAX_NODES 64u
#define PLAT_FLEET_ID_MAX 64u
#define PLAT_FLEET_REASON_MAX 160u
typedef enum {
PLAT_FLEET_NODE_PLANNED = 1,
PLAT_FLEET_NODE_APPLIED,
PLAT_FLEET_NODE_REFUSED,
PLAT_FLEET_NODE_FAILED,
PLAT_FLEET_NODE_UNAVAILABLE,
PLAT_FLEET_NODE_EXPIRED,
PLAT_FLEET_NODE_UNCOMPENSATED
} plat_fleet_node_state_t;
typedef enum {
PLAT_FLEET_CAS_APPLIED = 0,
PLAT_FLEET_CAS_IDEMPOTENT = 1,
PLAT_FLEET_CAS_STALE = -1,
PLAT_FLEET_CAS_CONFLICT = -2,
PLAT_FLEET_CAS_INVALID = -3
} plat_fleet_cas_result_t;
typedef enum {
PLAT_FLEET_GLOBAL_SUCCESS = 0,
PLAT_FLEET_GLOBAL_PARTIAL_FAILURE = 1,
PLAT_FLEET_GLOBAL_INVALID = 2
} plat_fleet_global_result_t;
typedef struct {
uint64_t generation;
uint8_t digest[32];
char last_request_id[PLAT_FLEET_ID_MAX];
uint64_t last_base_generation;
uint64_t last_desired_generation;
uint8_t last_desired_digest[32];
} plat_fleet_config_state_t;
typedef struct {
const char *request_id;
uint64_t base_generation;
uint64_t desired_generation;
uint8_t desired_digest[32];
} plat_fleet_config_patch_t;
typedef struct {
char node_id[PLAT_FLEET_ID_MAX];
plat_fleet_node_state_t state;
char action[PLAT_FLEET_ID_MAX];
char reason[PLAT_FLEET_REASON_MAX];
} plat_fleet_node_plan_t;
typedef struct {
uint32_t version;
char plan_id[PLAT_FLEET_ID_MAX];
uint64_t config_generation;
uint64_t expires_at_ms;
int dry_run;
size_t node_count;
plat_fleet_node_plan_t nodes[PLAT_FLEET_MAX_NODES];
} plat_fleet_plan_t;
plat_fleet_cas_result_t plat_fleet_config_cas(
plat_fleet_config_state_t *state,
const plat_fleet_config_patch_t *patch);
int plat_fleet_plan_init(plat_fleet_plan_t *plan, const char *plan_id,
uint64_t config_generation, uint64_t expires_at_ms,
int dry_run);
int plat_fleet_plan_add_node(plat_fleet_plan_t *plan, const char *node_id,
const char *action, plat_fleet_node_state_t state,
const char *reason);
plat_fleet_global_result_t plat_fleet_plan_result(
const plat_fleet_plan_t *plan, uint64_t now_ms);
/* Deterministic JSON evidence. Refuses truncation and invalid plans. */
int plat_fleet_plan_evidence_json(const plat_fleet_plan_t *plan,
uint64_t now_ms, char *out, size_t out_size);
const char *plat_fleet_node_state_name(plat_fleet_node_state_t state);
include/platx/gencommit.h
/* gencommit.h — S410/S411/S435/S443: две генерации, третьей не бывает.
*
* Договор: на диске в любой момент лежит ЛИБО целиком прежняя генерация, ЛИБО
* целиком новая. Смешанной не бывает никогда — ни при отказе проверки, ни при
* смерти процесса на любом шаге.
*
* Порядок:
* 1. кандидат пишется в <base>.next атомарной заменой;
* 2. кандидат ПЕРЕЧИТЫВАЕТСЯ С ДИСКА и проверяется;
* 3. годный — атомарно занимает <base>.current;
* 4. негодный — остаётся под <base>.rejected вместе с <base>.rejected.why.
*
* Шаг 2 читает с диска, а не проверяет буфер в памяти: проверять надо то, что
* действительно записалось. Буфер мог быть верным, а записанное — нет, и
* различие видно только так.
*
* Отвергнутая генерация НЕ УДАЛЯЕТСЯ (S443). Она — единственное объяснение
* того, почему система работает на прежней конфигурации, и стирать её значит
* оставить оператора без ответа на первый же вопрос.
*/
struct xio_t;
/* Проверка кандидата. 0 — годен. Отрицательное — негоден; why заполняется
* причиной для оператора. */
typedef int (*plat_gen_validate_fn)(const void *data, size_t len,
char *why, size_t why_sz, void *ud);
typedef enum {
PLAT_GEN_COMMITTED = 0, /* новая генерация стала текущей */
PLAT_GEN_REJECTED = 1 /* текущая не тронута, кандидат отложен */
} plat_gen_result_t;
/* 0 / -errno. Что произошло — в *res. */
int plat_gen_commit(const struct xio_t *io, const char *base,
const void *data, size_t len,
plat_gen_validate_fn validate, void *ud,
plat_gen_result_t *res);
/* Прочитать текущую генерацию. Возвращает длину, либо -errno.
* -ENOENT означает «генерации ещё нет» — это ответ, а не сбой. */
long plat_gen_load(const struct xio_t *io, const char *base,
void *buf, size_t cap);
/* Причина последнего отказа, если она есть. Длина или -errno. */
long plat_gen_why(const struct xio_t *io, const char *base,
char *buf, size_t cap);
include/platx/host.h
/* platx/host.h — one descriptor, two execution hosts.
*
* EMBEDDED: module shares the worker address space. plat_abi_t* is real.
* CHILD: module is a separate process. It receives a virtual plat_abi
* whose functions serialize to the worker. provides[] are
* registered in the worker as proxy vtables, never as child
* function pointers.
*
* Isolation is chosen by the deployment profile from
* isolation_preference / isolation_allowed. It is not a module type.
*
* Stub never launches children. Core child_host does:
* exec + sandbox profile (seccomp, rlimits, namespaces, no_new_privs).
*/
/* CHILD spawn is plat_child_host_* in child_host.h — Core, not stub. */
typedef enum plat_host_kind {
PLAT_HOST_EMBEDDED = 0,
PLAT_HOST_CHILD = 1
} plat_host_kind_t;
include/platx/incident_env.h
/* platx/incident_env.h — INT-145: incident result envelope.
*
* Every output from MSX/DSL/Fabric/Hades is wrapped in a typed envelope
* before it is stored or forwarded. The envelope adds provenance, timestamps,
* and a correlation chain so a central node can link inputs to outputs.
*/
#define PLAT_ENV_SOURCE_MAX 32
#define PLAT_ENV_KIND_MAX 32
#define PLAT_ENV_CORR_MAX 64
#define PLAT_ENV_REASON_MAX 256
#define PLAT_ENV_BODY_MAX 2048
typedef struct plat_incident_env {
char source[PLAT_ENV_SOURCE_MAX]; /* subsystem producing this */
char kind[PLAT_ENV_KIND_MAX]; /* event type label */
char correlation_id[PLAT_ENV_CORR_MAX]; /* chain: req→resp→audit */
uint64_t issued_at_ms; /* monotonic ms */
uint64_t context_id; /* plat_context handle.id (0 if none) */
uint64_t context_gen; /* plat_context handle.generation */
uint64_t lease_id; /* lease used (0 if none) */
char body[PLAT_ENV_BODY_MAX]; /* free-form JSON or text payload */
char reason[PLAT_ENV_REASON_MAX]; /* human-readable outcome */
int success; /* 0 = failure, 1 = success */
} plat_incident_env_t;
void plat_env_init(plat_incident_env_t *env,
const char *source, const char *kind,
const char *correlation_id);
void plat_env_set_context(plat_incident_env_t *env,
uint64_t ctx_id, uint64_t ctx_gen);
void plat_env_set_lease(plat_incident_env_t *env, uint64_t lease_id);
void plat_env_set_body(plat_incident_env_t *env, const char *body);
void plat_env_finish(plat_incident_env_t *env, int success, const char *reason);
/* Emit the envelope to audit. No-op if audit_write not linked. */
void plat_env_audit(const plat_incident_env_t *env);
include/platx/jrec.h
/* jrec.h — S459/S460/S490: версия схемы у записи-строки и сохранение чужих полей.
*
* ЧТО ЗДЕСЬ РЕШАЕТСЯ
* ──────────────────
* Аудит, доказательства и трассы пишутся построчно как JSON-объекты. Такой
* файл переживает выпуск, который его написал: его читает следующая версия,
* его переписывает сторонний инструмент, его несут в другую систему. Значит
* это ФОРМАТ, а не «просто лог», и у него обязаны быть три свойства.
*
* S459 У записи есть номер схемы, и для КАЖДОЙ поддерживаемой версии есть
* читатель, доказанный на замороженном образце. «Мы всё ещё умеем
* читать старое» без образца — это надежда, а не совместимость.
* Строка БЕЗ поля "schema" — это схема 1: так писал первый выпуск, и
* переписать историю задним числом нельзя.
*
* S460 Инструмент, переписывающий запись, обязан отдать НЕИЗВЕСТНЫЕ ему поля
* дословно. Потеря поля выглядит как решение автора: читатель не может
* отличить «этого не было» от «это выкинули по дороге». Поэтому чужие
* поля едут как сырой текст, в исходном порядке.
*
* S490 Читаем старое — пишем ТОЛЬКО текущее. Запись, отданная наружу, всегда
* объявляет текущую схему: иначе в файле окажется смесь, о которой никто
* не договаривался. И осмотр — только чтение: разбор записи не меняет ни
* байта на диске.
*
* ЧЕГО ЗДЕСЬ НЕТ, И ЭТО НАМЕРЕННО
* ───────────────────────────────
* Это не парсер JSON. Здесь режется ОДНА строка ОДНОГО объекта на пары
* ключ/значение, и значение не разбирается вовсе — оно едет как текст. Полный
* разбор не нужен для того, чтобы ничего не потерять, а лишний разбор — это
* лишний способ потерять.
*/
#define PLAT_JREC_MAX_LINE 4096
#define PLAT_JREC_MAX_FIELDS 48
typedef enum {
PLAT_JREC_AUDIT = 0,
PLAT_JREC_EVIDENCE = 1,
PLAT_JREC_TRACE = 2,
PLAT_JREC_KIND_N = 3
} plat_jrec_kind_t;
typedef enum {
PLAT_JREC_OK = 0,
PLAT_JREC_MALFORMED = 1,
PLAT_JREC_TOO_NEW = 2, /* схема новее текущей — отказ, не «как сможем» */
PLAT_JREC_TOO_OLD = 3, /* схема старше поддерживаемой */
PLAT_JREC_OVERFLOW = 4,
PLAT_JREC_BAD_ARG = 5
} plat_jrec_verdict_t;
typedef struct {
uint16_t koff, klen; /* смещения в jrec.buf: ключ без кавычек */
uint16_t voff, vlen; /* сырой текст значения, как в исходнике */
uint8_t known; /* 1 — поле известно ТЕКУЩЕЙ схеме */
} plat_jrec_field_t;
typedef struct {
char buf[PLAT_JREC_MAX_LINE];
size_t len;
plat_jrec_field_t f[PLAT_JREC_MAX_FIELDS];
int n;
uint32_t schema; /* 1, если поля "schema" не было */
int had_schema;
plat_jrec_kind_t kind;
} plat_jrec_t;
/* Границы поддержки. oldest — самая старая схема, для которой ЕСТЬ читатель. */
uint32_t plat_jrec_schema_current(plat_jrec_kind_t k);
uint32_t plat_jrec_schema_oldest (plat_jrec_kind_t k);
/* Разбор. Вход — const: осмотр ничего не меняет (S490). */
plat_jrec_verdict_t plat_jrec_parse(plat_jrec_kind_t kind,
const char *line, plat_jrec_t *out);
/* Значение по ключу как сырой текст. NULL — поля нет. */
const char *plat_jrec_get(const plat_jrec_t *r, const char *key, size_t *len);
/* Сколько полей запись несёт сверх известных текущей схеме. */
int plat_jrec_unknown_count(const plat_jrec_t *r);
/* Заменить/добавить ЗНАЧЕНИЕ известного поля. rawval — готовый JSON-текст
* (со своими кавычками, если это строка). Неизвестные поля менять нельзя:
* не понимаешь смысл — не трогай. Возвращает 0/-1. */
int plat_jrec_set(plat_jrec_t *r, const char *key, const char *rawval);
/* Собрать строку обратно. Схема ВСЕГДА текущая (S490). Порядок полей
* сохраняется; неизвестные поля выводятся дословно (S460). Без '\n'. */
plat_jrec_verdict_t plat_jrec_emit(const plat_jrec_t *r, char *out, size_t cap);
const char *plat_jrec_verdict_name(plat_jrec_verdict_t v);
include/platx/kernel.h
/* platx/kernel.h — static boot core that memfd links (libplatx.a).
*
* One process, one binary. This header is the boundary, not a second OS.
*
* IN (libplatx.a / PLATX_KERNEL_SRCS — boot contract):
* staged boot 0..5, register + layer init/start, fail-closed ready,
* Core ABI, lifecycle + recovery decide/apply, keyring v1 ABI
*
* OUT of kernel (linked product — not libplatx.a, do not implement here):
* supervisor, RA2C, POE
* xio, log, taskmgr (KERNEL stage calls them; they are not the archive)
* PIC is archive — not a kernel layer
* DRM, plugin, vfs, mbus, mesh, script, audit
* src/transport / src/transports, keyring store/daemon
* CHILD host / IPC / abi_proxy; Event-as-bus, extra transports
*
* platx-core / platx-alpha are test bins. They are not a microkernel.
*/
/* --------------------------------------------------------------------------
* Process boot stages — the one path.
*
* Why this axis exists: platform_ready() must mean "every mandatory
* predecessor finished", not "something registered". A later stage
* must not run after a mandatory fail (no half-ready: ACK without
* channel, plugin without verify, scripts on a dead core).
*
* This is NOT a second state machine:
* plat_lifecycle — per-module LOAD/CREATE/START (recovery requests).
* plat_layer — init/start waves of already-registered subsys
* (system → crypto → net → observe → automation,
* then payload). subsys_boot / subsys_start_all
* already walk those waves. Do not add another loop.
*
* Map, do not duplicate:
* KERNEL owns register + the existing layer init/start of core
* subsys (system..automation).
* MODULES owns PLAT_LAYER_PAYLOAD (PIC is archive, not kernel).
* CONFIG / MANIFEST / SCRIPTS / READY are process gates, not layers.
*
* Walk 0→5 in order. Optional work is entered then skipped, not jumped.
* Mandatory fail at N: stay at N, do not enter N+1, platform_ready stays 0.
* platform_ready() is true only after READY.
* -------------------------------------------------------------------------- */
typedef enum platx_boot_stage {
PLATX_STAGE_KERNEL = 0, /* register + Core ABI; calls linked xio/log/taskmgr */
PLATX_STAGE_CONFIG = 1, /* presets / platx.conf; kv+env before DSL */
PLATX_STAGE_MANIFEST = 2, /* DSL: what is mandatory vs optional */
PLATX_STAGE_MODULES = 3, /* ELF-load vfs PIC; fail closed on unwrap/bad sig */
PLATX_STAGE_SCRIPTS = 4, /* MSX after modules exist to serve it */
PLATX_STAGE_READY = 5 /* only here is the process "up" */
} platx_boot_stage_t;
#define PLATX_STAGE_COUNT 6
#define PLATX_STAGE_LAST PLATX_STAGE_READY
const char *platx_boot_stage_name(platx_boot_stage_t s); /* inline interface */
/* Fail-closed: only the next ordinal. READY is terminal. Out-of-range
* or a jump (skipping a stage that must be entered) is rejected. */
int platx_boot_stage_may_enter(platx_boot_stage_t from,
platx_boot_stage_t to); /* inline interface */
include/platx/keyrot.h
/* keyrot.h — S480: смена ключей между узлами РАЗНЫХ версий.
*
* Смена ключа выглядит простой ровно до того момента, когда собеседники
* умеют разное. Тогда появляются три разные ошибки, и все три тихие.
*
* 1. ВЫДАТЬ КЛЮЧ АЛГОРИТМА, КОТОРОГО У СОБЕСЕДНИКА НЕТ.
* Он не сможет им воспользоваться — и, если код «на всякий случай»
* откатится на что-нибудь общее, связь останется, а защита станет той,
* о которой никто не договаривался. Поэтому алгоритм нового ключа берётся
* только из ПЕРЕСЕЧЕНИЯ политик, и пустое пересечение — отказ, а не
* подстановка (см. plat_cryptopol_negotiate).
*
* 2. ПРОДОЛЖИТЬ ПОЛЬЗОВАТЬСЯ КЛЮЧОМ, КОТОРЫЙ БЫЛ ЗАКОННЫМ ВЧЕРА.
* Собеседник сузил политику — убрал алгоритм. Ключ, выданный до этого,
* формально исправен, но алгоритм теперь отсутствует у одной из сторон.
* «Отсутствует хотя бы у одной» проверяется В МОМЕНТ ИСПОЛЬЗОВАНИЯ, а не
* только при выдаче: политика — это то, что действует сейчас.
*
* 3. ПЕРЕПУТАТЬ ОКНО ПЕРЕКРЫТИЯ С РАЗРЕШЕНИЕМ.
* Во время смены обе стороны какое-то время принимают и старое поколение.
* Принимать — да. ПОДПИСЫВАТЬ старым — нет: иначе смена ключа не
* заканчивается никогда, и достаточно продержать окно открытым, чтобы она
* не состоялась вовсе.
*/
typedef struct {
uint32_t policy; /* маска PLAT_CP_* — что узел умеет СЕЙЧАС */
uint16_t wire; /* версия обрамления */
uint64_t gen; /* поколение ключа, известное узлу */
} plat_peer_t;
typedef struct {
uint32_t alg; /* ОДИН бит PLAT_CP_* */
uint64_t gen;
int valid;
} plat_key_t;
typedef enum {
PLAT_KEYROT_OK = 0,
PLAT_KEYROT_NO_COMMON = 1, /* пустое пересечение политик */
PLAT_KEYROT_ALG_GONE = 2, /* алгоритм пропал у одной из сторон */
PLAT_KEYROT_STALE_GEN = 3, /* поколение старше принимаемого */
PLAT_KEYROT_NOT_FOR_NEW = 4, /* старым поколением можно ПРИНИМАТЬ, не подписывать */
PLAT_KEYROT_BAD_ARG = 5
} plat_keyrot_verdict_t;
/* Выбрать алгоритм нового ключа и выдать его. Алгоритм — сильнейший из
* пересечения по ЗАМОРОЖЕННОМУ порядку предпочтения (детерминированно).
* При отказе *out не трогается. */
plat_keyrot_verdict_t plat_keyrot_issue(const plat_peer_t *local,
const plat_peer_t *remote,
plat_key_t *out);
/* Можно ли ПОДПИСЫВАТЬ этим ключом прямо сейчас. */
plat_keyrot_verdict_t plat_keyrot_may_sign(const plat_key_t *k,
const plat_peer_t *local,
const plat_peer_t *remote);
/* Можно ли ПРИНЯТЬ этим ключом. Окно перекрытия шире на overlap поколений
* назад, но алгоритм обязан присутствовать у обеих сторон и здесь. */
plat_keyrot_verdict_t plat_keyrot_may_accept(const plat_key_t *k,
const plat_peer_t *local,
const plat_peer_t *remote,
uint64_t overlap);
const char *plat_keyrot_verdict_name(plat_keyrot_verdict_t v);
include/platx/keystate.h
/* keystate.h — S437/S439: ключи и одноразовые значения переживают перезапуск.
*
* S437 Отсутствующий, отозванный или испорченный ключ хранения — это отказ
* ЗАКРЫТЫМ, а не повод завести новый. Автоматическая переинициализация
* выглядит как выздоровление, а на деле осиротит всё уже запечатанное:
* данные останутся на диске нечитаемыми навсегда, и никто не заметит
* момента, когда это случилось.
*
* S439 Одноразовое значение не имеет права повториться ни после чистого
* перезапуска, ни после краха, ни после сорванной контрольной точки.
* Поэтому на диск пишется не «последнее использованное», а ГРАНИЦА
* ВЫДАННОГО: блок резервируется долговечно ДО того, как из него выдадут
* хоть одно значение. Крах теряет остаток блока — и это правильный
* размен: потерянные номера ничего не стоят, повторённые стоят всего.
*/
struct xio_t;
typedef enum {
PLAT_KEY_OK = 0,
PLAT_KEY_MISSING = 1,
PLAT_KEY_REVOKED = 2,
PLAT_KEY_CORRUPT = 3
} plat_key_state_t;
/* Состояние ключа хранения. НИЧЕГО не создаёт и не чинит. */
plat_key_state_t plat_key_check(const struct xio_t *io, const char *path);
/* Открыть ключ для работы. Любое состояние кроме OK — отказ (-EKEYREJECTED
* / -ENOKEY / -EBADMSG), и ключ НЕ создаётся. */
int plat_key_open(const struct xio_t *io, const char *path,
uint8_t out[32], plat_key_state_t *why);
/* ── одноразовые значения ────────────────────────────────────────────────── */
typedef struct {
const struct xio_t *io;
char path[256];
uint64_t next; /* следующее к выдаче */
uint64_t reserved; /* граница, записанная на диск */
uint32_t block; /* размер резервируемого блока */
} plat_nonce_t;
int plat_nonce_open(plat_nonce_t *n, const struct xio_t *io,
const char *path, uint32_t block);
/* Выдать следующее значение. 0 / -errno. */
int plat_nonce_next(plat_nonce_t *n, uint64_t *out);
include/platx/lifecycle.h
/* platx/lifecycle.h — formal module state machine.
* All transitions go through the lifecycle manager. Recovery only requests.
*/
typedef enum plat_mod_state {
PLAT_ST_DISCOVERED = 0,
PLAT_ST_LOADED,
PLAT_ST_CREATED,
PLAT_ST_STARTING,
PLAT_ST_RUNNING,
PLAT_ST_DEGRADED,
PLAT_ST_QUIESCING,
PLAT_ST_STOPPING,
PLAT_ST_STOPPED,
PLAT_ST_DESTROYED,
PLAT_ST_FAILED
} plat_mod_state_t;
typedef enum plat_health_code {
PLAT_HEALTH_OK = 0,
PLAT_HEALTH_DEGRADED = -1,
PLAT_HEALTH_FATAL = -99
} plat_health_code_t;
typedef struct plat_health {
plat_health_code_t code;
char reason[96];
} plat_health_t;
/* Recovery / supervisor name an action. Only the manager walks the SM
* and calls descriptor hooks. RESTART is a sequence inside the manager,
* not a license for recovery to call stop/start itself. */
typedef enum plat_lifecycle_action {
PLAT_LC_LOAD = 1,
PLAT_LC_CREATE,
PLAT_LC_START,
PLAT_LC_QUIESCE,
PLAT_LC_STOP,
PLAT_LC_DESTROY,
PLAT_LC_FAIL,
PLAT_LC_RESTART,
PLAT_LC_DEGRADE
} plat_lifecycle_action_t;
struct plat_module_descriptor;
struct plat_create_args;
typedef struct plat_lifecycle {
plat_mod_state_t state;
const struct plat_module_descriptor *desc;
void *instance;
const struct plat_create_args *create_args;
} plat_lifecycle_t;
/* Valid edges (anything else is rejected by the manager):
* DISCOVERED → LOADED | FAILED
* LOADED → CREATED | DESTROYED | FAILED
* CREATED → STARTING | DESTROYED | FAILED
* STARTING → RUNNING | DEGRADED | FAILED
* RUNNING → DEGRADED | QUIESCING | STOPPING | FAILED
* DEGRADED → RUNNING | QUIESCING | STOPPING | FAILED
* QUIESCING → STOPPING | FAILED
* STOPPING → STOPPED | FAILED
* STOPPED → STARTING | DESTROYED | FAILED
* FAILED → DESTROYED | STOPPED (never FAILED → RUNNING)
* DESTROYED — terminal
*/
int plat_state_can_enter(plat_mod_state_t from, plat_mod_state_t to);
const char *plat_state_name(plat_mod_state_t s);
int plat_lifecycle_init(plat_lifecycle_t *lm);
int plat_lifecycle_bind(plat_lifecycle_t *lm,
const struct plat_module_descriptor *desc,
const struct plat_create_args *args);
/* Adopt a live instance. No hooks. Further changes go through request(). */
int plat_lifecycle_attach(plat_lifecycle_t *lm,
const struct plat_module_descriptor *desc,
void *instance, plat_mod_state_t state);
/* Illegal edge: reject, state unchanged.
* Hook already ran and cannot undo: FAILED (not a silent half-state). */
int plat_lifecycle_request(plat_lifecycle_t *lm, plat_lifecycle_action_t action);
plat_mod_state_t plat_lifecycle_state(const plat_lifecycle_t *lm);
void *plat_lifecycle_instance(const plat_lifecycle_t *lm);
include/platx/lifecycle_serial.h
/* platx/lifecycle_serial.h — A1-P03: lifecycle serialization, bounded recovery.
*
* platx/lifecycle.h is the state machine and platx/recovery.h is the policy.
* Neither of them says who is allowed to act, on which instance, how many
* times, or for how long a decision stays true. This header is that missing
* half, and it is deliberately additive: no type, field or entrypoint of the
* two published headers changes, so every existing caller keeps compiling and
* keeps its ABI.
*
* FOUR THINGS LIVE HERE
*
* 1. Identity. A decision is taken about one generation of one instance.
* Between deciding and applying, that instance can be
* restarted; the generation moves and the decision is about
* something that no longer exists. plat_lc_owner_publish()
* records which generation is live, and every claim is
* checked against it (P03-105). generation 0 is never an
* identity, only the absence of one (P03-114).
*
* 2. Decisions. A decision is a token, not a function call: issued once,
* claimed at most once (P03-112), valid for a bounded time
* (P03-106), and drawn from a fixed-size table so that a
* producer which stops consuming fails loudly instead of
* growing (P03-106, P03-135). The incumbent state is never
* touched by a refused claim.
*
* 3. Admission. Stopping is not a moment, it is an interval. The barrier
* closes first and teardown runs after, so no admission can
* slip between "we decided to stop" and "we started tearing
* down" (P03-117). plat_lc_admits() is the query XIO submit
* makes; plat_lc_admit_drain() is the bounded wait that
* reports what was still in flight rather than pretending
* the count was zero (P03-118, P03-124).
*
* 4. Deadlines. A stop is one absolute deadline shared by every phase, not
* a fresh timeout per phase. plat_lc_deadline_left_ms()
* hands each phase the remainder (P03-123).
*
* WHAT THIS FILE DOES NOT DO
*
* It never calls a descriptor hook, never starts or stops anything, and never
* blocks while holding its own lock. It is a table and a clock. The rule that
* recovery decides and lifecycle alone executes (checked by
* tests/platx/check_lifecycle_policy.sh) is not weakened by anything here.
*
* LOCK ORDER (P03-103)
*
* PLATX_LR_LC_TXN (120) the lifecycle transaction. Entry lock: held while
* descriptor hooks and the resource/task/event
* sweeps run. Everything below may be taken under
* it; it may never be taken under anything below.
* PLATX_LR_REC_WATCH (121) the recovery watch table. Must be released
* before requesting a transition — a tick that held
* it into plat_lifecycle_request() would invert the
* order. Rank 121 > 120 makes that inversion a
* reported violation rather than a comment.
* PLATX_LR_LC_SERIAL (216) the tables in this file. A leaf: no callback,
* no hook and no wait runs while it is held, which
* is what lets XIO submit query the barrier while
* holding its own owner/registry locks (211/212).
* PLATX_LR_CAPREG (217) the capability registry, read from lc_start()
* under the transaction lock.
*
* Ranks are checked only in builds that define PLATX_LOCK_RANK; see
* platx/lock_rank.h. They are declared here rather than added to that file so
* that P03 does not write into a header shared with every other stream; the
* fold-in is a named handoff, and tests/platx/test_lifecycle_contract.c
* asserts meanwhile that these four values collide with nothing in it.
*
* Cards: 101 state contract, 103 lock order, 104 decide/mutate split,
* 105 generation gate, 106 bounded pending decisions, 112 idempotent
* apply, 113 unknown action, 114 generation zero, 115 generation
* exhaustion, 117 revoke ordering, 118 admission barrier for P06,
* 123 single stop deadline, 124 honest drain timeout, 134 stable
* reason codes, 135 bounded history.
*/
/* ── bounds ──────────────────────────────────────────────────────────────
*
* Every table in this file is fixed at compile time. There is no allocation
* anywhere in the implementation, so "the queue is full" is a real answer a
* caller must handle, not a condition that quietly becomes heap growth. */
#define PLAT_LC_PENDING_MAX 16
#define PLAT_LC_OWNER_MAX 32
#define PLAT_LC_HISTORY_MAX 32
#define PLAT_LC_DECISION_TTL_SEC 30
/* ── result codes ────────────────────────────────────────────────────────
*
* Negative, stable, and distinct: a caller that gets -3 back must be able to
* say "stale generation" and not merely "it failed". These numbers are part
* of the contract; A4 receipts quote them (P03-134). */
#define PLAT_LC_OK 0
#define PLAT_LC_E_ARG (-1) /* NULL / malformed argument */
#define PLAT_LC_E_GEN_ZERO (-2) /* generation 0 is not an identity */
#define PLAT_LC_E_STALE_GEN (-3) /* decision is about a dead generation*/
#define PLAT_LC_E_DUPLICATE (-4) /* this token was already claimed */
#define PLAT_LC_E_EXPIRED (-5) /* decision outlived its TTL */
#define PLAT_LC_E_QUEUE_FULL (-6) /* pending table is full */
#define PLAT_LC_E_ACTION (-7) /* action is not a lifecycle action */
#define PLAT_LC_E_GEN_EXHAUSTED (-8) /* no next generation without a wrap */
#define PLAT_LC_E_CLOSED (-9) /* admission barrier is closed */
#define PLAT_LC_E_TIMEOUT (-10) /* bounded wait expired, work remains */
#define PLAT_LC_E_NOENT (-11) /* no such owner / token */
#define PLAT_LC_E_OWNER_FULL (-12) /* owner table is full */
/* ── reason codes (P03-134) ──────────────────────────────────────────────
*
* One vocabulary for "why", shared by the recovery fact, the decision token,
* the history ring and the A4 receipt. Values are frozen: append only, never
* renumber. A failure, the decision it produced, the transition that decision
* caused and the cleanup outcome are joined by (token, reason), which is what
* lets a receipt be read without the source next to it. */
typedef enum plat_lc_reason {
PLAT_LC_R_NONE = 0,
PLAT_LC_R_HEALTH_FATAL = 1,
PLAT_LC_R_HEALTH_DEGRADED = 2,
PLAT_LC_R_TASK_SILENT = 3, /* heartbeat silence > 2×interval */
PLAT_LC_R_CRASH = 4,
PLAT_LC_R_KILLED = 5,
PLAT_LC_R_EXITED = 6,
PLAT_LC_R_BUDGET_EXHAUSTED = 7,
PLAT_LC_R_BACKOFF = 8, /* refused: too soon */
PLAT_LC_R_POLICY_NEVER = 9,
PLAT_LC_R_OPERATOR_STOP = 10, /* a human said stop; never undone */
PLAT_LC_R_STALE_GENERATION = 11,
PLAT_LC_R_DUPLICATE_DECISION = 12,
PLAT_LC_R_DECISION_EXPIRED = 13,
PLAT_LC_R_QUEUE_FULL = 14,
PLAT_LC_R_DRAIN_TIMEOUT = 15,
PLAT_LC_R_GEN_EXHAUSTED = 16,
PLAT_LC_R_NO_PROVIDER = 17, /* nothing to restart into */
PLAT_LC_R_TAMPER = 18,
PLAT_LC_R_ROLLBACK_FAILED = 19, /* the retry failed too */
PLAT_LC_R__MAX = 20
} plat_lc_reason_t;
/* Stable, allocation-free name for a reason. Unknown → "?" (never NULL). */
const char *plat_lc_reason_name(uint16_t reason);
/* Stable, allocation-free name for a result code. Unknown → "?". */
const char *plat_lc_err_name(int rc);
/* ── lock ranks (P03-103) ────────────────────────────────────────────────
* See the header comment. Deliberately spaced into the gaps that
* platx/lock_rank.h leaves between its layers. */
enum {
PLATX_LR_LC_TXN = 120,
PLATX_LR_REC_WATCH = 121,
PLATX_LR_LC_SERIAL = 216,
PLATX_LR_CAPREG = 217
};
/* ── identity ────────────────────────────────────────────────────────────
*
* The registry answers one question: for this (module_id, instance_id), which
* generation is live right now? An instance that was never published has no
* answer, and an unknown instance is not treated as stale — that would refuse
* every caller who never opted in. Publication is what arms the check. */
/* Record `owner` as the live generation of its instance. Publishing a new
* generation replaces the old one; that is the moment every decision about
* the previous generation becomes stale. generation 0 → E_GEN_ZERO. */
int plat_lc_owner_publish(plat_owner_t owner);
/* Live generation of an instance. E_NOENT when it was never published. */
int plat_lc_owner_generation(uint32_t module_id, uint32_t instance_id,
uint32_t *out);
/* 1 = a different generation of this instance is live (the caller's decision
* is about a corpse), 0 = current or never published, <0 = bad argument. */
int plat_lc_owner_stale(plat_owner_t owner);
/* Drop the record. Used on DESTROY so a slot is not held by a dead instance. */
int plat_lc_owner_forget(plat_owner_t owner);
/* Next generation of the same instance, without wrapping to a value that was
* once live (P03-115). At UINT32_MAX the answer is E_GEN_EXHAUSTED and *out
* is untouched: the context ends by an explicit refusal, not by reusing
* generation 1 and silently validating every decision ever taken about it. */
int plat_lc_owner_next_generation(plat_owner_t cur, plat_owner_t *out);
/* ── decisions ───────────────────────────────────────────────────────────
*
* Issue produces a token and holds a slot. Claim consumes the slot and is the
* single point where identity, lifetime and uniqueness are checked. The
* mutation follows the claim and never precedes it, which is how one failure
* becomes one decision and one observable state change (P03-104). */
typedef struct plat_lc_decision {
uint64_t token; /* 0 = not a decision */
plat_owner_t owner;
plat_lifecycle_action_t action;
uint16_t reason; /* plat_lc_reason_t */
uint16_t _pad;
int64_t issued_at; /* monotonic seconds, caller's clock */
int64_t expires_at; /* issued_at + TTL */
} plat_lc_decision_t;
/* Issue a decision about `owner`. `now` is the caller's monotonic clock, so a
* test can drive time without sleeping and a caller without a working clock
* source cannot invent one here.
*
* E_GEN_ZERO owner.generation == 0 (P03-114)
* E_ACTION action is outside the enum (P03-113)
* E_QUEUE_FULL the pending table is full (P03-106) — the incumbent
* state is untouched and no slot is stolen from a live decision
* E_STALE_GEN a newer generation is already published */
int plat_lc_decision_issue(plat_owner_t owner, plat_lifecycle_action_t action,
uint16_t reason, int64_t now,
plat_lc_decision_t *out);
/* Consume the decision. Exactly one caller ever gets PLAT_LC_OK for a given
* token; a second attempt is E_DUPLICATE, so replaying a decision cannot run
* the effect twice or spend the restart budget twice (P03-112).
*
* E_STALE_GEN the instance moved on since the decision was issued (P03-105)
* E_EXPIRED now >= expires_at; the slot is released
* E_NOENT the token was retired, expired away, or never issued */
int plat_lc_decision_claim(const plat_lc_decision_t *d, int64_t now);
/* Release a slot without claiming it — the decision is abandoned, not
* applied. Idempotent: retiring an unknown token is E_NOENT, not a crash. */
int plat_lc_decision_retire(uint64_t token);
/* How many decisions are issued and not yet claimed, retired or expired. */
int plat_lc_decision_pending(void);
/* Drop every decision whose TTL has passed. Returns the number dropped, so a
* caller can tell "nothing to do" from "we are shedding decisions". */
int plat_lc_decision_expire(int64_t now);
/* Total decisions refused because the table was full, since reset. A number
* that grows is a producer outrunning its consumer, and is reported rather
* than absorbed (P03-106). */
uint64_t plat_lc_decision_overflow(void);
/* ── history (P03-135) ───────────────────────────────────────────────────
*
* A fixed ring. When it wraps, the oldest record is lost and the loss is
* counted — the ring never grows, and never claims a completeness it does not
* have. Reading it copies out; no pointer into the ring escapes. */
typedef struct plat_lc_hist {
uint64_t token;
plat_owner_t owner;
plat_lifecycle_action_t action;
uint16_t reason;
int16_t outcome; /* 0 = applied, else a PLAT_LC_E_* */
int64_t at;
} plat_lc_hist_t;
int plat_lc_history_note(const plat_lc_decision_t *d, int outcome);
/* Oldest first. Returns the number written, at most `max`. */
int plat_lc_history_read(plat_lc_hist_t *out, int max);
uint64_t plat_lc_history_dropped(void);
/* ── admission barrier (P03-117 / P03-118) ───────────────────────────────
*
* The contract handed to P06 (card 118, consumed by P06-268):
*
* arm is called once a start is fully allowed — after the module's start
* hook returned and the state is RUNNING or DEGRADED, never before,
* so no consumer sees admission open on a half-started module.
* close is called on entry to STOPPING, before any teardown step. It is
* one atomic operation: after it returns, plat_lc_admits() is 0 for
* this owner and no new submission can be accepted. Submissions
* already inside the gate are counted, not cancelled.
* admits is the query on the submit path. It is a plain read of a leaf
* table: no allocation, no callback, no wait, safe to call while
* holding XIO owner/registry locks.
* enter/leave bracket one in-flight submission so close/drain can tell the
* truth about what was still running.
* drain waits up to timeout_ms for the in-flight count to reach zero. It
* returns E_TIMEOUT and writes the remaining count when it does not,
* which is what stops a stop from reporting success over a module
* that still holds work (P03-124).
*
* The barrier is keyed by the full owner triple. A restart publishes a new
* generation and therefore starts from a closed barrier — a stale generation
* can never be admitted by the barrier its successor armed. */
int plat_lc_admit_arm(plat_owner_t owner);
/* Returns the barrier epoch (>= 1) that this close ended, or a negative
* result code. Closing an already-closed barrier returns its epoch again and
* is not an error: stop is idempotent. */
int plat_lc_admit_close(plat_owner_t owner);
/* 1 = admit, 0 = refuse. An owner with no barrier row refuses: admission is
* something a module is granted, never something it has by default. */
int plat_lc_admits(plat_owner_t owner);
int plat_lc_admit_enter(plat_owner_t owner);
int plat_lc_admit_leave(plat_owner_t owner);
/* Current in-flight count, or a negative result code. */
int plat_lc_admit_inflight(plat_owner_t owner);
/* Bounded wait. `left` may be NULL. timeout_ms <= 0 polls once. */
int plat_lc_admit_drain(plat_owner_t owner, int timeout_ms, unsigned *left);
/* Drop the barrier row entirely (DESTROY). */
int plat_lc_admit_forget(plat_owner_t owner);
/* ── one stop deadline (P03-123) ─────────────────────────────────────────
*
* Quiesce, stop, task drain, XIO drain and unsubscribe drain are phases of
* one stop, not five stops. Each asks for the remainder; none is handed a
* fresh full timeout. An unarmed deadline reports "no limit" (-1) so that a
* caller which never armed one behaves exactly as it did before. */
typedef struct plat_lc_deadline {
int64_t at_ms; /* monotonic milliseconds */
int armed;
} plat_lc_deadline_t;
int64_t plat_lc_now_ms(void);
void plat_lc_deadline_arm(plat_lc_deadline_t *d, int total_ms);
/* Remaining milliseconds: -1 = unarmed (no limit), 0 = expired. */
int plat_lc_deadline_left_ms(const plat_lc_deadline_t *d);
int plat_lc_deadline_expired(const plat_lc_deadline_t *d);
/* ── state contract (P03-101) ────────────────────────────────────────────
*
* plat_state_can_enter() is the implementation of the edge table written in
* the comment of platx/lifecycle.h. This is that comment, machine-readable,
* so the two can be compared instead of trusted:
* tests/platx/test_lifecycle_contract.c walks all 11×11 pairs and fails on
* the first disagreement.
*
* `allowed` is a bitmask over plat_mod_state_t. `terminal` marks a state no
* edge leaves. FAILED is reachable from every state except DESTROYED and is
* therefore folded into every row rather than repeated in prose. */
#define PLAT_LC_BIT(s) (1u << (unsigned)(s))
typedef struct plat_lc_edges {
plat_mod_state_t from;
uint32_t allowed; /* including FAILED where reachable */
int terminal;
} plat_lc_edges_t;
/* The published contract, in declaration order DISCOVERED..FAILED. Returns
* the number of rows; `*rows` points at static storage that outlives any
* caller and is never written. */
int plat_lc_contract_edges(const plat_lc_edges_t **rows);
/* ── test / boot reset ───────────────────────────────────────────────────
* Returns every table in this file to its initial state. Bounded, no
* allocation, no callbacks. Safe to call before any other entrypoint. */
int plat_lc_reset(void);
include/platx/lock_rank.h
/* platx/lock_rank.h — W8-02 / S352: the lock order, made checkable.
*
* tools/lock_order.py extracts the ordering that the code actually follows
* and reports whether it contradicts itself. That is a static answer about
* the paths the extractor could see: it cannot follow a function pointer,
* and it cannot know which branch runs. This is the dynamic half — the same
* ordering, asserted on the acquisitions that really happen.
*
* THE RULE
*
* A thread may acquire a lock only if its rank is strictly greater than
* every rank it already holds. Strictly, not merely greater-or-equal: two
* locks of equal rank have no defined order between them, so nesting them
* is exactly the situation where two threads can pick opposite orders. An
* equal-rank nesting is therefore a violation, not a tie.
*
* RANKS
*
* The values below are a topological layering of build/lock-order.json,
* which had no cycles — that is why an assignment exists at all. Layers are
* spaced by 100 so a lock discovered later can be inserted between two
* existing ones without renumbering the file and invalidating every rank in
* the tree.
*
* 0xx entry locks boot paths, CLI command state, managers. Taken
* first, held while calling inward.
* 1xx subsystem locks the core state of one subsystem.
* 2xx leaf locks ownership tables and registries. Taken last, held
* briefly, and never held while calling back out —
* which is what keeps the order acyclic.
*
* A lock with no rank is unranked and unchecked; unranked locks are ignored
* rather than assumed safe, because guessing a rank would manufacture
* violations that say nothing about the code.
*
* COST
*
* The checking is compiled out entirely unless PLATX_LOCK_RANK is defined.
* Release builds carry no per-thread state, no branches, and no calls: the
* macros expand to nothing. This is a debug-build assertion, in the sense
* W8-02 asks for, not a runtime tax on production.
*
* SPDX-License-Identifier: GPL-2.0
*/
/* ── Ranks, layered from the extracted graph ─────────────────────────── */
enum {
/* Layer 0 — entry locks. */
PLATX_LR_CMD_MBUS = 10,
PLATX_LR_CMD_MESH = 11,
PLATX_LR_DRM_MANAGER = 12,
PLATX_LR_HOOK_BOOT = 13,
PLATX_LR_HOOK_UPROBE = 14,
PLATX_LR_TRACE_DRAIN = 15,
PLATX_LR_XIM_BOOT = 16,
PLATX_LR_XIO_EBPF_LIFE = 17,
PLATX_LR_XIO_MODE = 18,
/* Layer 1 — subsystem state. */
PLATX_LR_HOOK_CORE = 110,
PLATX_LR_MESH_LIVE = 111,
PLATX_LR_XIM_CORE = 112,
PLATX_LR_XIM_HOST = 113,
PLATX_LR_XIO_EBPF_RX = 114,
PLATX_LR_XIO_MODE_DEF = 115,
/* D056: the provider fabric is taken BEFORE the SENSE registry, because
* registration and publication call into it. It is not a leaf: no
* provider vtable entry is ever called while it is held. */
PLATX_LR_PROV_FABRIC = 116,
/* A3 handoff H7: ранги 117/118 приняты за платформой Windows. Слой 1, а
* не 2, потому что порт завершения вызывает наружу, пока держится: лист
* этого не делает по определению. Имена согласуются с A3; номера
* закреплены здесь, и переиспользовать их под другое нельзя. */
PLATX_LR_IOCP_PORT = 117,
PLATX_LR_IOCP_OP = 118,
/* Layer 2 — leaf tables. Nothing calls outward while holding these. */
PLATX_LR_PLUGIN_MANAGER = 210,
PLATX_LR_XIO_OWNER = 211,
PLATX_LR_XIO_REGISTRY = 212,
/* SENSE055: the source table is taken before the ring, never after.
* Both are leaves: no external callback runs while either is held. */
PLATX_LR_SENSE_REGISTRY = 213,
PLATX_LR_SENSE_RING = 214,
/* SENSE081-094: плоскость покрытия берётся после кольца и
* никогда до него. Лист: внешний код под ней не вызывается. */
PLATX_LR_SENSE_COVERAGE = 215,
/* A1-P04: обе таблицы владения — листья, и это утверждение проверяемое.
* Ни один releaser ресурса и ни одна точка входа задачи не вызываются
* под своим локом: sweep помечает строку DRAINING, отпускает таблицу и
* только потом зовёт releaser (plat_resource_owner.c), а трамплин задачи
* забирает точку входа под локом и вызывает её после разблокировки.
*
* Таблица ресурсов берётся ПЕРЕД таблицей задач: releaser ресурса
* PLAT_RES_THREAD ходит в plat_task, обратного пути нет — plat_task_init
* ставит releaser уже после того, как отпустил свою таблицу. */
PLATX_LR_RES_TABLE = 216,
PLATX_LR_TASK_TABLE = 217,
/* A1-P05: таблица подписчиков plat_event — лист, и это проверяемое
* утверждение, а не намерение. Диспетчер отпускает лок перед вызовом
* callback и берёт его снова после возврата; ожидание unsubscribe идёт
* через pthread_cond_(timed)wait, который лок отпускает. Гейт
* test_event_bound.c::c217 доказывает это поведением: callback изнутри
* зовёт plat_event_recent() и plat_event_stats(), обе берут тот же
* нерекурсивный мьютекс — под локом это был бы самозахват.
*
* Ранг ВЫШЕ таблиц ресурсов и задач: releaser ресурса и точка входа
* задачи могут выпускать факт, обратного пути нет — plat_event ничего
* не вызывает наружу под своим локом. */
PLATX_LR_EVENT_TABLE = 218,
PLATX_LR_UNRANKED = 0
};
/* ── Runtime, present only in instrumented builds ────────────────────── */
/* Record an acquisition. Reports a violation when `rank` is not strictly
* greater than every rank this thread already holds. */
void platx_lock_rank_acquire(int rank, const char *name,
const char *file, int line);
/* Record a release. Releasing a rank this thread does not hold is itself
* reported: it means an unlock is running on the wrong lock, or a lock
* leaked out of the function that took it. */
void platx_lock_rank_release(int rank, const char *name);
/* Violations seen since the last reset. Tests read this instead of relying
* on an abort, so that a deliberate inversion can be exercised and the
* process can keep running to check the rest. */
unsigned platx_lock_rank_violations(void);
void platx_lock_rank_reset(void);
/* Snapshot of the most recent violation into `dst` — for a test to assert on
* which inversion was caught, not merely that one was.
*
* Copies under the report mutex rather than handing back a pointer into the
* live buffer: report() rewrites that buffer from another thread, so a
* returned pointer could be overwritten mid-read (B-007).
*
* Writes at most `cap - 1` bytes plus a terminator; `dst` may be NULL when
* `cap` is 0. Returns the length of the full record, so a caller can tell a
* truncated snapshot from a complete one. */
size_t platx_lock_rank_last(char *dst, size_t cap);
/* When set, a violation calls abort() after reporting. Off by default so a
* test can observe violations; a debug run that wants to stop at the first
* inversion turns it on. */
void platx_lock_rank_set_abort(int on);
/* Ranks this thread currently holds, outermost first. Returns the count. */
size_t platx_lock_rank_held(int *out, size_t max);
#define PLATX_LOCK_ACQUIRE(rank, name) \
platx_lock_rank_acquire((rank), (name), __FILE__, __LINE__)
#define PLATX_LOCK_RELEASE(rank, name) \
platx_lock_rank_release((rank), (name))
#define PLATX_LOCK_ACQUIRE(rank, name) ((void)0)
#define PLATX_LOCK_RELEASE(rank, name) ((void)0)
include/platx/logrot.h
/* logrot.h — S417: после ротации писатель пишет в живой файл, а не в сироту.
*
* ОТКУДА БЕРЁТСЯ СИРОТА
* ─────────────────────
* Дескриптор указывает на ИНОД, а не на имя. После rename(path, path.1) старый
* дескриптор продолжает писать — в файл, который теперь называется path.1.
* Если открыть новый path не удалось, а писателя не перепривязали, он останется
* на этом иноде НАВСЕГДА: журнал внешне ротируется, а строки уходят в резервную
* копию, которую следующая ротация переименует дальше и в конце концов удалит.
*
* Поэтому у ротации ровно три допустимых исхода, и «молча продолжать» среди них
* нет:
* 1. перепривязались к новому файлу — обычный успех;
* 2. не смогли — вернули прежнее имя на место и остались на нём;
* 3. не смогли и этого — писатель ОТКАЗЫВАЕТ на каждой записи, пока его не
* откроют заново. Потерянные строки видны как ошибки; строки, ушедшие в
* сироту, не видны никак.
*/
struct xio_t;
typedef struct {
const struct xio_t *io;
char path[256];
int fd;
int refusing; /* 1 — перепривязка не удалась, записи отвергаются */
unsigned long rotations;
} plat_writer_t;
int plat_writer_open (plat_writer_t *w, const struct xio_t *io, const char *path);
int plat_writer_write(plat_writer_t *w, const void *buf, size_t n);
/* Сдвинуть path.N→path.N+1 (не более keep), path→path.1, открыть новый path.
* 0 / -errno. При -EIO писатель в состоянии отказа: см. исход 3. */
int plat_writer_rotate(plat_writer_t *w, unsigned keep);
int plat_writer_close(plat_writer_t *w);
/* Пишет ли писатель прямо сейчас в файл с ЖИВЫМ именем.
* 1 — да, 0 — нет (сирота либо отказ). Для проверок и диагностики. */
int plat_writer_on_live_name(const plat_writer_t *w);
include/platx/module.h
/* platx/module.h — one descriptor, two hosts (embedded / child). */
#define PLAT_MOD_FLAG_OPTIONAL 0x0001u
#define PLAT_MOD_FLAG_SINGLETON 0x0002u
#define PLAT_MOD_FLAG_STATEFUL 0x0004u
/* Isolation is a deploy policy, not the nature of the module. */
typedef enum plat_isolate {
PLAT_ISO_EMBEDDED = 0,
PLAT_ISO_CHILD = 1
} plat_isolate_t;
typedef struct plat_requirement {
const char *capability; /* logical name, e.g. PLAT_NAME_STORAGE_KEYRING */
uint32_t version;
} plat_requirement_t;
typedef struct plat_provide {
const char *capability;
uint32_t version;
} plat_provide_t;
typedef struct plat_create_args {
plat_module_ctx_t *ctx;
const plat_abi_t *abi; /* real or proxy — same type */
plat_owner_t owner;
plat_isolate_t isolate; /* chosen by profile, not by the .c file */
const char *cfg_json;
} plat_create_args_t;
typedef struct plat_module_descriptor {
uint32_t descriptor_size;
uint32_t descriptor_version;
const char *name;
uint32_t module_version;
uint32_t required_core_abi;
const plat_requirement_t *requires; /* NULL-terminated or n_requires */
const plat_requirement_t *optional;
const plat_provide_t *provides;
uint32_t n_requires, n_optional, n_provides;
int (*create)(const plat_create_args_t *args, void **instance);
int (*start)(void *instance);
int (*quiesce)(void *instance); /* drain; no new work */
int (*stop)(void *instance);
void (*destroy)(void *instance);
int (*health)(void *instance, plat_health_t *out);
int (*save_state)(void *instance, void **buf, size_t *len);
int (*load_state)(void *instance, const void *buf, size_t len);
void (*free_state)(void *buf);
int (*register_cmds)(void *instance, void *cmd_reg);
void (*register_script)(void *instance, void *script_ctx);
/* Preferences. Profile may still pick the other allowed mode. */
plat_isolate_t isolation_preference;
uint32_t isolation_allowed; /* bitmask: 1<<EMBEDDED | 1<<CHILD */
const char *seccomp_profile; /* used only if launched as CHILD */
uint32_t flags;
uint8_t reserved[32];
} plat_module_descriptor_t;
/* Every PIC module exports this symbol. */
#define PLAT_MODULE_DESCRIPTOR plat_module_descriptor
/* Are this descriptor's declared requires[] satisfiable right now.
*
* 0 = every mandatory capability is in the registry; -1 = one is not, and
* *missing_out / *version_out name it. optional[] is not consulted: optional
* means the module has a path without it.
*
* Declared here rather than in abi.h because it speaks about a descriptor, and
* abi.h is included by this header, not the other way round.
*
* Lifecycle calls this before start(). A module that named a capability it
* cannot work without must not reach start() and discover the absence halfway
* through, with whatever it had already opened still standing. Both conventions
* the descriptor allows -- n_requires, or NULL-terminated -- are walked. */
int plat_cap_requires_met(const plat_module_descriptor_t *d,
const char **missing_out, uint32_t *version_out);
include/platx/plat_ops_cap.h
/* platx/plat_ops_cap.h — plat_ops via capability APIs (INT-115). */
#define PLAT_OPS_CAP_DIAG "ops.diag:v1"
#define PLAT_OPS_CAP_CTRL "ops.ctrl:v1"
#define PLAT_OPS_CAP_REASON 128
typedef enum {
PLAT_OPS_OK = 0,
PLAT_OPS_NOLINK = 1,
PLAT_OPS_DENIED = 2,
PLAT_OPS_ERROR = 3,
} plat_ops_status_t;
typedef struct {
plat_ops_status_t status;
char reason[PLAT_OPS_CAP_REASON];
} plat_ops_result_t;
/* Gate an ops call via capability lease. 0/-1. */
int plat_ops_cap_gate(uint64_t ctx_id, uint32_t ctx_gen,
const char *cap_name, plat_ops_result_t *out);
include/platx/platx_dsl.h
/* platx_dsl.h — DSL engine public API */
/* INV-DSL-01: undefined variable → compile error (not runtime) */
/* INV-DSL-02: VM stack overflow → rule disabled (no crash) */
/* VM */
typedef struct { uint8_t opcode; uint64_t operand; } dsl_insn_t;
int dsl_vm_rule_add(const char *name, const dsl_insn_t *insns, uint32_t count);
int dsl_vm_exec(uint32_t rule_idx, uint64_t *env_vars, uint32_t env_count);
int dsl_vm_rule_count(void);
int dsl_vm_rule_disabled(uint32_t idx);
void dsl_vm_reset(void);
/* Route DSL */
int dsl_route_add(const char *traffic, const char *condition,
const char *target, uint32_t rule_idx);
const char *dsl_route_match(const char *traffic, uint64_t *env, uint32_t ecount);
int dsl_route_disable(uint32_t idx);
uint32_t dsl_route_count(void);
void dsl_route_reset(void);
/* Sensor DSL */
typedef enum { SENSOR_OP_GT=0, SENSOR_OP_LT=1, SENSOR_OP_EQ=2 } sensor_op_t;
typedef enum { SENSOR_ACT_LOG=0, SENSOR_ACT_ALERT=1, SENSOR_ACT_BLOCK=2 } sensor_action_t;
int dsl_sensor_add(const char *field, sensor_op_t op,
uint64_t threshold, sensor_action_t action);
uint32_t dsl_sensor_eval(const char *field, uint64_t value);
uint64_t dsl_sensor_fire_count(uint32_t idx);
uint32_t dsl_sensor_count(void);
void dsl_sensor_reset(void);
/* Filesystem watch DSL */
typedef enum {
FS_EVENT_CREATE=0x01, FS_EVENT_MODIFY=0x02,
FS_EVENT_DELETE=0x04, FS_EVENT_ANY=0xFF,
} fs_event_mask_t;
typedef void (*fs_action_fn)(const char *path, fs_event_mask_t event, void *ctx);
int dsl_fs_watch_add(const char *path, fs_event_mask_t mask,
fs_action_fn action, void *ctx);
void dsl_fs_dispatch(const char *path, fs_event_mask_t event);
int dsl_fs_watch_disable(uint32_t idx);
uint64_t dsl_fs_trigger_count(uint32_t idx);
uint32_t dsl_fs_watch_count(void);
void dsl_fs_reset(void);
include/platx/platx_features.h
/* SPDX-License-Identifier: GPL-2.0-only */
/* include/platx/platx_features.h - compile-time feature registry. Wave 2.43,
* 2.44, 5.41. Feature flags are additive; a profile turns them off to build a
* smaller image (MINIMAL mode). A feature being OFF must never fault another.
* Default: all ON unless PLATX_PROFILE_MINIMAL is defined. */
# define PLATX_FEATURE_HADES 0 /* MINIMAL: sense works without ebpf */
# define PLATX_FEATURE_HADES 1
# define PLATX_FEATURE_SENSE 1
# define PLATX_FEATURE_SELFPROTECT 1
include/platx/platx_log.h
/* platx_log.h — public API forwarding for the log subsystem */
/* log_export.c */
/* INV-LOG-01: register audit sink so LOG_FATAL → audit ring */
int log_audit_sink_register(void);
/* Export ring snapshot to binary file.
* Returns number of entries written or -1. */
int log_export_to_file(const char *path);
/* log_rotate.c */
typedef struct log_s log_t;
int log_rotate(log_t *l, const char *path);
include/platx/platx_mirage.h
/* include/platx/platx_mirage.h — MIRAGE sandbox public API (task 4.23) */
/* Feature flag — compile-time opt-in */
/* Invariants:
* INV-MIRAGE-01: sandbox exit code != 0 → rehearsal FAILED
* INV-MIRAGE-02: sandbox writes outside sandbox_root → immediate error
* INV-MIRAGE-03: sandbox duration > MIRAGE_MAX_TTL_MS → SIGKILL
*/
#define MIRAGE_MAX_TTL_MS 10000u
typedef struct {
int exit_code;
int timed_out; /* INV-MIRAGE-03 tripped */
int passed; /* INV-MIRAGE-01: 1 iff exit_code==0 && !timed_out */
uint64_t elapsed_ms;
char label[64];
} mirage_result_t;
/* World (sandbox environment) */
int mirage_world_define(const char *root, const char *workdir,
uint32_t uid, uint32_t gid, int readonly);
int mirage_world_get (int id, char *root, char *work,
uint32_t *uid, uint32_t *gid, int *readonly);
int mirage_world_count (void);
/* Execution */
typedef struct {
const char *argv[16];
int argc;
int world_id;
int probe;
} mirage_exec_req_t;
typedef struct {
int exit_code;
int timed_out;
uint64_t elapsed_ms;
} mirage_exec_result_t;
int mirage_exec(const mirage_exec_req_t *req, mirage_exec_result_t *res);
/* Probe mode */
int mirage_probe_path(const char *sandbox_root, const char *rel_path);
int mirage_probe_is_writable(const char *sandbox_root, const char *path);
/* Record & compare */
int mirage_record_append(const char *label, int exit_code,
int timed_out, uint64_t elapsed_ms);
void mirage_record_stats(uint64_t *rehearsals, uint64_t *passed, uint64_t *failed);
int mirage_compare(int rehearsal_exit, int real_exit);
int mirage_compare_elapsed(uint64_t r_ms, uint64_t real_ms, uint64_t tol_ms);
/* MIL gate */
int mirage_mil_gate(int rehearsal_id, int threat_level, int has_mirage);
/* GC + watchdog */
int mirage_gc_register(pid_t pid, uint64_t started_ms);
int mirage_gc_run(uint64_t now_ms, uint64_t max_ttl_ms);
int mirage_watchdog_wait(pid_t pid, uint64_t ttl_ms);
/* Audit */
void mirage_audit_record(uint32_t op_type, uint32_t flags,
uint64_t rehearsal_id, int exit_code);
uint64_t mirage_audit_cursor(void);
/* Seccomp */
int mirage_seccomp_apply(void);
include/platx/platx_modhost.h
/* include/platx/platx_modhost.h — MODHOST public API (task 5.29) */
/* Invariants:
* INV-MODHOST-01: module without MSX signature → REFUSE (§MIL-10)
* INV-MODHOST-02: module without lease → REFUSE
* INV-MODHOST-03: unload failing → force unload + panic report
*/
#define MODHOST_INVENTORY_SLOTS 64u
#define MODHOST_AUDIT_SLOTS 1024u
typedef struct {
char name[128];
char path[256];
void *handle;
int loaded;
uint64_t load_count;
uint32_t flags;
uint64_t loaded_at_ms;
} modhost_slot_t;
/* Inventory */
int modhost_inventory_add(const char *name, const char *path,
void *handle, uint32_t flags);
int modhost_inventory_remove(const char *name);
modhost_slot_t *modhost_inventory_find(const char *name);
int modhost_inventory_count(void);
/* Hot reload */
int modhost_reload(const char *name, const char *new_path);
/* Sandbox */
int modhost_sandbox_run(const char *module_path, int world_id);
/* Audit */
void modhost_audit_record(uint32_t op, uint32_t flags,
const char *name, int result);
uint64_t modhost_audit_cursor(void);
/* Lockdown (TL4) */
int modhost_lockdown_check(int threat_level, const char *name);
/* Metrics */
void modhost_metrics_inc_loaded(void);
void modhost_metrics_inc_failed(void);
void modhost_metrics_inc_unloaded(void);
int modhost_metrics_export(void (*emit)(const char*, uint64_t, void*), void *ctx);
include/platx/platx_msx.h
/* include/platx/platx_msx.h — MSX signature public API (task 5.29) */
/* INV-MSX-01: revoked signature → immediate module unload */
#define MSX_REVOKE_SLOTS 256u
#define MSX_KEY_SLOTS 8u
/* Revocation */
int msx_revoke_add(const uint8_t *fingerprint);
int msx_revoke_check(const uint8_t *fingerprint); /* 1=revoked, 0=ok */
int msx_revoke_count(void);
/* Key rotation */
int msx_key_add(uint32_t key_id, const uint8_t *pubkey);
int msx_key_rotate(uint32_t new_key_id);
int msx_key_active_id(void);
const uint8_t *msx_key_get_pubkey(uint32_t key_id);
/* §MIL-10 enforcement */
int msx_mil_verify(const char *path, const uint8_t *fingerprint);
int msx_mil_check_fp(const uint8_t *fingerprint);
/* Existing API stubs (implemented in msx_verify.c / msx_sign.c) */
int msx_verify_path(const char *path);
include/platx/platx_plugin.h
/* include/platx/platx_plugin.h — Plugin public API (task 5.29) */
#define PLUGIN_API_VERSION_MAJOR 1u
#define PLUGIN_API_VERSION_MINOR 0u
#define PLUGIN_API_VERSION ((PLUGIN_API_VERSION_MAJOR << 16) | PLUGIN_API_VERSION_MINOR)
typedef struct {
uint32_t api_version;
const char *name;
const char *description;
int (*init)(void *ctx);
int (*fini)(void *ctx);
int (*call)(uint32_t op, void *in, void *out, size_t out_len);
} plugin_api_t;
/* Registration */
int plugin_api_register(const plugin_api_t *api);
const plugin_api_t *plugin_api_find(const char *name);
uint32_t plugin_api_version(void);
int plugin_api_count(void);
/* Sandbox */
int plugin_sandbox_register(const char *name, int sandboxed);
int plugin_sandbox_check(const char *name);
/* Revoke */
int plugin_revoke_add(const char *name);
int plugin_revoke_check(const char *name); /* 1=revoked */
int plugin_revoke_count(void);
include/platx/platx_poe.h
/* platx_poe.h — Proof-of-Execution public API */
#define POE_CHAIN_SLOTS 256
#define POE_RECEIPT_CLAIM 0u
#define POE_RECEIPT_DECISION 1u
#define POE_RECEIPT_EFFECT 2u
/* INV-POE-01: every EFFECT must have CLAIM + DECISION predecessors */
int poe_chain_append(uint32_t receipt_type, const uint8_t *payload, size_t plen);
int poe_chain_verify(uint64_t seq);
uint64_t poe_chain_count(void);
const uint8_t *poe_chain_tip_hash(void); /* 32-byte SHA-256, NULL if empty */
int poe_anchor_tip(uint64_t corr_id, uint64_t source_id);
int poe_anchor_check_chain(uint32_t t0, uint32_t t1, uint32_t t2);
uint64_t poe_anchor_chain_depth(void);
include/platx/platx_proxy.h
/* platx_proxy.h — Proxy/X public API (PLATX_HAS_PROXY; not in MIL mode) */
/* §7.31: gated by PLATX_HAS_PROXY; §7.32: unavailable in MIL mode */
void proxy_intercept_set_mil(int v);
int proxy_intercept_record(const char *url, const uint8_t *req, uint32_t rlen,
const uint8_t *resp, uint32_t resplen, int tls);
uint32_t proxy_intercept_count(void);
const char *proxy_intercept_url(uint32_t idx);
void proxy_intercept_reset(void);
/* HTTP decode */
typedef struct {
char method[16]; char path[512]; char version[16];
struct { char name[64]; char value[256]; } headers[32];
uint32_t hdr_count, body_offset, body_len;
} proxy_request_t;
int proxy_decode_request(const uint8_t *raw, size_t len, proxy_request_t *out);
const char *proxy_decode_header(const proxy_request_t *req, const char *name);
/* Scan */
int proxy_scan_add(const char *pattern, const char *label);
uint32_t proxy_scan_check(const uint8_t *data, size_t dlen);
uint64_t proxy_scan_match_count(uint32_t idx);
void proxy_scan_reset(void);
/* Modify */
int proxy_modify_add(const char *field, const char *old, const char *new_v);
int proxy_modify_apply(char *buf, size_t bufsz, const char *field);
void proxy_modify_reset(void);
/* Replay */
int proxy_replay_store(const uint8_t *req, uint32_t len);
const uint8_t *proxy_replay_get(uint32_t idx, uint32_t *len_out);
uint32_t proxy_replay_count(void);
void proxy_replay_reset(void);
/* Fuzz */
int proxy_fuzz_init_builtins(void);
int proxy_fuzz_add(const uint8_t *data, uint32_t len);
const uint8_t *proxy_fuzz_payload(uint32_t idx, uint32_t *len_out);
uint32_t proxy_fuzz_count(void);
void proxy_fuzz_reset(void);
/* Report */
int proxy_report_add(const char *url, const char *finding,
uint32_t pattern_mask, int severity);
int proxy_report_write(const char *path);
uint32_t proxy_report_count(void);
void proxy_report_reset(void);
/* POE export */
int proxy_export_to_poe(const char *url, uint32_t pattern_mask,
int blocked, uint64_t corr_id);
/* Encode */
int proxy_urlencode(const char *in, char *out, size_t outsz);
int proxy_urldecode(const char *in, char *out, size_t outsz);
int proxy_base64_encode(const uint8_t *in, size_t ilen, char *out, size_t outsz);
include/platx/platx_taskmgr.h
/* platx_taskmgr.h — public API forwarding for the task manager */
/* INV-TASKMGR-01: task without deadline → MAX_TASK_TTL (300 s) */
#define MAX_TASK_TTL 300
/* taskmgr_queue.c */
void taskmgr_queue_init(void);
int taskmgr_queue_push(const char *name, const char *owner,
task_prio_t prio, time_t deadline);
int taskmgr_queue_depth(void);
/* taskmgr_sched.c */
void taskmgr_sched_init(void);
int taskmgr_sched_submit(const char *name, const char *owner,
task_prio_t prio, time_t deadline);
int taskmgr_sched_cancel(int slot);
int taskmgr_sched_tick(void);
int taskmgr_sched_pending(void);
include/platx/platx_trace.h
/* platx_trace.h — public API forwarding for the trace subsystem */
/* trace_propagate.c */
int trace_propagate_capture(trace_ctx_t *out);
int trace_propagate_restore(const trace_ctx_t *ctx);
int trace_propagate_encode(const trace_ctx_t *ctx, char *buf, size_t buflen);
int trace_propagate_decode(const char *header, trace_ctx_t *out);
/* trace_export.c */
int trace_export_to_file(const trace_id_t *id, const char *path);
int trace_export_jsonl(const char *path);
include/platx/platx_vault.h
/* platx_vault.h — Vault secret storage public API (INV-VAULT-01) */
#define VAULT_SLOTS 32
#define VAULT_SECRET_MAX 512
#define VAULT_HMAC_LEN 32
/* INV-VAULT-01: vault requires valid profile HMAC (§SEC-0) before any op */
int vault_set_profile_hmac(const uint8_t *hmac, size_t len);
int vault_seal(const uint8_t *secret, size_t slen,
const uint8_t *key_hmac, size_t klen,
uint32_t *out_id);
int vault_unseal(uint32_t id, const uint8_t *key_hmac, size_t klen,
uint8_t *out, size_t out_cap, size_t *out_len);
int vault_unseal_safe(uint32_t id, const uint8_t *key_hmac, size_t klen,
uint8_t *out, size_t out_cap, size_t *out_len);
int vault_close(uint32_t id);
int vault_rotate_init(const uint8_t *initial_key, size_t klen);
int vault_rotate_slot(uint32_t id,
const uint8_t *old_key, size_t oklen,
const uint8_t *new_key, size_t nklen);
int vault_rotate_profile(const uint8_t *new_hmac, size_t len);
include/platx/platx_vfs.h
/* platx_vfs.h — public API for the VFS subsystem */
/* vfs_file.c */
int vfs_file_load(const char *fspath, const char *vpath, vfs_kind_t kind);
int vfs_file_save(const char *vpath, const char *fspath);
int vfs_file_read_text(const char *vpath, char **out, size_t *out_len);
int vfs_file_write_text(const char *vpath, vfs_kind_t kind,
const char *text, size_t len);
/* vfs_dir.c */
int vfs_dir_list(const char *prefix, vfs_stat_t *buf, int buf_count);
int vfs_dir_count(const char *prefix);
int vfs_dir_exists(const char *prefix);
/* vfs_async.c */
typedef void (*vfs_async_cb)(int result, void *arg);
int vfs_async_get(const char *path, vfs_async_cb cb, void *arg);
int vfs_async_put(const char *path, vfs_kind_t kind,
const uint8_t *data, size_t len,
vfs_async_cb cb, void *arg);
int vfs_async_drain(void);
include/platx/platx_wiper.h
/* platx_wiper.h — public API for the wiper subsystem */
/* INV-WIPER-01: wiper_mem must be called for every secret at cleanup.
* The volatile barrier prevents compiler elision. */
void wiper_mem(void *ptr, size_t len);
/* Convenience wrappers */
void wiper_key(void *key, size_t klen);
void wiper_key32(void *key); /* wipes 32 bytes */
void wiper_key64(void *key); /* wipes 64 bytes */
void wiper_key_field(void *obj, size_t offset, size_t klen);
/* 3-pass DoD file overwrite (0x00 / 0xFF / 0x55) + fsync after each pass.
* Returns 0 on success, -1 on I/O error. */
int wiper_file(const char *path);
include/platx/pluginset.h
/* pluginset.h — S489: набор плагинов принимается ЦЕЛИКОМ или не принимается.
*
* ПОЧЕМУ НАБОР, А НЕ ПЛАГИН
* ─────────────────────────
* Плагины загружают списком, и список почти всегда смешанный: часть собрана
* против прошлого выпуска, часть против нынешнего. Загрузка «по одному, пока
* получается» оставляет систему в состоянии, которого нет ни в одном плане:
* три плагина работают, четвёртый отвергнут, пятый не пробовали. Оператор
* видит «частично загружено» и не знает, что из этого действует.
*
* Поэтому две фазы, и граница между ними — главное в этом файле:
*
* ДОПУСК все члены проверяются ДО того, как хоть один начал работать.
* Ни одного побочного действия: проверка не «пробует загрузить».
* ВКЛЮЧЕНИЕ начинается только если допуск прошёл ВЕСЬ набор.
*
* И третье, без чего первые два — половина решения: если включение упало на
* середине, уже включённые ВЫКЛЮЧАЮТСЯ. Иначе всё сводится к тому же
* «частично», только позже.
*
* Отказ обязан назвать ПЕРВОГО виновника поимённо. «Набор отвергнут» без
* имени заставляет выключать плагины по одному, чтобы найти который.
*/
#define PLAT_PSET_MAX 32
#define PLAT_PSET_NAME_MAX 48
/* Поддерживаемый диапазон ABI плагина. Смешанный набор — это набор, где
* встречаются оба края диапазона; он допустим. Выход за край — нет. */
#define PLAT_PSET_ABI_MIN 2u
#define PLAT_PSET_ABI_MAX 3u
typedef enum {
PLAT_PSIG_OK = 0,
PLAT_PSIG_BAD = 1,
PLAT_PSIG_MISSING = 2
} plat_psig_t;
typedef struct {
char name[PLAT_PSET_NAME_MAX];
uint32_t abi;
plat_psig_t sig;
int active; /* выставляет только plat_pset_activate */
} plat_plugin_t;
typedef enum {
PLAT_PSET_OK = 0,
PLAT_PSET_ABI_OLD = 1,
PLAT_PSET_ABI_NEW = 2,
PLAT_PSET_SIG = 3,
PLAT_PSET_DUPLICATE = 4,
PLAT_PSET_ACTIVATE = 5, /* включение упало — набор откачен */
PLAT_PSET_BAD_ARG = 6
} plat_pset_verdict_t;
/* Проверка всего набора. НИЧЕГО не включает и не меняет.
* bad_idx (если не NULL) — индекс ПЕРВОГО непринятого. */
plat_pset_verdict_t plat_pset_admit(const plat_plugin_t *set, size_t n,
size_t *bad_idx);
/* Включатель одного плагина: 0 — включён, иначе отказ. */
typedef int (*plat_pset_on_fn)(plat_plugin_t *p, void *ud);
/* Выключатель для отката. Обязан быть безусловным. */
typedef void (*plat_pset_off_fn)(plat_plugin_t *p, void *ud);
/* Допуск + включение. Если допуск не прошёл — НИ ОДИН не включается.
* Если включение упало на k-м — все включённые до него выключаются, и в
* наборе не остаётся ни одного активного. */
plat_pset_verdict_t plat_pset_activate(plat_plugin_t *set, size_t n,
plat_pset_on_fn on,
plat_pset_off_fn off, void *ud,
size_t *bad_idx);
/* Сколько активных сейчас. */
size_t plat_pset_active_count(const plat_plugin_t *set, size_t n);
const char *plat_pset_verdict_name(plat_pset_verdict_t v);
include/platx/policy_dist.h
/* platx/policy_dist.h — INT-144: Signed policy bundle distribution.
* HMAC verification hook is weak; unit tests link without a crypto impl. */
#define PLAT_POLICY_MAX_CAPS 32
#define PLAT_POLICY_MAX_BUNDLES 16
#define PLAT_POLICY_ID_LEN 64
#define PLAT_POLICY_CAP_LEN 64
typedef struct plat_policy_cap_entry {
char capability[PLAT_POLICY_CAP_LEN];
uint32_t version;
uint32_t flags;
} plat_policy_cap_entry_t;
typedef struct plat_policy_bundle {
char policy_id[PLAT_POLICY_ID_LEN];
uint32_t version;
plat_policy_cap_entry_t caps[PLAT_POLICY_MAX_CAPS];
size_t n_caps;
uint8_t hmac_digest[32]; /* HMAC-SHA256 signature */
uint64_t issued_at_ms;
uint64_t expiry_ms; /* 0 = no expiry */
} plat_policy_bundle_t;
typedef struct plat_policy_dist_result {
int loaded;
int verified; /* 1 if HMAC verified; 0 if verifier not linked */
int applied;
char reason[128];
} plat_policy_dist_result_t;
int plat_policy_dist_init(void);
void plat_policy_dist_fini(void);
/* Load and optionally verify a bundle. Returns 0 on success. */
int plat_policy_dist_load(const plat_policy_bundle_t *bundle,
plat_policy_dist_result_t *out);
/* Apply a loaded bundle to the running ABI. Returns 0 on success. */
int plat_policy_dist_apply(const plat_policy_bundle_t *bundle,
plat_policy_dist_result_t *out);
/* Revoke by policy_id. Returns 0 if found, -1 if not found. */
int plat_policy_dist_revoke(const char *policy_id);
/* Count active (applied, not expired) bundles. */
int plat_policy_dist_count(void);
include/platx/privcheck.h
/* privcheck.h — S519: недостающее право называется ДО попытки.
*
* ПОЧЕМУ «ДО», А НЕ «ПОСЛЕ»
* ─────────────────────────
* Привилегированную операцию почти всегда пишут так: попробовать, поймать
* EPERM, пожаловаться. Это работает ровно до тех пор, пока операция не
* оставляет следа. А она оставляет: половина цепочки уже выполнена, сокет
* открыт, файл создан, поколение потрачено. Отказ приходит в середине, и
* убирать за ним приходится тому же коду, который только что узнал, что он
* не имел права начинать.
*
* И вторая, более тихая беда: EPERM ничего не называет. «Operation not
* permitted» одинаково означает «нет CAP_NET_RAW», «нет CAP_SYS_ADMIN»,
* «мешает NO_NEW_PRIVS» и «ядро старое». Все четыре чинятся по-разному,
* и оператор различает их только чтением исходника.
*
* Поэтому здесь: объявленная таблица привилегированных операций, и у
* каждой — требуемое право, способ ЭТО ПРОВЕРИТЬ и точное указание, что
* сделать. Проверка происходит до первого побочного действия.
*
* ЧЕГО ЗДЕСЬ НЕТ
* ──────────────
* Это не замена проверке ядра. Ядро всё равно решает, и его отказ остаётся
* последним словом. Здесь — предварительный ответ на вопрос «а стоит ли
* начинать», и он обязан быть ЧЕСТНЫМ в обе стороны: сказать «нельзя» там,
* где можно, так же вредно, как наоборот.
*/
typedef enum {
PLAT_PRIV_RAW_SOCKET = 0, /* захват с интерфейса */
PLAT_PRIV_BPF_LOAD = 1, /* загрузка программы eBPF */
PLAT_PRIV_IO_URING_SQ = 2, /* SQPOLL: опрашивающий поток ядра */
PLAT_PRIV_USERNS = 3, /* своё пространство пользователей */
PLAT_PRIV_LANDLOCK = 4, /* ограничение доступа к ФС */
PLAT_PRIV_PTRACE = 5, /* присоединение к чужому процессу */
PLAT_PRIV_COUNT = 6
} plat_priv_op_t;
typedef enum {
PLAT_PRIV_OK = 0, /* можно начинать */
PLAT_PRIV_MISSING = 1, /* права нет — названо какого */
PLAT_PRIV_NO_KERNEL= 2, /* ядро не умеет — правом не поможешь */
PLAT_PRIV_BLOCKED = 3, /* запрещено политикой хоста */
PLAT_PRIV_UNKNOWN = 4 /* проверить нечем — начинать вслепую */
} plat_priv_verdict_t;
typedef struct {
plat_priv_verdict_t verdict;
char op[48]; /* что собирались делать */
char need[64]; /* чего именно не хватает */
char how[160]; /* что сделать оператору */
char probed[96]; /* чем проверяли — чтобы можно было перепроверить */
} plat_priv_report_t;
/* Проверить ДО попытки. Никаких побочных действий: ни сокета, ни файла,
* ни процесса. Возвращает вердикт и заполняет отчёт. */
plat_priv_verdict_t plat_priv_check(plat_priv_op_t op, plat_priv_report_t *out);
/* Человеку — одной строкой. Никогда не NULL. */
const char *plat_priv_op_name(plat_priv_op_t op);
const char *plat_priv_verdict_name(plat_priv_verdict_t v);
/* Есть ли у нас возможность по имени бита (CAP_*). -1 — проверить нечем. */
int plat_priv_have_cap(int cap_bit);
include/platx/profile_check.h
/* platx/profile_check.h — INT-141: profile minimum capability contract.
*
* A profile declares the minimum set of capabilities that must be live
* before it is considered READY. plat_profile_check() walks the required
* list and verifies each is registered in the core capability table.
*
* Profiles are static descriptors; no dynamic registration.
* "READY" means every required capability is present AND version-compatible.
* A missing or version-incompatible capability is a hard fail, not a warning.
*
* ── A1-P02 wave 2 (2026-09-07). Additive; no legacy field moved. ─────────
* P02-054 The extended-policy block moved INSIDE the include guard and
* INSIDE extern "C". It used to sit after the `#endif`, guarded
* only by `#pragma once`. `#pragma once` keys on file identity,
* so two physical copies of this header on one include path
* (vendored copy, out-of-tree stage dir, A3 portable copy)
* were both expanded and every enum/struct was redeclared:
* 35 hard errors. It was also outside extern "C", so a C++
* consumer mangled plat_profile_validate_ext and failed to link
* against the C core. Both are fixed here.
* P02-058 Error codes name the phase that refused. PLAT_PROFVAL_TRUSTGEN
* split out of PLAT_PROFVAL_BOUNDS; plat_profval_err_name()
* returns a stable name that never carries profile bytes,
* key material or digests.
* P02-055 plat_profile_snapshot_t: identity + revision + digest + boot
* generation, published once, read by every consumer.
* P02-056 plat_profile_apply_defaults(): defaults for absent optional
* fields that never weaken a mandatory CIVIL or MIL rule.
* P02-057 Semantic rejection of incompatible capability/limit combos.
* P02-063 plat_profile_ext_note_field(): order-independent duplicate
* accounting for mandatory fields, shared with the wire decoder.
* P02-071 plat_profile_set_limits(): the only supported mutator; it
* refuses after freeze with PLAT_PROFVAL_FROZEN.
*/
#define PLAT_PROFILE_ID_MAX 32
#define PLAT_PROFILE_CAP_MAX 16
#define PLAT_PROFILE_REASON_MAX 128
/* ── Required capability entry ───────────────────────────────────────── */
typedef struct plat_profile_cap_req {
const char *capability; /* capability name (must match capreg) */
uint32_t min_version; /* minimum acceptable version */
} plat_profile_cap_req_t;
/* ── Profile descriptor ──────────────────────────────────────────────── */
typedef struct plat_profile_desc {
char id[PLAT_PROFILE_ID_MAX];
const plat_profile_cap_req_t *required; /* NULL-terminated array */
size_t n_required;
} plat_profile_desc_t;
/* ── Check result ────────────────────────────────────────────────────── */
typedef struct plat_profile_result {
int ready; /* non-zero = all caps present */
char missing[PLAT_PROFILE_CAP_MAX][PLAT_PROFILE_ID_MAX]; /* absent caps */
int n_missing;
char reason[PLAT_PROFILE_REASON_MAX];
} plat_profile_result_t;
/* ── API ─────────────────────────────────────────────────────────────── */
/* Check all required capabilities for profile desc.
* Returns 0 (all present) or -1 (at least one missing).
* out is always populated; out->ready reflects the overall verdict.
*/
int plat_profile_check(const plat_profile_desc_t *desc,
plat_profile_result_t *out);
/* Well-known profile IDs */
#define PLAT_PROFILE_AGENT "agent" /* minimal agent: context+lease+gadget */
#define PLAT_PROFILE_SERVER "server" /* server: agent + mbus + ra2c */
#define PLAT_PROFILE_FULL "full" /* full stack: all fabric subsystems */
/* Predefined profiles (call plat_profile_check with these) */
extern const plat_profile_desc_t plat_profile_agent;
extern const plat_profile_desc_t plat_profile_server;
extern const plat_profile_desc_t plat_profile_full;
/* ── Extended policy validation (A1-POLICY-LOCAL P02:051-080) ────────── */
/* Profile kind: governs MIL→CIVIL transition prevention (P02-070). */
typedef enum {
PLAT_PROFILE_KIND_CIVIL = 0,
PLAT_PROFILE_KIND_MIL = 1,
} plat_profile_kind_t;
/* Module verification policy (P02-080): how the profile asserts module integrity. */
typedef enum {
PLAT_MODVER_NONE = 0, /* No module verification required */
PLAT_MODVER_REQUIRED = 1, /* Module verification capability must be present */
PLAT_MODVER_STRICT = 2, /* Strict: verifier must be linked and confirm */
} plat_modver_policy_t;
/* Watchdog co-validation parameters (P02-078). */
typedef struct plat_watchdog_policy {
uint32_t interval_ms; /* Heartbeat interval; 0 = not configured */
uint32_t max_strikes; /* Max missed heartbeats before restart; 0 = uncapped */
uint32_t restart_ceil; /* Max restarts before permanent failure; 0 = uncapped */
} plat_watchdog_policy_t;
/* Audit retention policy fields (P02-079). */
typedef struct plat_audit_policy {
int required; /* Non-zero: audit sink is mandatory at startup */
uint32_t min_retention_s; /* Minimum retention in seconds; 0 = no minimum */
} plat_audit_policy_t;
/* Numeric budget limits passed from profile snapshot to runtime (P02-076). */
typedef struct plat_profile_limits {
uint32_t max_xio_owners; /* 0 = not set (P02-077 check) */
uint32_t max_tasks; /* 0 = not set */
uint32_t max_events; /* 0 = not set */
} plat_profile_limits_t;
#define PLAT_PROFILE_EXT_VERSION 1
/* P02-063: mandatory-field identifiers. The wire decoder reports each
* mandatory field it has seen through plat_profile_ext_note_field(); a
* second report of the same identifier is a duplicate regardless of the
* order the fields arrived in. */
typedef enum {
PLAT_PROFFIELD_KIND = 0,
PLAT_PROFFIELD_MODVER = 1,
PLAT_PROFFIELD_LIMITS = 2,
PLAT_PROFFIELD_WATCHDOG = 3,
PLAT_PROFFIELD_AUDIT = 4,
PLAT_PROFFIELD_TRUSTGEN = 5,
PLAT_PROFFIELD__COUNT = 6,
} plat_proffield_t;
/* Extended profile descriptor: carries all load-time policy fields.
* Fields are validated by plat_profile_validate_ext() before startup.
* After plat_profile_freeze(), the struct must not be mutated (P02-071). */
typedef struct plat_profile_ext {
uint32_t version; /* Must be PLAT_PROFILE_EXT_VERSION */
uint32_t n_required; /* Cap count (bounds-checked, P02-061) */
plat_profile_kind_t kind; /* MIL or CIVIL (P02-070) */
plat_modver_policy_t modver; /* Module verification policy (P02-080) */
plat_watchdog_policy_t watchdog; /* Watchdog co-validation (P02-078) */
plat_audit_policy_t audit; /* Audit retention policy (P02-079) */
plat_profile_limits_t limits; /* Numeric budget limits (P02-076) */
uint64_t trust_gen; /* Trust root generation (P02-066) */
uint64_t issued_at_ms; /* Issuance time; 0 = no freshness (P02-069) */
uint64_t expires_at_ms; /* 0 = no expiry */
int revoked; /* Non-zero: revoked; check before READY (P02-067) */
int frozen; /* Non-zero: locked, no mutations (P02-071) */
uint32_t seen_mask; /* P02-063: plat_proffield_t bitmap */
/* No padding fields: reserved bits are not extensible silently (P02-064) */
} plat_profile_ext_t;
/* Validation error codes (P02-058).
* Each code names the phase that refused. No code carries profile bytes,
* key material or a digest — see plat_profval_err_name(). */
typedef enum {
PLAT_PROFVAL_OK = 0,
PLAT_PROFVAL_BOUNDS = 1, /* P02-061: truncated/out-of-range input */
PLAT_PROFVAL_OVERFLOW = 2, /* P02-062: length/offset overflow */
PLAT_PROFVAL_DUPLICATE = 3, /* P02-063: duplicate mandatory field */
PLAT_PROFVAL_CRITICAL = 4, /* P02-064: unknown critical field */
PLAT_PROFVAL_REVOKED = 5, /* P02-067: revoked profile */
PLAT_PROFVAL_NO_VERIFIER = 6, /* P02-068: verifier absent, auth required */
PLAT_PROFVAL_STALE = 7, /* P02-069: freshness check failed */
PLAT_PROFVAL_MIL_CIVIL = 8, /* P02-070: MIL→CIVIL transition blocked */
PLAT_PROFVAL_FROZEN = 9, /* P02-071: mutation after freeze */
PLAT_PROFVAL_ZERO_BUDGET = 10, /* P02-077: unconfigured critical budget */
PLAT_PROFVAL_WATCHDOG = 11, /* P02-078: watchdog co-validation failed */
PLAT_PROFVAL_AUDIT = 12, /* P02-079: audit retention field invalid */
PLAT_PROFVAL_MODVER = 13, /* P02-080: module verification policy violation */
PLAT_PROFVAL_TRUSTGEN = 14, /* P02-066: trust-root generation mismatch */
PLAT_PROFVAL_MAGIC = 15, /* P02-052: wire magic/version not ours */
PLAT_PROFVAL_SEMANTICS = 16, /* P02-057: capability/limit combo refused */
PLAT_PROFVAL_AUTH = 17, /* P02-065: authenticated span does not
* cover the semantic bytes */
} plat_profval_err_t;
typedef struct plat_profile_val_result {
plat_profval_err_t code;
char reason[PLAT_PROFILE_REASON_MAX];
} plat_profile_val_result_t;
/* Validate an extended profile descriptor. Must be called before READY.
* current_trust_gen: current boot's trust root generation (for P02-066).
* now_ms: current monotonic time in ms (for P02-069 freshness check).
* current_kind: running profile kind (for P02-070 MIL→CIVIL check).
* Returns PLAT_PROFVAL_OK on success; populates out on any failure.
*/
plat_profval_err_t plat_profile_validate_ext(
const plat_profile_ext_t *ext,
uint64_t current_trust_gen,
uint64_t now_ms,
plat_profile_kind_t current_kind,
plat_profile_val_result_t *out);
/* Freeze the profile after successful validation (P02-071/072).
* Sets ext->frozen = 1. Subsequent mutation attempts through
* plat_profile_set_limits() are refused with PLAT_PROFVAL_FROZEN.
*/
void plat_profile_freeze(plat_profile_ext_t *ext);
/* P02-071/075: the only supported mutator for a validated descriptor.
* Refuses with PLAT_PROFVAL_FROZEN once plat_profile_freeze() ran, so a
* caller that kept a non-const alias cannot widen policy after READY. */
plat_profval_err_t plat_profile_set_limits(plat_profile_ext_t *ext,
const plat_profile_limits_t *lim,
plat_profile_val_result_t *out);
/* Extract validated numeric limits from a frozen profile (P02-076).
* Returns -1 if ext is NULL or not frozen.
*/
int plat_profile_get_limits(const plat_profile_ext_t *ext,
plat_profile_limits_t *out);
/* P02-056: fill absent optional fields with their defaults.
* Defaults are chosen so they never weaken a mandatory rule: a MIL profile
* gets audit.required=1 and modver=REQUIRED, a CIVIL profile keeps audit
* optional but still gets a finite watchdog once any watchdog field is set.
* Mandatory fields (limits, kind) are never invented — an absent limit
* stays zero and is refused by P02-077. Refuses on a frozen descriptor. */
plat_profval_err_t plat_profile_apply_defaults(plat_profile_ext_t *ext,
plat_profile_val_result_t *out);
/* P02-063: record that a mandatory field was present on the wire.
* Returns PLAT_PROFVAL_DUPLICATE if the same field was already recorded,
* independently of the order fields arrived in. */
plat_profval_err_t plat_profile_ext_note_field(plat_profile_ext_t *ext,
plat_proffield_t f,
plat_profile_val_result_t *out);
/* Stable name of a validation code. Never contains profile bytes, key
* material or digests — a refusal names the phase, not the secret (P02-058). */
const char *plat_profval_err_name(plat_profval_err_t code);
/* ── Immutable profile snapshot (P02-055/073/074/075) ─────────────────── */
#define PLAT_PROFILE_DIGEST_LEN 32
/* One immutable instance of the selected policy. Published exactly once
* per boot by plat_profile_snapshot_publish(); every consumer reads the
* same bytes through plat_profile_snapshot_get(). */
typedef struct plat_profile_snapshot {
char id[PLAT_PROFILE_ID_MAX]; /* profile identity */
uint32_t revision; /* profile revision */
uint8_t digest[PLAT_PROFILE_DIGEST_LEN]; /* authenticated */
uint64_t boot_gen; /* P02-073 boot gen */
plat_profile_kind_t kind;
plat_profile_limits_t limits;
/* P02-078/A1-P04-184: watchdog-политика входит в снимок, потому что
* «сколько молчания считается слишком долгим» — решение профиля, а не
* константа в коде подсистемы задач. Пока её здесь не было, plat_task.c
* пользовался зашитым «2× heartbeat_sec»: числом, которого никто не
* утверждал и которое нельзя изменить, не пересобрав ядро. */
plat_watchdog_policy_t watchdog;
int published; /* non-zero = live */
} plat_profile_snapshot_t;
/* P02-055/073: publish the one immutable snapshot for this boot.
* The digest is copied in, not referenced, so a later mutation of the
* caller's buffer cannot change runtime policy (P02-071).
* Refuses with PLAT_PROFVAL_FROZEN if a snapshot is already published for
* this boot generation — a second publish would be a policy swap without
* a new boot context (P02-075). */
plat_profval_err_t plat_profile_snapshot_publish(
const plat_profile_ext_t *ext,
const char *id,
uint32_t revision,
const uint8_t digest[PLAT_PROFILE_DIGEST_LEN],
uint64_t boot_gen,
plat_profile_val_result_t *out);
/* Read the published snapshot. Returns 0 on success, -1 if none published.
* The caller receives a copy: there is no writable alias to live policy. */
int plat_profile_snapshot_get(plat_profile_snapshot_t *out);
/* P02-074: the CLI/diagnostic path. Returns a one-line status containing
* the profile id, revision, boot generation and the first 8 hex digits of
* the digest. Never prints key material. Returns the number of bytes that
* would be written (snprintf semantics), or -1 if nothing is published. */
int plat_profile_snapshot_status(char *buf, size_t buflen);
/* P02-074/075: any attempt to install a different policy after a snapshot
* is live is refused. Returns PLAT_PROFVAL_FROZEN and leaves the active
* digest and limits untouched. Exposed so the CLI and the API setter share
* one refusal, rather than each inventing its own. */
plat_profval_err_t plat_profile_snapshot_replace_attempt(
plat_profile_val_result_t *out);
/* Test-only: drop the published snapshot so a fixture can simulate a new
* boot context. Never called on a production startup path. */
void plat_profile_snapshot_reset_for_test(void);
include/platx/profile_wire.h
/* platx/profile_wire.h — A1-P02: portable load-time profile codec.
*
* Two things live on the wire and they are NOT the same thing (P02-053):
*
* build composition what this binary actually contains. Fixed at link
* time, discoverable, never read from a file.
* load-time policy what the operator asks this binary to enforce.
* Read from the profile blob, authenticated, then
* frozen.
*
* A profile that requests a provider the binary does not contain is a
* composition error and is refused before any module initialises — it is
* not a policy that "degrades". plat_profile_wire_bind() is where the two
* meet, and it is the only place they are allowed to meet.
*
* ── Base image [FROZEN at 120 bytes by C31] ──────────────────────────────
* Byte-for-byte the layout platx-windows/win/plat/platx_profile.h freezes.
* That file is A3's; this is the portable Linux reader for the same bytes,
* so a vector produced on either side decodes identically (P02-052/059).
* All integers are big-endian. Offsets are absolute from byte 0.
*
* off size field
* 0 4 magic 0x504C5458
* 4 4 version 1
* 8 4 flags PLAT_PROFW_FLAG_*
* 12 4 threat_level 0..4
* 16 4 max_leases
* 20 4 max_tasks
* 24 4 max_events
* 28 4 audit_mask
* 32 4 lease_ttl seconds
* 36 4 task_ttl seconds
* 40 8 padding must be zero
* 48 32 platform_id
* 80 8 reserved2 must be zero
* 88 32 hmac authenticates bytes [0, 88)
* 120 end of base image
*
* ── Versioned extension area (P02-054) ───────────────────────────────────
* New policy is added AFTER byte 120 as TLV sections, never by re-reading
* a legacy offset with a new meaning. A decoder that does not know a
* section skips it — unless the section is marked critical, in which case
* the profile is refused (P02-064). Section header, big-endian:
*
* 0 2 type PLAT_PROFW_SEC_*
* 2 2 sflags bit 0 = critical
* 4 4 length payload bytes that follow this 8-byte header
*
* ── Authentication span (P02-065) ────────────────────────────────────────
* The verifier is bound to the exact bytes the semantic parser accepted.
* plat_profile_wire_t.auth_len records that span. Nothing is normalised,
* re-encoded, trimmed or case-folded before verification: the decoder
* reads out of the caller's buffer and never writes back into it.
*/
/* ── Base image constants [FROZEN — mirror of A3's platx_profile.h] ────── */
#define PLAT_PROFW_MAGIC 0x504C5458u
#define PLAT_PROFW_VERSION 1u
#define PLAT_PROFW_SIZE 120u
#define PLAT_PROFW_HMAC_OFF 88u
#define PLAT_PROFW_HMAC_LEN 32u
#define PLAT_PROFW_PLATID_OFF 48u
#define PLAT_PROFW_PLATID_LEN 32u
#define PLAT_PROFW_PAD_OFF 40u
#define PLAT_PROFW_PAD_LEN 8u
#define PLAT_PROFW_RSV2_OFF 80u
#define PLAT_PROFW_RSV2_LEN 8u
/* Flags. Any bit outside the known mask is an unknown mandatory semantic
* and is refused with PLAT_PROFVAL_CRITICAL (P02-064). */
#define PLAT_PROFW_FLAG_MIL_MODE 0x0001u
#define PLAT_PROFW_FLAG_AUDIT_ALL 0x0002u
#define PLAT_PROFW_FLAG_NO_NETWORK 0x0004u
#define PLAT_PROFW_FLAG_LOCKDOWN_FS 0x0008u
#define PLAT_PROFW_FLAG_SIGNED_ONLY 0x0010u
#define PLAT_PROFW_FLAG_NO_EXEC 0x0020u
#define PLAT_PROFW_FLAG_COVERT 0x0040u
#define PLAT_PROFW_FLAG_DAL_B 0x0080u
#define PLAT_PROFW_FLAG_GOST 0x0100u
#define PLAT_PROFW_FLAG_MIL_STD882 0x0200u
#define PLAT_PROFW_FLAGS_KNOWN_MASK 0x03FFu
/* Range contract, agreed with A3 in platx_profile.h (P01-022). */
#define PLAT_PROFW_LIMIT_MIN 1u
#define PLAT_PROFW_LIMIT_MAX 65535u
#define PLAT_PROFW_TTL_MIN_S 1u
#define PLAT_PROFW_TTL_MAX_S 86400u
#define PLAT_PROFW_THREAT_MAX 4u
/* ── Extension sections (P02-054/062/063/064) ─────────────────────────── */
#define PLAT_PROFW_SECHDR_LEN 8u
#define PLAT_PROFW_SEC_CRITICAL 0x0001u /* sflags bit 0 */
typedef enum {
PLAT_PROFW_SEC_WATCHDOG = 1, /* 12 bytes: interval, strikes, ceil */
PLAT_PROFW_SEC_AUDIT = 2, /* 8 bytes: required, min_retention_s */
PLAT_PROFW_SEC_TRUST = 3, /* 24 bytes: trust_gen, issued, expires */
PLAT_PROFW_SEC_MODVER = 4, /* 4 bytes: plat_modver_policy_t */
PLAT_PROFW_SEC_END = 0xFFFF, /* 0 bytes: mandatory terminator */
} plat_profw_sec_t;
/* ── Why the extension area needs a terminator (P02-061) ──────────────────
* The base image is frozen at 120 bytes and has no room for a total
* length, so nothing in it states how many sections should follow. That
* makes a blob truncated exactly on a section boundary a well-formed
* SHORTER blob — and dropping the trailing sections is a policy DOWNGRADE,
* not a corruption: cutting the TRUST section off a MIL profile leaves
* trust_gen == 0, which switches off both the trust-generation check
* (P02-066) and the mandatory-verifier check (P02-068).
*
* This was found by the truncation sweep in test_profile_wire.c, which
* accepted a 148-byte prefix of a 180-byte MIL vector.
*
* So: when an extension area is present it MUST end with a critical,
* zero-length PLAT_PROFW_SEC_END. Truncating at any section boundary
* removes the terminator and the blob is refused with BOUNDS. The
* terminator is inside the authenticated span, so it cannot be re-added
* by anyone who cannot re-sign the profile.
*/
/* Extension area limits. A blob larger than this is refused outright
* rather than parsed, so a hostile length field can never drive the
* decoder past the caller's buffer (P02-061/062). */
#define PLAT_PROFW_EXT_MAX 4096u
#define PLAT_PROFW_BLOB_MAX (PLAT_PROFW_SIZE + PLAT_PROFW_EXT_MAX)
/* ── Decoded image (host byte order) ──────────────────────────────────── */
typedef struct plat_profile_wire {
uint32_t magic;
uint32_t version;
uint32_t flags;
uint32_t threat_level;
uint32_t max_leases;
uint32_t max_tasks;
uint32_t max_events;
uint32_t audit_mask;
uint32_t lease_ttl;
uint32_t task_ttl;
uint8_t platform_id[PLAT_PROFW_PLATID_LEN];
uint8_t hmac[PLAT_PROFW_HMAC_LEN];
/* P02-065: the exact byte span the verifier must authenticate. It is
* an offset into the CALLER's buffer; the decoder copies nothing back
* and normalises nothing, so verify(buf, auth_len) and the semantic
* result below are derived from the same bytes. */
size_t auth_len;
/* Policy assembled from base flags + extension sections. */
plat_profile_ext_t ext;
} plat_profile_wire_t;
/* Decode and semantically check a profile blob.
*
* len must be exactly PLAT_PROFW_SIZE, or PLAT_PROFW_SIZE plus a
* well-formed extension area. Every shorter prefix is refused with
* PLAT_PROFVAL_BOUNDS and no byte past len is read (P02-061).
*
* Returns PLAT_PROFVAL_OK, or the code naming the phase that refused.
* out is always populated when non-NULL; *w is only meaningful on OK.
*/
plat_profval_err_t plat_profile_wire_decode(const uint8_t *buf,
size_t len,
plat_profile_wire_t *w,
plat_profile_val_result_t *out);
/* ── Build composition vs load-time policy (P02-053) ──────────────────── */
/* What this binary contains. Set at link time by the composition unit;
* a profile can only ever ask for a subset of these. */
typedef struct plat_profile_composition {
uint32_t providers; /* PLAT_PROFW_PROV_* bitmap */
int has_verifier; /* module verification provider linked */
int has_audit_sink; /* audit sink provider linked */
int has_watchdog; /* watchdog provider linked */
} plat_profile_composition_t;
#define PLAT_PROFW_PROV_AUDIT 0x0001u
#define PLAT_PROFW_PROV_WATCHDOG 0x0002u
#define PLAT_PROFW_PROV_MODVER 0x0004u
#define PLAT_PROFW_PROV_NETWORK 0x0008u
/* Reject a profile that asks for a provider this binary does not contain.
* This runs BEFORE module initialisation, so a missing provider is a
* refused startup and never a silently reduced policy (P02-053/057).
* Returns PLAT_PROFVAL_OK or PLAT_PROFVAL_SEMANTICS. */
plat_profval_err_t plat_profile_wire_bind(
const plat_profile_wire_t *w,
const plat_profile_composition_t *comp,
plat_profile_val_result_t *out);
/* The composition of the binary the caller is linked into. Weak: a unit
* test that does not link a composition unit gets the empty composition,
* which refuses every provider request rather than allowing it. */
const plat_profile_composition_t *plat_profile_composition(void);
include/platx/provgen.h
/* provgen.h — S479: смена поколения провайдера ПРИ ЖИВЫХ СЕССИЯХ.
*
* Заменить транспорт или провайдера на работающем узле — это не «переставить
* указатель». В момент перестановки есть сессии, которые уже начались на
* прежнем поколении, и у каждой из них своё представление о том, с кем она
* разговаривает.
*
* Отсюда четыре правила, и каждое закрывает свой способ потерять данные.
*
* 1. СЕССИЯ НЕ ПЕРЕЕЗЖАЕТ. Начавшаяся на поколении N доживает на N. Перевести
* её на N+1 посередине значит поменять под ней состояние, о котором она
* ничего не знает: буферы, ключи, счётчики — всё чужое.
*
* 2. ПРЕЖНЕЕ ПОКОЛЕНИЕ ОБСЛУЖИВАЕТ ДО КОНЦА. Оно снимается не тогда, когда
* пришло новое, а когда ушла ЕГО ПОСЛЕДНЯЯ сессия. Иначе «замена без
* простоя» означает обрыв всех текущих соединений.
*
* 3. ОТКАЗ НИЧЕГО НЕ МЕНЯЕТ. Если новое поколение не поднялось, старое
* остаётся текущим — и не «возвращается назад», а просто никогда не
* переставало им быть. Разница видна при отказе на середине: возвращать
* некуда, потому что не уходили.
*
* 4. НОВЫЕ СЕССИИ ИДУТ НА НОВОЕ ТОЛЬКО ПОСЛЕ ФИКСАЦИИ. Пока идёт подготовка,
* новое поколение не принимает никого: сессия, начатая на непринятом
* поколении, при откате остаётся сиротой.
*/
#define PLAT_PROVGEN_MAX 4 /* текущее + отставные с недошедшими сессиями */
typedef enum {
PLAT_PG_EMPTY = 0,
PLAT_PG_STAGING = 1, /* готовится, никого не принимает */
PLAT_PG_CURRENT = 2, /* принимает новые сессии */
PLAT_PG_DRAINING = 3 /* новых не принимает, старые доживают */
} plat_pg_state_t;
typedef struct {
uint64_t gen;
plat_pg_state_t state;
uint32_t sessions; /* сколько живых сессий на этом поколении */
} plat_pg_slot_t;
typedef struct {
plat_pg_slot_t slot[PLAT_PROVGEN_MAX];
uint64_t next_gen;
} plat_provgen_t;
typedef enum {
PLAT_PG_OK = 0,
PLAT_PG_BUSY = 1, /* нет места: слишком много непросохших поколений */
PLAT_PG_NO_STAGED = 2,
PLAT_PG_REFUSED = 3, /* подготовка не прошла — текущее не тронуто */
PLAT_PG_NO_CURRENT= 4,
PLAT_PG_BAD_ARG = 5
} plat_pg_verdict_t;
/* Проверка поднявшегося поколения перед фиксацией. 0 — годно. */
typedef int (*plat_pg_probe_fn)(uint64_t gen, void *ud);
void plat_provgen_init(plat_provgen_t *p);
/* Завести новое поколение в состоянии STAGING. Текущее продолжает работать. */
plat_pg_verdict_t plat_provgen_stage(plat_provgen_t *p, uint64_t *out_gen);
/* Проверить и зафиксировать. При отказе пробы STAGING снимается, текущее
* НЕ ТРОГАЕТСЯ (правило 3), живые сессии не задеты. */
plat_pg_verdict_t plat_provgen_commit(plat_provgen_t *p,
plat_pg_probe_fn probe, void *ud);
/* Открыть сессию. Всегда на CURRENT — и только на нём (правило 4).
* Возвращает поколение сессии в *gen. */
plat_pg_verdict_t plat_provgen_open(plat_provgen_t *p, uint64_t *gen);
/* Закрыть сессию своего поколения. Опустевшее DRAINING освобождается. */
plat_pg_verdict_t plat_provgen_close(plat_provgen_t *p, uint64_t gen);
uint64_t plat_provgen_current(const plat_provgen_t *p);
uint32_t plat_provgen_sessions(const plat_provgen_t *p, uint64_t gen);
int plat_provgen_alive(const plat_provgen_t *p, uint64_t gen);
size_t plat_provgen_live_gens(const plat_provgen_t *p);
const char *plat_pg_verdict_name(plat_pg_verdict_t v);
include/platx/reclog.h
/* reclog.h — S402/S416: кадрированный журнал записей.
*
* ФОРМАТ
* ──────
* Заголовок файла (16 байт):
* magic uint32 PLAT_RECLOG_MAGIC
* version uint16 версия формата
* hdr_len uint16 длина ЭТОГО заголовка
* reserved uint64 обязан быть нулём
*
* Запись:
* len uint32 длина полезной нагрузки
* crc uint32 CRC-32 от (len || payload)
* payload len байт
*
* Почему CRC накрывает ДЛИНУ, а не только полезную нагрузку: испорченная длина
* иначе неотличима от правильной, и читатель разложил бы по ней весь остаток
* файла — одна перевёрнутая ячейка сместила бы границы всех последующих
* записей, и каждая из них прошла бы как «целая». Длина под CRC делает
* рассинхронизацию видимой на первой же записи.
*
* hdr_len отдельно от version: читатель обязан уметь ПРОПУСТИТЬ заголовок
* большей длины, чем знает. Без этого поля добавление поля в заголовок —
* несовместимое изменение, а с ним — совместимое.
*
* ЧТО ЗНАЧИТ «ВОССТАНОВИТЬ»
* ─────────────────────────
* Читатель возвращает САМЫЙ ДЛИННЫЙ ПРОВЕРЯЕМЫЙ ПРЕФИКС и останавливается на
* первой записи, которая обрезана или не сходится по CRC. Он НЕ пропускает
* плохую запись, чтобы продолжить дальше: за обрывом лежат байты, о которых
* ничего не известно, и принять их за записи значило бы принять подделанный
* хвост. Пропуск с продолжением — именно тот способ, которым журнал
* «восстанавливается» в чужие данные.
*
* Отказ на первой же записи — это префикс нулевой длины, а не ошибка чтения:
* пустой проверяемый префикс тоже ответ.
*/
#define PLAT_RECLOG_MAGIC 0x50584C47u /* "PXLG" */
#define PLAT_RECLOG_VERSION 1u
#define PLAT_RECLOG_HDR_LEN 16u
#define PLAT_RECLOG_MAX_REC (16u * 1024u * 1024u)
/* Почему обрыв. Значение отличает «файл кончился» от «байты не сходятся»:
* первое — обычный след краха, второе — повод не доверять источнику. */
typedef enum {
PLAT_RECLOG_END_CLEAN = 0, /* дочитан до конца, всё сошлось */
PLAT_RECLOG_END_TRUNCATED = 1, /* файл обрывается посреди записи */
PLAT_RECLOG_END_BADCRC = 2, /* запись на месте, но байты не сходятся*/
PLAT_RECLOG_END_BADLEN = 3 /* длина невозможна (0 или > максимума) */
} plat_reclog_end_t;
typedef struct {
uint64_t records; /* сколько записей проверено */
uint64_t good_bytes; /* смещение конца проверяемого префикса */
plat_reclog_end_t end;
} plat_reclog_scan_t;
struct xio_t;
/* Создать пустой журнал (заголовок файла). 0 / -errno. */
int plat_reclog_create(const struct xio_t *io, const char *path, int mode);
/* Дописать запись и сделать её долговечной. 0 / -errno.
* Заголовок записи и нагрузка пишутся ОДНИМ вызовом: два вызова оставляли бы
* после краха заголовок без нагрузки заметно чаще, чем это необходимо. */
int plat_reclog_append(const struct xio_t *io, const char *path,
const void *rec, size_t len);
/* Пройти журнал. Возвращает 0, если файл открыт и заголовок принят,
* -errno иначе (в том числе при чужой magic, старшей версии и ненулевом
* reserved). Результат обхода — в out; end говорит, чем кончился префикс.
*
* cb вызывается на каждую ПРОВЕРЕННУЮ запись; NULL допустим, если нужен
* только размер префикса. */
int plat_reclog_scan(const struct xio_t *io, const char *path,
void (*cb)(const void *rec, size_t len, void *ud),
void *ud, plat_reclog_scan_t *out);
/* Причина карантина. Отличать обязательно: обрыв — обычный след краха, и
* усечь его до проверенного префикса законно; несошедшиеся байты и чужая
* версия — чужие данные, и трогать их нельзя. */
typedef enum {
PLAT_RECLOG_Q_NONE = 0, /* нечего делать */
PLAT_RECLOG_Q_TRIMMED = 1, /* хвост отрезан до проверенного префикса */
PLAT_RECLOG_Q_SET_ASIDE = 2 /* файл отложен целиком, содержимое цело */
} plat_reclog_quar_t;
/* Привести журнал в состояние, на которое можно опираться.
*
* обрыв (TRUNCATED) → усечь до проверенного префикса. Отрезаются
* только байты недописанной записи: они не
* содержат ничьего решения.
* несходство (BADCRC/BADLEN) → отложить ВЕСЬ файл под ограниченным именем и
* с правами 0600, на его месте создать пустой
* журнал. Хвост может быть подделан, а может
* быть уликой — ни то ни другое не удаляют.
* чужая версия / magic → отложить, НЕ ТРОГАЯ НИ БАЙТА (S430): формат
* старше нашего понимания читается откатом или
* человеком, и «починить» его мы не в праве.
*
* Имя карантина ограничено: путь + ".quar" + номер, не более 32 попыток,
* после чего честный отказ. Неограниченный перебор на повреждённом входе —
* это вечный цикл, в который систему и загоняют (S429).
*
* Повторный вызов на уже приведённом журнале ничего не делает и возвращает
* PLAT_RECLOG_Q_NONE: приведение обязано быть идемпотентным, потому что
* перезапуск после краха может случиться и во время него самого (S426).
*
* 0 / -errno. Что именно сделано — в *what. */
int plat_reclog_quarantine(const struct xio_t *io, const char *path,
plat_reclog_quar_t *what, char *quar_path,
size_t quar_path_sz);
/* CRC-32 (IEEE 802.3), вынесен ради проверок. */
uint32_t plat_reclog_crc32(uint32_t seed, const void *data, size_t len);
include/platx/recovery.h
/* platx/recovery.h — recovery decides; lifecycle alone executes.
* This file must never call descriptor hooks.
*/
typedef enum plat_recovery_event {
PLAT_REC_DEGRADED = 1,
PLAT_REC_FATAL, /* health fatal / task ERROR */
PLAT_REC_CRASH, /* abort / unexpected death */
PLAT_REC_KILLED, /* intentional kill */
PLAT_REC_EXITED /* clean DONE */
} plat_recovery_event_t;
typedef enum plat_recovery_policy {
PLAT_REC_NEVER = 0,
PLAT_REC_ON_FAILURE,
PLAT_REC_ON_ABNORMAL,
PLAT_REC_ALWAYS
} plat_recovery_policy_t;
typedef enum plat_recovery_status {
PLAT_REC_ST_OK = 0,
PLAT_REC_ST_BACKOFF,
PLAT_REC_ST_FATAL
} plat_recovery_status_t;
typedef enum plat_recovery_action {
PLAT_REC_ACT_NONE = 0,
PLAT_REC_ACT_DEGRADE,
PLAT_REC_ACT_RESTART,
PLAT_REC_ACT_FAIL
} plat_recovery_action_t;
typedef struct plat_recovery_opts {
plat_recovery_policy_t policy; /* default ON_FAILURE */
int max_restarts; /* 0 → 5 */
int window_sec; /* 0 → 60 */
int backoff_start; /* 0 → 1 */
int backoff_max; /* 0 → 300 */
} plat_recovery_opts_t;
typedef struct plat_recovery {
plat_recovery_opts_t opts;
plat_recovery_status_t status;
plat_recovery_action_t last_action;
int restart_count;
time_t window_start;
time_t next_allowed;
int backoff_sec;
plat_lifecycle_t *lm; /* observed, not owned; may be NULL */
} plat_recovery_t;
int plat_recovery_init(plat_recovery_t *r, plat_lifecycle_t *lm,
const plat_recovery_opts_t *opts);
/* Policy + window + backoff. No lifecycle call. */
plat_recovery_action_t plat_recovery_choose(plat_recovery_t *r,
plat_recovery_event_t ev,
time_t now);
/* choose() + plat_lifecycle_request(). Exhaustion → FAIL, not another RESTART. */
int plat_recovery_apply(plat_recovery_t *r, plat_recovery_event_t ev);
int plat_recovery_apply_at(plat_recovery_t *r, plat_recovery_event_t ev,
time_t now);
/* Default ON_FAILURE, one-shot state. Existing tests / simple callers. */
int plat_recovery_decide(plat_lifecycle_t *lm, plat_recovery_event_t ev);
/* Watch table keyed by plat_owner_t. r is caller-owned and must outlive
* unwatch *and* a concurrent tick (unwatch waits for in-flight tick).
* generation 0 is rejected. Stale generation does not match. */
int plat_recovery_watch(plat_recovery_t *r, plat_lifecycle_t *lm,
plat_owner_t owner, const plat_recovery_opts_t *opts);
int plat_recovery_unwatch(plat_owner_t owner);
/* Same instance, new generation. Does not re-init r (budget stays). */
int plat_recovery_retarget(plat_owner_t from, plat_owner_t to);
/* health() + task silence > 2×heartbeat_sec → apply_at. Never start/stop. */
void plat_recovery_tick(time_t now);
/* Live watch row. r is the pointer passed to watch; name is desc->name. */
typedef struct plat_recovery_snap {
plat_owner_t owner;
plat_recovery_t *r;
plat_lifecycle_t *lm;
const char *name;
} plat_recovery_snap_t;
/* Copy the watch table. out/max invalid → 0. No mbus, no JSON. */
int plat_recovery_snapshot(plat_recovery_snap_t *out, int max);
/* Fact after apply. Stack-copied into PLAT_EV_RECOVERY. No pointers. */
typedef struct plat_recovery_fact {
plat_recovery_action_t action; /* RESTART, or FAIL when budget is gone */
uint32_t id; /* module_id if lm has owner, else 0 */
int restart_count;
} plat_recovery_fact_t;
include/platx/replay.h
/* replay.h — S438/S440: окно антиповтора и ровно одна личность.
*
* S438 Окно приёма хранит границу принятого и битовую карту недавних. После
* перезапуска оно НЕ имеет права начаться с нуля: нулевое окно принимает
* заново всё, что уже было принято, — то есть перезапуск открывает
* повтор. Если состояние не читается, окно ПЕРЕСОГЛАСУЕТСЯ: граница
* поднимается так, что старое не проходит. Отказ идёт в сторону, которая
* не принимает лишнего.
*
* S440 Личность меняется целиком. В любой момент действует РОВНО ОДНА
* генерация: незавершённая смена не оставляет двух действующих и не
* оставляет ни одной.
*/
struct xio_t;
#define PLAT_RWIN_BITS 64
typedef struct {
uint64_t high; /* наибольший принятый номер */
uint64_t mask; /* битовая карта: high-1 .. high-64 */
int renegotiated; /* состояние не прочлось — окно поднято */
} plat_rwin_t;
typedef enum {
PLAT_RWIN_ACCEPT = 0,
PLAT_RWIN_REPLAY = 1, /* уже принимали */
PLAT_RWIN_TOO_OLD = 2 /* вышло за окно — судить не можем */
} plat_rwin_verdict_t;
/* Загрузить окно. Если файла нет или он нечитаем, окно ПЕРЕСОГЛАСОВАНО:
* high поднимается до floor, и всё ниже отвергается. */
int plat_rwin_load(plat_rwin_t *w, const struct xio_t *io, const char *path,
uint64_t floor);
plat_rwin_verdict_t plat_rwin_check(const plat_rwin_t *w, uint64_t seq);
/* Принять номер и сохранить состояние долговечно. 0 / -errno / -EEXIST. */
int plat_rwin_accept(plat_rwin_t *w, const struct xio_t *io, const char *path,
uint64_t seq);
/* ── S440: смена личности ────────────────────────────────────────────────── */
typedef struct {
uint64_t generation;
char id[64];
} plat_identity_t;
/* Опубликовать новую личность. Атомарно: либо старая, либо новая. */
int plat_identity_rotate(const struct xio_t *io, const char *base,
const plat_identity_t *next);
/* Действующая личность. -ENOENT, если её нет. */
int plat_identity_active(const struct xio_t *io, const char *base,
plat_identity_t *out);
/* ── S441: аутентификация не переживает перезапуск ───────────────────────
*
* Сессия помнит, что была аутентифицирована. Это знание привязано к ЭПОХЕ
* ЗАГРУЗКИ и восстановлению не подлежит: после перезапуска ключи сессии в
* памяти утрачены, состояние сопряжения с той стороной неизвестно, а сама та
* сторона могла смениться. Поднять сохранённую сессию сразу в AUTHENTICATED
* значит пустить данные по каналу, которого никто заново не подтверждал.
*
* Поэтому загруженная сессия оказывается максимум в NEEDS_AUTH, и передача
* данных отвергается до нового подтверждения — даже если на диске записано
* AUTHENTICATED и с момента записи прошла секунда.
*/
typedef enum {
PLAT_SESS_NEW = 0,
PLAT_SESS_NEEDS_AUTH = 1,
PLAT_SESS_AUTHENTICATED = 2
} plat_sess_state_t;
typedef struct {
char epoch_id[64];
plat_sess_state_t state;
uint64_t peer;
} plat_session_t;
int plat_session_open(plat_session_t *s, uint64_t peer);
int plat_session_authenticate(plat_session_t *s);
/* Состояние сессии ПРЯМО СЕЙЧАС, с учётом эпохи. */
plat_sess_state_t plat_session_state(const plat_session_t *s);
/* Можно ли передавать данные. 0 / -EACCES. */
int plat_session_may_send(const plat_session_t *s);
/* Сохранить и загрузить. Загрузка НИКОГДА не даёт AUTHENTICATED. */
int plat_session_save(const struct xio_t *io, const char *path,
const plat_session_t *s);
int plat_session_load(const struct xio_t *io, const char *path,
plat_session_t *out);
include/platx/resilience.h
/* resilience.h — S436/S444/S445: пределы, размыкатель и приоритет под давлением.
*
* S436 Ротация резервных копий ограничена И числом, И объёмом — но последняя
* ПРОВЕРЕННАЯ точка восстановления не удаляется никогда, даже если она
* одна нарушает оба предела. Предел, ради соблюдения которого стирают
* единственную возможность восстановиться, защищает диск от системы, а
* не систему.
*
* S444 Размыкатель цикла падений. Счётчик попыток переживает перезапуск;
* после предела автоматическое восстановление ПРЕКРАЩАЕТСЯ и остаётся
* видимое оператору состояние отказа. Бесконечный автоперезапуск — это
* не устойчивость, а способ скрыть неустранимую поломку и сжечь диск
* журналами.
*
* S445 Под нехваткой места первым отказывают отладочные трассы и
* необязательная телеметрия, а состояние безопасности и целостность
* аудита пишутся до последнего. Порядок отказа — это решение, и
* принимать его надо заранее, а не тем, чей write первым вернул ENOSPC.
*/
struct xio_t;
/* ── S436: ротация ───────────────────────────────────────────────────────── */
typedef struct {
size_t kept;
size_t removed;
size_t protected_verified; /* сохранено вопреки пределам */
uint64_t bytes_kept;
} plat_retain_report_t;
/* Проверенной считается копия, рядом с которой лежит <имя>.verified.
* Ротация удаляет самые старые, пока не уложится в пределы, и НИКОГДА не
* удаляет последнюю проверенную. 0 / -errno. */
int plat_retain_rotate(const struct xio_t *io, const char *dir,
const char *prefix, size_t max_count,
uint64_t max_bytes, plat_retain_report_t *out);
/* ── S444: размыкатель ───────────────────────────────────────────────────── */
typedef enum {
PLAT_BREAKER_CLOSED = 0, /* попытки разрешены */
PLAT_BREAKER_OPEN = 1 /* предел исчерпан: автовосстановление стоит */
} plat_breaker_state_t;
/* Отметить начало рискованной попытки. Возвращает состояние ПОСЛЕ отметки:
* OPEN означает, что пытаться больше нельзя и нужен оператор. */
plat_breaker_state_t plat_breaker_attempt(const struct xio_t *io,
const char *path, uint32_t limit);
/* Отметить успех: счётчик обнуляется. */
int plat_breaker_success(const struct xio_t *io, const char *path);
/* Сколько попыток записано. -errno при ошибке чтения. */
long plat_breaker_count(const struct xio_t *io, const char *path);
/* ── S445: приоритет под давлением ───────────────────────────────────────── */
typedef enum {
PLAT_CLASS_SECURITY = 0, /* состояние безопасности — последним */
PLAT_CLASS_AUDIT = 1,
PLAT_CLASS_OPERATION = 2,
PLAT_CLASS_TRACE = 3,
PLAT_CLASS_TELEMETRY = 4 /* необязательное — первым */
} plat_write_class_t;
/* 1 — писать разрешено, 0 — отказано заранее.
* pressure: 0 нет давления, 1 мало места, 2 критично. */
int plat_write_allowed(plat_write_class_t cls, int pressure);
include/platx/resource.h
/* platx/resource.h — ownership with instance + generation. */
typedef enum plat_res_type {
PLAT_RES_FD = 1,
PLAT_RES_THREAD,
PLAT_RES_SOCKET,
PLAT_RES_MEMFD,
PLAT_RES_TIMER,
PLAT_RES_EVENT_SUB,
PLAT_RES_SERVICE,
PLAT_RES_CHILD
} plat_res_type_t;
typedef struct plat_owner {
uint32_t module_id;
uint32_t instance_id;
uint32_t generation; /* increments on each recreate; stale cbs die */
} plat_owner_t;
typedef uint64_t plat_res_id_t;
typedef struct plat_resource_api {
plat_res_id_t (*own)(plat_owner_t owner, plat_res_type_t type,
intptr_t handle, const char *name);
int (*release)(plat_res_id_t id);
int (*release_owner)(plat_owner_t owner); /* sweep; gen 0 no-op */
int (*check)(plat_res_id_t id, plat_owner_t expected);
} plat_resource_api_t;
int plat_res_init(void);
void plat_res_fini(void);
extern const plat_resource_api_t plat_res_api;
include/platx/resource_owner.h
/* platx/resource_owner.h — A1-P04: владение ресурсом с настоящим освобождением.
*
* ЗАЧЕМ ЭТОТ ЗАГОЛОВОК СУЩЕСТВУЕТ
*
* `platx/resource.h` описывает таблицу владения: кто чем владеет. Ровно это
* она и делает — и ничего больше. `plat_res_api.release()` обнуляет строку
* таблицы; дескриптор, поток, memfd или сокет, ради которых строка заводилась,
* не закрываются никогда. Учёт был честным, освобождение — отсутствовало.
* Формулировка «ownership без остатков» до этого файла означала «без остатков
* в таблице», а не «без остатков в системе»; разница и есть весь P04.
*
* Здесь у каждого kind появляется releaser: функция, которая действительно
* закрывает handle, и результат которой сохраняется. Отказавшийся releaser не
* исчезает и не превращается в успех — слот остаётся в состоянии
* RELEASE_FAILED и виден в snapshot до конца жизни процесса или до явного
* повторного вызова. Отчёт о нуле остатков теперь может быть неверным только
* если врёт сам OS, а не потому, что мы не пытались.
*
* ЧТО ЗДЕСЬ НЕ МЕНЯЕТСЯ
*
* Опубликованный `plat_resource_api_t` из platx/resource.h остаётся байт в
* байт тем же: он входит в plat_abi_t, а тот заморожен A1-CONTRACT. Старый
* API становится тонкой обёрткой над этим ядром и получает освобождение
* бесплатно. Новые возможности — токены, счётчики, snapshot, deadline —
* живут только здесь и не сдвигают ни одного offset в ABI.
*
* МОДЕЛЬ СОСТОЯНИЙ
*
* FREE слот свободен.
* RESERVED claim прошёл, publish ещё нет. Ёмкость занята, но снаружи
* ресурса не существует: snapshot его не показывает, sweep его
* не трогает, releaser по нему не зовётся.
* LIVE опубликован. Единственное состояние, в котором ресурс можно
* использовать.
* REVOKED использование запрещено, handle ещё принадлежит нам. Это
* логический запрет, он мгновенен и не ждёт медленного close().
* DRAINING releaser выполняется прямо сейчас, вне таблицы и вне её лока.
* RELEASE_FAILED releaser вернул ошибку. Слот НЕ освобождается: строка с
* утечкой обязана быть видимой, иначе отчёт «утечек нет» становится
* следствием сокрытия, а не проверки.
*
* FREE → RESERVED → LIVE → REVOKED → DRAINING → FREE
* └──────────────┘ └→ RELEASE_FAILED
*
* Обратных переходов нет. RESERVED уходит в FREE только через abandon.
*
* ЛОК И CALLBACK
*
* Ни один releaser не вызывается под таблицей. Sweep копирует пачку строк под
* локом, помечает их DRAINING, отпускает лок, зовёт releaser'ы и возвращается
* записать исход. Releaser, который сам освобождает другой ресурс, поэтому не
* может замкнуть цикл на нашем локе — а именно так и выглядит естественный
* releaser для составного ресурса.
*
* SPDX-License-Identifier: GPL-2.0
*/
/* ── Ёмкость (A1-P04-152) ────────────────────────────────────────────────
*
* Ёмкость фиксирована на этапе сборки и не растёт в рантайме: таблица
* статическая, §SEC-3 запрещает ядру ходить в кучу ради владения ресурсом.
* Значение обязано быть доступно диагностике вместе с digest профиля —
* «сколько ресурсов помещается» бессмысленно без «в какой политике». Пара
* отдаётся одним вызовом plat_res_capacity_report(), чтобы никто не сложил
* ёмкость этой сборки с digest соседней. */
#define PLAT_RES_MAX 512
#error "PLAT_RES_MAX must fit the 16-bit slot index of plat_res_token_t"
#define PLAT_RES_CAPACITY ((size_t)PLAT_RES_MAX)
/* ── Токен (A1-P04-156/157) ──────────────────────────────────────────────
*
* plat_res_id_t из старого API — просто монотонный счётчик; чтобы найти
* ресурс, приходится линейно сканировать таблицу, а чтобы понять, тот ли это
* ресурс, — сравнивать владельца, которого старый release() даже не
* спрашивает. Токен решает обе задачи сразу:
*
* биты 0..15 индекс слота — адресация за O(1)
* биты 16..63 поколение слота — защита от переиспользования
*
* Поколение слота увеличивается при КАЖДОМ освобождении. Токен, выданный до
* освобождения, после него не совпадёт с поколением слота и не сможет
* закрыть ресурс следующего владельца — сценарий, ради которого карточка 156
* и существует.
*
* 48 бит поколения не переполняются за время жизни процесса при любой
* достижимой частоте, но «недостижимо» не равно «невозможно»: при исчерпании
* слот ВЫВОДИТСЯ ИЗ ОБОРОТА (RETIRED) вместо возврата к поколению 1. Пригодная
* ёмкость при этом уменьшается на единицу, и это наблюдается в
* plat_res_capacity_report(): retired_slots растёт, usable убывает, а
* counters.slots_retired считает событие. Заметный и объяснимый исход, в
* отличие от молчаливого повтора идентичности (A1-P04-157). */
typedef uint64_t plat_res_token_t;
#define PLAT_RES_TOKEN_NONE ((plat_res_token_t)0)
#define PLAT_RES_TOKEN_IDX_BITS 16
#define PLAT_RES_TOKEN_IDX_MASK ((plat_res_token_t)0xFFFFu)
/* 2^48 поколений недостижимы прогоном, и из-за этого ветка RETIRED в
* slot_free() до волны 5 не была проверена ни разу — код существовал, а
* доказательства не было. Предел вынесен в переопределяемую константу
* (A1-P04-157): фикстура собирается с маленьким пределом и исполняет ту же
* производственную ветку, не изменяя ни раскладку токена, ни сам код.
* Продуктовая сборка предел не переопределяет. */
#define PLAT_RES_GEN_MAX (((plat_res_token_t)1 << 48) - 1)
unsigned plat_res_token_slot(plat_res_token_t t); /* inline interface */
uint64_t plat_res_token_gen(plat_res_token_t t); /* inline interface */
/* ── Коды результата ─────────────────────────────────────────────────────
*
* Отдельный код на каждый отказ, а не общий -1. Причина прикладная: вызывающий
* на пути остановки обязан различать «таблица полна» и «владелец запечатан» —
* первое чинится ожиданием, второе не чинится никогда. Старый API возвращал
* 0 и на «не готово», и на «нет места»; отличить их было нечем. */
typedef enum plat_res_err {
PLAT_RES_OK = 0,
PLAT_RES_E_ARGS = -1, /* NULL, generation==0, неизвестный kind */
PLAT_RES_E_NOTREADY = -2, /* plat_res_owner_init() не выполнялся */
PLAT_RES_E_NOSPACE = -3, /* нет свободного слота (152/187) */
PLAT_RES_E_SEALED = -4, /* владелец остановлен, новые claim закрыты */
PLAT_RES_E_STALE = -5, /* токен не совпал с поколением слота (156) */
PLAT_RES_E_NOTFOUND = -6, /* слот свободен */
PLAT_RES_E_STATE = -7, /* переход запрещён из текущего состояния */
PLAT_RES_E_FOREIGN = -8, /* слот принадлежит другому владельцу (154) */
PLAT_RES_E_EXHAUSTED = -9, /* поколения слота исчерпаны (157) */
PLAT_RES_E_RELEASE = -10, /* releaser отказал; строка сохранена (163) */
PLAT_RES_E_TIMEOUT = -11, /* дедлайн истёк, остаток не освобождён (164) */
PLAT_RES_E_RETRY = -12 /* snapshot не сошёлся, повторить (165) */
} plat_res_err_t;
const char *plat_res_err_name(plat_res_err_t e);
/* ── Состояния слота ─────────────────────────────────────────────────── */
typedef enum plat_res_state {
PLAT_RES_ST_FREE = 0,
PLAT_RES_ST_RESERVED = 1,
PLAT_RES_ST_LIVE = 2,
PLAT_RES_ST_REVOKED = 3,
PLAT_RES_ST_DRAINING = 4,
PLAT_RES_ST_RELEASE_FAILED = 5,
PLAT_RES_ST_RETIRED = 6
} plat_res_state_t;
const char *plat_res_state_name(plat_res_state_t s);
/* ── Каталог kind и releaser'ов (A1-P04-151) ─────────────────────────────
*
* Каждый kind обязан иметь releaser. Kind без releaser — это ресурс, который
* учитывается, но не освобождается, то есть ровно тот дефект, который P04
* закрывает; таблица инициализации отвергает такую строку при сборке.
*
* Контракт releaser'а:
* • вызывается ВНЕ таблицы и вне её лока;
* • получает handle и имя, не получает указателей внутрь таблицы;
* • возвращает 0 при освобождении и <0 при отказе; отказ сохраняется;
* • обязан быть идемпотентен относительно уже закрытого handle —
* повторный вызов на закрытом дескрипторе возвращает 0, а не ошибку
* (A1-P04-161: повтор не даёт второго OS close). */
typedef int (*plat_res_releaser_fn)(plat_res_type_t type, intptr_t handle,
const char *name, void *ctx);
typedef struct plat_res_kind_desc {
plat_res_type_t type;
const char *name; /* стабильное имя для диагностики */
plat_res_releaser_fn releaser; /* никогда NULL */
int is_fd; /* handle — файловый дескриптор */
int external; /* освобождается не ядром, а модулем */
} plat_res_kind_desc_t;
/* Каталог целиком: n строк, по одной на каждый PLAT_RES_* тип. Возвращаемая
* таблица неизменяема и живёт всё время процесса. */
const plat_res_kind_desc_t *plat_res_kind_table(size_t *n_out);
const plat_res_kind_desc_t *plat_res_kind_lookup(plat_res_type_t type);
/* Переопределить releaser одного kind. Только для fixtures: production-путь
* пользуется таблицей по умолчанию. Возвращает прежний releaser, чтобы
* фикстура могла его восстановить, и NULL при неизвестном kind. */
plat_res_releaser_fn plat_res_kind_set_releaser(plat_res_type_t type,
plat_res_releaser_fn fn,
void *ctx);
/* ── Счётчики (A1-P04-166) ──────────────────────────────────────────────
*
* Инвариант, ради которого счётчики разделены:
*
* reserved + live + revoked + draining + release_failed + retired
* + free == PLAT_RES_CAPACITY
*
* Одно число «занято» этот инвариант не выражает, и при ошибке releaser'а по
* нему нельзя понять, чем именно занята ёмкость: незавершённой транзакцией,
* запрещённым, но не закрытым ресурсом, или строкой с утечкой. */
typedef struct plat_res_counters {
uint32_t reserved;
uint32_t live;
uint32_t revoked;
uint32_t draining;
uint32_t release_failed;
uint32_t retired;
uint32_t free_slots;
/* Кумулятивные, не сбрасываются: */
uint64_t claims_ok;
uint64_t claims_refused_full;
uint64_t claims_refused_sealed;
uint64_t claims_dedup; /* повтор live-claim, A1-P04-155 */
uint64_t releases_ok;
uint64_t releases_failed;
uint64_t releases_stale; /* токен не совпал, A1-P04-156 */
uint64_t sweeps_timed_out; /* дедлайн, A1-P04-164 */
uint64_t slots_retired; /* исчерпание поколения, A1-P04-157 */
} plat_res_counters_t;
void plat_res_counters_get(plat_res_counters_t *out);
/* Отчёт о ёмкости вместе с digest профиля (A1-P04-152). digest_ok == 0
* означает, что снимок профиля не опубликован: ёмкость известна, политика —
* нет, и такой отчёт нельзя выдавать за профиль-привязанный. */
typedef struct plat_res_capacity_report {
size_t capacity;
size_t in_use;
uint32_t profile_max_tasks;
uint32_t profile_max_xio_owners;
uint8_t profile_digest[32];
int digest_ok;
char profile_id[64];
uint32_t profile_revision;
/* A1-P04-157 (волна 5). До этой правки заголовок обещал, что при
* исчерпании поколения «ёмкость уменьшается на единицу», а
* plat_res_capacity_report() отдавал константу PLAT_RES_CAPACITY: слот
* выводился из оборота, и в отчёте о ёмкости это не было видно ничем.
* Фикстура с уменьшенным PLAT_RES_GEN_MAX показала capacity=512->512 при
* 21 выведенном слоте.
*
* capacity остаётся сконфигурированным максимумом — его смысл не меняется,
* иначе сломались бы читатели, сверяющие его с профилем. Убыль ёмкости
* наблюдается отдельными полями: usable == capacity - retired_slots. */
uint32_t retired_slots; /* слоты, выведенные из оборота навсегда */
size_t usable; /* capacity - retired_slots */
} plat_res_capacity_report_t;
void plat_res_capacity_report(plat_res_capacity_report_t *out);
/* ── Снимок (A1-P04-165) ────────────────────────────────────────────────
*
* Наружу отдаётся копия. Указателя внутрь таблицы не существует ни в одном
* публичном виде: он пережил бы освобождение слота и превратил бы любой
* диагностический обход в чтение освобождённой строки. */
typedef struct plat_res_entry {
plat_res_token_t token;
plat_res_id_t id; /* совместимость со старым API */
plat_res_type_t type;
plat_res_state_t state;
plat_owner_t owner;
intptr_t handle;
int last_release_rc;
char name[48];
} plat_res_entry_t;
/* Копирует до cap строк в out. *n_out — сколько строк реально существует
* (не сколько скопировано), поэтому вызывающий видит нехватку буфера, а не
* тихо усечённый список. PLAT_RES_E_NOSPACE: cap мал, *n_out — требуемое.
* owner == NULL: все владельцы. */
plat_res_err_t plat_res_snapshot(const plat_owner_t *owner,
plat_res_entry_t *out, size_t cap,
size_t *n_out);
/* Одна строка по токену. */
plat_res_err_t plat_res_describe(plat_res_token_t t, plat_res_entry_t *out);
/* Мост к старому API: найти токен по plat_res_id_t. Существует только ради
* platx/resource.h, чья сигнатура release(id) не несёт ни владельца, ни
* поколения. Новый код пользуется токеном. */
plat_res_token_t plat_res_token_by_id(plat_res_id_t id);
/* ── Жизненный цикл ядра ─────────────────────────────────────────────── */
int plat_res_owner_init(void);
void plat_res_owner_fini(void);
/* ── Транзакция claim → publish (A1-P04-158) ────────────────────────────
*
* Причина двух шагов: слот, заполненный наполовину, не должен быть виден
* никому. Пока идёт настройка ресурса (dup2, привязка, регистрация в
* подсистеме), слот занят — ёмкость честно уменьшена — но состояние RESERVED,
* и его не увидит ни snapshot LIVE-строк, ни sweep. Опубликовать может только
* тот, у кого токен.
*
* abandon откатывает RESERVED и НЕ зовёт releaser: ресурса ещё не было, звать
* releaser значило бы закрыть handle, которым владеет вызывающий. */
plat_res_err_t plat_res_claim(plat_owner_t owner, plat_res_type_t type,
intptr_t handle, const char *name,
plat_res_token_t *out);
plat_res_err_t plat_res_publish(plat_res_token_t t);
plat_res_err_t plat_res_abandon(plat_res_token_t t);
/* Одношаговый вариант для ресурсов, готовых к использованию сразу.
* Ровно claim+publish под одним взятием лока — никакого окна между ними.
*
* A1-P04-155: если у ЭТОГО ЖЕ владельца уже есть LIVE-строка с тем же type и
* handle, возвращается её токен и claims_dedup++. Второй строки, а значит и
* второго вызова releaser'а на один handle, не появляется. */
plat_res_err_t plat_res_own_live(plat_owner_t owner, plat_res_type_t type,
intptr_t handle, const char *name,
plat_res_token_t *out);
/* ── Разделение запрета и освобождения (A1-P04-162) ──────────────────────
*
* revoke мгновенен и не зовёт releaser: он лишь запрещает использование.
* Ресурс, освобождение которого стоит секунды (сетевой сокет с graceful
* shutdown, дочерний процесс), не должен эти секунды оставаться разрешённым
* к использованию только потому, что физически ещё жив. */
plat_res_err_t plat_res_revoke(plat_res_token_t t);
plat_res_err_t plat_res_revoke_owner(plat_owner_t owner, size_t *n_out);
/* Проверка допуска: PLAT_RES_OK только для LIVE-строки этого владельца.
* REVOKED → PLAT_RES_E_STATE; чужой владелец → PLAT_RES_E_FOREIGN; токен от
* прошлого поколения слота → PLAT_RES_E_STALE. */
plat_res_err_t plat_res_check_token(plat_res_token_t t, plat_owner_t expected);
/* ── Освобождение ───────────────────────────────────────────────────────
*
* A1-P04-161: повторный вызов на уже освобождённом токене возвращает
* PLAT_RES_E_STALE и НЕ зовёт releaser второй раз. Это объявленный результат,
* а не ошибка вызывающего: на пути остановки два независимых пути вполне
* законно доходят до одного ресурса. */
plat_res_err_t plat_res_release_token(plat_res_token_t t);
/* ── Дедлайн (A1-P04-164) ────────────────────────────────────────────────
*
* Один АБСОЛЮТНЫЙ момент на всю уборку владельца, а не таймаут на ресурс.
* Иначе общий бюджет остановки молча умножается на число ресурсов: 200 мс на
* ресурс при сорока ресурсах — это восемь секунд, которых никто не назначал.
*
* budget_ms из plat_res_deadline_in() отсчитывается по CLOCK_MONOTONIC:
* перевод системных часов не продлевает и не сокращает срок. */
typedef struct plat_res_deadline {
uint64_t at_mono_ms; /* 0 = без ограничения */
} plat_res_deadline_t;
plat_res_deadline_t plat_res_deadline_in(int budget_ms);
uint64_t plat_res_mono_ms(void);
int plat_res_deadline_remaining_ms(plat_res_deadline_t d);
/* ── Барьер остановки (A1-P04-159) ──────────────────────────────────────
*
* Между «владелец останавливается» и «владелец убран» существует окно, в
* котором его же поток может успеть зарегистрировать ресурс. Без барьера
* этот ресурс не попадает ни в один drain и остаётся навсегда.
*
* seal закрывает окно: после него claim от этого владельца — PLAT_RES_E_SEALED,
* а всё, что уже зарегистрировано, гарантированно входит в sweep. Третьего
* исхода нет, и это и есть приёмка карточки. */
plat_res_err_t plat_res_owner_seal(plat_owner_t owner);
int plat_res_owner_sealed(plat_owner_t owner);
plat_res_err_t plat_res_owner_unseal(plat_owner_t owner); /* fixtures */
/* Итог уборки владельца. released + failed + left == найдено на входе. */
typedef struct plat_res_sweep_result {
size_t found;
size_t released;
size_t failed; /* releaser отказал; строки сохранены */
size_t left; /* дедлайн истёк, не пытались */
plat_res_err_t worst;
} plat_res_sweep_result_t;
/* Отзывает и освобождает все ресурсы владельца в пределах ОДНОГО дедлайна.
* Releaser'ы вызываются вне лока таблицы (A1-P04-160).
*
* PLAT_RES_E_TIMEOUT: дедлайн истёк раньше, чем закончились ресурсы. Строки,
* до которых не дошли, остаются LIVE/REVOKED и видны в snapshot — успех над
* неубранной работой не возвращается ни при каких условиях. */
plat_res_err_t plat_res_sweep_owner(plat_owner_t owner,
plat_res_deadline_t deadline,
plat_res_sweep_result_t *out);
/* Повторная попытка по строкам RELEASE_FAILED (A1-P04-163). Ничего не
* скрывает: строка уходит из таблицы только при успехе releaser'а. */
plat_res_err_t plat_res_retry_failed(plat_owner_t owner,
plat_res_deadline_t deadline,
plat_res_sweep_result_t *out);
/* Число строк владельца в состояниях, которые считаются остатком:
* LIVE, REVOKED, DRAINING, RELEASE_FAILED. RESERVED не считается — это
* незавершённая транзакция вызывающего, а не оставленный ресурс. */
size_t plat_res_leftover_owner(plat_owner_t owner);
include/platx/safe.h
/* platx/safe.h — Wave 7: checked arithmetic, wire, handles, time, path.
*
* These helpers exist so production code does not invent a local "it'll fit"
* conversion at every boundary. Unknown, overflow, and empty are refusals,
* not silent truncation. Not a second allocator stack and not a new registry.
*/
enum {
PLAT_SAFE_OK = 0,
PLAT_SAFE_OVERFLOW = -1,
PLAT_SAFE_RANGE = -2,
PLAT_SAFE_ARGS = -3,
PLAT_SAFE_PATH_TOO_LONG = -4,
PLAT_SAFE_EMPTY = -5,
PLAT_SAFE_MALFORMED = -6,
PLAT_SAFE_UNSET = -7,
PLAT_SAFE_HOSTILE = -8,
/* B-018: отказ аллокатора — не переполнение. Раньше plat_mem_alloc()
* возвращал PLAT_SAFE_OVERFLOW, хотя никакого переполнения не было. */
PLAT_SAFE_NOMEM = -9,
/* B-022: ресурс исчерпан — не ошибка аргументов. plat_guard_push()
* возвращал PLAT_SAFE_ARGS и на !g/!fn, и на полный guard; у вызывающего
* это разные ситуации с разным лечением. */
PLAT_SAFE_FULL = -10
};
/* Stable API class (S330). Never return a raw errno from a public helper. */
typedef enum {
PLAT_ERR_OK = 0,
PLAT_ERR_ARGS = 1,
PLAT_ERR_RANGE = 2,
PLAT_ERR_NOMEM = 3,
PLAT_ERR_IO = 4,
PLAT_ERR_PERM = 5,
PLAT_ERR_AGAIN = 6,
PLAT_ERR_UNSUPPORTED = 7,
PLAT_ERR_INTERNAL = 8
} plat_err_class_t;
#define PLAT_FD_NONE (-1)
/* ── S311 / S319 narrowing ───────────────────────────────────────────── */
int plat_narrow_size_to_int(size_t v, int *out);
int plat_narrow_size_to_u32(size_t v, uint32_t *out);
int plat_narrow_u64_to_u32(uint64_t v, uint32_t *out);
int plat_narrow_i64_to_i32(int64_t v, int32_t *out);
int plat_narrow_off_to_size(int64_t v, size_t *out);
int plat_fd_from_int(int raw, int *out); /* 0 is a valid fd */
/* ── S316 shifts / masks ─────────────────────────────────────────────── */
int plat_shl_u32(uint32_t v, unsigned bits, uint32_t *out);
int plat_shl_u64(uint64_t v, unsigned bits, uint64_t *out);
int plat_shr_u32(uint32_t v, unsigned bits, uint32_t *out);
uint32_t plat_mask_u32(uint32_t v, unsigned width); /* width 0 or >32 → 0 */
/* ── S317 / S318 overflow ────────────────────────────────────────────── */
int plat_add_size(size_t a, size_t b, size_t *out);
int plat_mul_size(size_t a, size_t b, size_t *out);
int plat_add_u32(uint32_t a, uint32_t b, uint32_t *out);
int plat_mul_u32(uint32_t a, uint32_t b, uint32_t *out);
/* ── S320 / S321 / S323 endian + unaligned, no packed deref ──────────── */
uint16_t plat_load_le16(const void *p);
uint32_t plat_load_le32(const void *p);
uint64_t plat_load_le64(const void *p);
uint16_t plat_load_be16(const void *p);
uint32_t plat_load_be32(const void *p);
uint64_t plat_load_be64(const void *p);
void plat_store_le16(void *p, uint16_t v);
void plat_store_le32(void *p, uint32_t v);
void plat_store_le64(void *p, uint64_t v);
void plat_store_be16(void *p, uint16_t v);
void plat_store_be32(void *p, uint32_t v);
void plat_store_be64(void *p, uint64_t v);
/* ── S312 / S341 buffers and paths ───────────────────────────────────── */
int plat_str_copy(char *dst, size_t cap, const char *src); /* NUL or refuse */
int plat_path_join(char *dst, size_t cap, const char *a, const char *b);
/* ── S342 environment ────────────────────────────────────────────────── */
int plat_env_u64(const char *name, uint64_t *out); /* empty/malformed/overflow */
/* ── S343 monotonic time ─────────────────────────────────────────────── */
int plat_mono_now_ns(uint64_t *out);
int plat_mono_deadline_ns(uint64_t timeout_ns, uint64_t *out);
int plat_mono_expired(uint64_t deadline_ns);
/* ── S327 / S328 ownership sentinels ─────────────────────────────────── */
int plat_fd_valid(int fd); /* fd 0 is valid; -1 is not */
int plat_fd_close(int *fd); /* idempotent; never closes -1 as 0 */
void plat_ptr_move(void **dst, void **src); /* src becomes NULL */
#define PLAT_ASSERT_MOVED(p) do { if ((p) != NULL) __builtin_trap(); } while (0)
#define PLAT_ASSERT_MOVED(p) ((void)(p))
/* ── S329 / S330 errno and API class ─────────────────────────────────── */
int plat_errno_save(void);
void plat_errno_restore(int saved);
plat_err_class_t plat_err_from_errno(int e);
const char *plat_err_class_name(plat_err_class_t c);
/* ── S331 / S332 / S333 allocation ───────────────────────────────────── */
enum { PLAT_ALLOC_HEAP = 1, PLAT_ALLOC_WIRE = 2 };
typedef struct plat_mem {
void *p;
size_t n;
uint32_t family;
} plat_mem_t;
int plat_mem_alloc(plat_mem_t *m, size_t n, uint32_t family);
int plat_mem_realloc(plat_mem_t *m, size_t n); /* never `p = realloc(p)` */
void plat_mem_free(plat_mem_t *m);
int plat_mem_free_checked(plat_mem_t *m, uint32_t family); /* cross-family → fail */
/* Test hook (S334). NULL restores libc. Hook may return NULL to inject OOM. */
typedef void *(*plat_alloc_fn)(size_t n);
typedef void *(*plat_realloc_fn)(void *p, size_t n);
typedef void (*plat_free_fn)(void *p);
void plat_alloc_install(plat_alloc_fn a, plat_realloc_fn r, plat_free_fn f);
/* ── S324 flexible / trailing buffer ─────────────────────────────────── */
void *plat_flex_alloc(size_t hdr, size_t count, size_t elem, size_t *out_total);
void plat_flex_free(void *p);
/* ── S325 copy out of transient storage ──────────────────────────────── */
char *plat_dup_bounded(const char *s, size_t max);
/* ── S335 reverse-order cleanup ──────────────────────────────────────── */
typedef void (*plat_dtor_fn)(void *obj);
typedef struct plat_guard {
plat_dtor_fn fn[8];
void *obj[8];
unsigned n;
} plat_guard_t;
void plat_guard_init(plat_guard_t *g);
int plat_guard_push(plat_guard_t *g, plat_dtor_fn fn, void *obj);
void plat_guard_disarm(plat_guard_t *g);
void plat_guard_run(plat_guard_t *g); /* reverse order; idempotent */
/* ── S336 signal: handler may only note ──────────────────────────────── */
void plat_signal_note(int sig); /* async-signal-safe */
int plat_signal_take(void); /* coordinator thread */
int plat_signal_pending(void);
/* ── S337 generation / flags memory order ────────────────────────────── */
void plat_gen_store(uint32_t *slot, uint32_t v); /* release */
uint32_t plat_gen_load(const uint32_t *slot); /* acquire */
void plat_flag_set(uint32_t *slot);
int plat_flag_seen(const uint32_t *slot);
/* ── S338 / S339 typed callback ──────────────────────────────────────── */
typedef int (*plat_cb_fn)(void *ctx, uint32_t abi_minor, const void *msg);
typedef struct plat_cb_adapter {
uint32_t abi_minor;
plat_cb_fn fn;
void *ctx;
} plat_cb_adapter_t;
int plat_cb_invoke(const plat_cb_adapter_t *a, uint32_t expect_minor,
const void *msg);
#define PLAT_CHECK_CB(fn) \
((void)sizeof(_Generic((fn), plat_cb_fn: (fn), default: (fn))))
#define PLAT_CHECK_CB(fn) ((void)(fn))
/* ── S313 / S314 enum validation ─────────────────────────────────────── */
int plat_enum_u32_ok(uint32_t v, uint32_t lo, uint32_t hi_inclusive);
/* ── S326 zero-before-use ────────────────────────────────────────────── */
void plat_zero(void *p, size_t n);
/* ── S340 / S344 small typed inlines instead of unsafe macros ────────── */
size_t plat_min_size(size_t a, size_t b); /* inline interface */
size_t plat_max_size(size_t a, size_t b); /* inline interface */
int plat_safe_snprintf(char *dst, size_t cap, const char *fmt, ...)
__attribute__((format(printf, 3, 4)));
int plat_safe_snprintf(char *dst, size_t cap, const char *fmt, ...);
include/platx/services/echo_v1.h
/* platx/services/echo_v1.h — tiny loopback capability for CHILD IPC.
* Same vtable both ways: worker-local test.echo and child-provided
* test.childcap. Not a domain product API.
*/
#define PLAT_CAP_ECHO PLAT_NAME_TEST_ECHO
#define PLAT_CAP_CHILDCAP PLAT_NAME_TEST_CHILDCAP
#define PLAT_ECHO_V1 0x00010001u
#define PLAT_ECHO_M_ECHO 1u
typedef struct plat_echo_v1 {
uint32_t struct_size;
int (*echo)(const void *in, size_t in_len,
void *out, size_t out_cap, size_t *out_len);
} plat_echo_v1_t;
include/platx/shutdown_quiesce.h
/* platx/shutdown_quiesce.h — Coordinated shutdown quiesce order (INT-109). */
#define PLAT_QUIESCE_TIMEOUT_MS_DEFAULT 5000
#define PLAT_QUIESCE_REASON 128
typedef enum {
PLAT_QUIESCE_OK = 0,
PLAT_QUIESCE_TIMEOUT = 1,
PLAT_QUIESCE_ERROR = 2,
} plat_quiesce_status_t;
typedef struct {
plat_quiesce_status_t status;
int steps_completed;
char reason[PLAT_QUIESCE_REASON];
} plat_quiesce_result_t;
/* Execute ordered quiesce: Flow → MBus → Transport → FSX → FUSE → VFS.
* timeout_ms: per-step budget (0 = default). 0/-1. */
int plat_shutdown_quiesce(int timeout_ms, plat_quiesce_result_t *out);
include/platx/span.h
/* span.h — S418/S420: неполное помечено неполным, родство не выдумано.
*
* S418 После обрыва экспорта восстанавливаются ЛИБО целые отрезки, ЛИБО
* явно помеченные неполными. Незакрытый отрезок не достраивается
* временем обрыва: у него нет конца, и приписать ему конец значит
* сочинить длительность, по которой потом будут делать выводы.
*
* И отдельно: отрезок НЕ ПРИСОЕДИНЯЕТСЯ к родителю из другой генерации.
* Идентификаторы переиспользуются, генерация растёт при каждом
* пересоздании владельца; сшить их по одному лишь совпадению номера
* значит построить дерево вызовов, которого не было.
*
* S420 Состояние, экспорт которого оборвался, читается как UNKNOWN.
* Честное «не знаю» дешевле придуманной непрерывности: по второму
* принимают решения, по первому идут смотреть.
*/
#define PLAT_SPAN_MAX 256
typedef enum {
PLAT_SPAN_INCOMPLETE = 0, /* начат, конец не записан */
PLAT_SPAN_COMPLETE = 1
} plat_span_state_t;
typedef struct {
uint64_t id;
uint64_t generation;
uint64_t parent; /* 0 — корень */
uint64_t parent_gen;
uint64_t begin_ns;
uint64_t end_ns; /* значим только при COMPLETE */
plat_span_state_t state;
int orphan; /* родитель не найден в этой генерации */
} plat_span_t;
typedef struct {
plat_span_t s[PLAT_SPAN_MAX];
size_t n;
size_t incomplete;
size_t cross_gen_refused; /* сшивок, которых не сделали */
size_t overflow;
} plat_span_view_t;
struct xio_t;
int plat_span_begin(const struct xio_t *io, const char *log, uint64_t id,
uint64_t generation, uint64_t parent, uint64_t parent_gen,
uint64_t ns);
int plat_span_end (const struct xio_t *io, const char *log, uint64_t id,
uint64_t generation, uint64_t ns);
int plat_span_replay(const struct xio_t *io, const char *log,
plat_span_view_t *out);
const plat_span_t *plat_span_find(const plat_span_view_t *v, uint64_t id,
uint64_t generation);
include/platx/sqlrage.h
/* sqlrage.h — offensive.sqlrage:v1
*
* ARENA / LAB OFFENSIVE PROFILE ONLY.
* Этот заголовок запрещён в defensive shipped profile (C19/C20).
* CI-тест include-guard проверяет отсутствие в defensive manifest.
*
* SQLRage — Red-side SQL injection fixture для PLATX ARENA.
* Архитектура: Grammar → Bandit(select/mutate) → XIO(request) →
* Evaluator(score) → Extractor(harvest) → Finding(watermark)
*
* Все сетевые операции идут через xio:v1.
* Все findings несут (world_gen, watermark) текущей Arena-сессии.
* External target вне Arena CIDR → SQLRAGE_ERR_TARGET_OUTSIDE_ARENA.
*/
/* ── версия ABI ─────────────────────────────────────────────────────────── */
#define SQLRAGE_ABI_VERSION 1
#define SQLRAGE_SCHEMA_VER 1
/* ── SQL диалекты ───────────────────────────────────────────────────────── */
typedef enum {
SQLRAGE_DIALECT_MYSQL = 0,
SQLRAGE_DIALECT_MSSQL = 1,
SQLRAGE_DIALECT_PGSQL = 2,
SQLRAGE_DIALECT_ORACLE = 3,
SQLRAGE_DIALECT_AUTO = 255 /* определить fingerprint-ом */
} sqlrage_dialect_t;
/* ── техника инъекции ───────────────────────────────────────────────────── */
typedef enum {
SQLRAGE_TECH_NONE = 0,
SQLRAGE_TECH_BOOLEAN_BLIND = 1,
SQLRAGE_TECH_ERROR = 2,
SQLRAGE_TECH_UNION = 3,
SQLRAGE_TECH_TIME_BLIND = 4,
SQLRAGE_TECH_STACKED = 5,
SQLRAGE_TECH_SECOND_ORDER = 6,
SQLRAGE_TECH_OOB_DNS = 7,
} sqlrage_technique_t;
/* ── уверенность ────────────────────────────────────────────────────────── */
typedef enum {
SQLRAGE_CONF_NONE = 0, /* не публиковать finding */
SQLRAGE_CONF_LOW = 1, /* score < 0.5; подозрение */
SQLRAGE_CONF_MEDIUM = 2, /* score ≥ 0.5; вероятна */
SQLRAGE_CONF_HIGH = 3, /* score ≥ 0.9; подтверждена */
} sqlrage_confidence_t;
/* ── результат операции ─────────────────────────────────────────────────── */
typedef enum {
SQLRAGE_RESULT_CONFIRMED = 0, /* injection найдена и подтверждена */
SQLRAGE_RESULT_PARTIAL = 1, /* probe завершена частично */
SQLRAGE_RESULT_CLEAN = 2, /* injection не обнаружена */
SQLRAGE_RESULT_UNAVAILABLE = 3, /* target недостижим / cap отозвана */
SQLRAGE_RESULT_STALE = 4, /* world_gen устарел */
SQLRAGE_ERR_BAD_ARG = -1,
SQLRAGE_ERR_TARGET_OUTSIDE_ARENA= -2, /* external IP вне Arena CIDR */
SQLRAGE_ERR_XIO_FAIL = -3,
SQLRAGE_ERR_NO_PROVIDER = -4,
SQLRAGE_ERR_GENERATION_MISMATCH = -5,
SQLRAGE_ERR_CORPUS_IO = -6,
SQLRAGE_ERR_BUDGET_EXCEEDED = -7,
} sqlrage_result_t;
/* ── WAF вендор (fingerprint) ────────────────────────────────────────────── */
typedef enum {
SQLRAGE_WAF_UNKNOWN = 0,
SQLRAGE_WAF_NONE = 1,
SQLRAGE_WAF_CLOUDFLARE = 2,
SQLRAGE_WAF_MODSECURITY = 3,
SQLRAGE_WAF_AWS_WAF = 4,
SQLRAGE_WAF_F5_ASM = 5,
SQLRAGE_WAF_BARRACUDA = 6,
SQLRAGE_WAF_SUCURI = 7,
} sqlrage_waf_t;
/* ── bandit-алгоритм ────────────────────────────────────────────────────── */
typedef enum {
SQLRAGE_BANDIT_UCB1 = 0,
SQLRAGE_BANDIT_THOMPSON = 1,
SQLRAGE_BANDIT_EPSILON_GREEDY = 2,
} sqlrage_bandit_t;
/* ── finding: одна подтверждённая injection-точка ───────────────────────── */
#define SQLRAGE_PARAM_MAX 64
#define SQLRAGE_PAYLOAD_MAX 512
#define SQLRAGE_STRATEGY_MAX 32
#define SQLRAGE_URL_MAX 1024
#define SQLRAGE_WATERMARK_LEN 16
typedef struct {
uint32_t schema_ver; /* = SQLRAGE_SCHEMA_VER */
uint64_t world_gen; /* Arena world generation */
uint8_t watermark[SQLRAGE_WATERMARK_LEN]; /* Mirage provenance */
uint64_t probe_ts_ms; /* время обнаружения (monotonic)*/
char target_url[SQLRAGE_URL_MAX];
char param[SQLRAGE_PARAM_MAX];
sqlrage_dialect_t dialect;
sqlrage_technique_t technique;
sqlrage_confidence_t confidence;
float score; /* 0.0 .. 1.0 */
char tamper_strategy[SQLRAGE_STRATEGY_MAX];
uint32_t rounds_used;
char payload[SQLRAGE_PAYLOAD_MAX];
sqlrage_waf_t waf_detected;
uint32_t requests_sent;
uint32_t bytes_sent;
int extraction_done; /* 1 если Extract-фаза прошла */
} sqlrage_finding_t;
/* ── extracted data record ──────────────────────────────────────────────── */
#define SQLRAGE_FIELD_MAX 64
#define SQLRAGE_VALUE_MAX 256
typedef struct {
uint64_t world_gen;
uint8_t watermark[SQLRAGE_WATERMARK_LEN];
char db_name[SQLRAGE_FIELD_MAX];
char table_name[SQLRAGE_FIELD_MAX];
char column_name[SQLRAGE_FIELD_MAX];
uint32_t row_index;
char value[SQLRAGE_VALUE_MAX]; /* hex-encoded если binary */
int is_hex;
sqlrage_technique_t via_technique;
uint32_t requests_cost; /* запросов потрачено на запись */
} sqlrage_record_t;
/* ── corpus: persistence состояния бандита ──────────────────────────────── */
typedef struct sqlrage_corpus sqlrage_corpus_t; /* opaque */
/* ── конфигурация probe-сессии ──────────────────────────────────────────── */
typedef struct {
const char *target_url; /* arena:// или http://arena.* */
const char * const *params; /* NULL-terminated список */
int params_n;
sqlrage_dialect_t dialect; /* AUTO → fingerprint */
sqlrage_bandit_t bandit;
uint32_t max_rounds; /* default: 60 */
uint32_t max_requests; /* hard budget; default: 500 */
/* техники: битовая маска SQLRAGE_TECH_* */
uint32_t techniques_mask; /* 0 = все */
/* tamper */
float epsilon; /* для ε-greedy; default: 0.15 */
/* timing */
uint32_t time_sec; /* sleep длительность; default 5*/
uint32_t timing_samples; /* калибровка; default: 6 */
float sigma_k; /* порог; default: 3.0 */
/* extraction */
int do_extract; /* 0 = probe only */
uint32_t max_rows; /* 0 = без лимита */
/* OOB DNS */
const char *oob_domain; /* NULL = отключено */
/* markers */
const char *true_marker_re; /* NULL = auto */
const char *false_marker_re;
const char *union_marker;
/* HTTP */
int use_post;
const char *cookie;
const char * const *extra_headers; /* NULL-terminated */
uint32_t timeout_ms; /* default: 15000 */
uint32_t delay_ms; /* между запросами */
/* PLATX context */
uint64_t world_gen; /* текущая Arena world generation*/
const uint8_t *operator_scope; /* signed scope из Arena session */
uint32_t operator_scope_len;
/* corpus */
sqlrage_corpus_t *corpus; /* NULL = не персистить */
int verbose;
} sqlrage_config_t;
/* ── session handle ─────────────────────────────────────────────────────── */
typedef struct sqlrage_session sqlrage_session_t; /* opaque */
/* ═══════════════════════════════════════════════════════════════════════════
* API
* ═══════════════════════════════════════════════════════════════════════════ */
/* Создать probe-сессию. Валидирует target против Arena CIDR.
* Возвращает NULL + errno при ошибке. */
sqlrage_session_t *sqlrage_session_create(const sqlrage_config_t *cfg);
/* Запустить probe. Блокирующий вызов (должен жить в CHILD task).
* findings и findings_n — out-параметры; память принадлежит сессии.
* Возвращает sqlrage_result_t. */
sqlrage_result_t sqlrage_probe(sqlrage_session_t *s,
sqlrage_finding_t **findings,
uint32_t *findings_n);
/* Extract-фаза для конкретного finding.
* records, records_n — out; память принадлежит сессии. */
sqlrage_result_t sqlrage_extract(sqlrage_session_t *s,
const sqlrage_finding_t *finding,
sqlrage_record_t **records,
uint32_t *records_n);
/* Опросить WAF до probe. Можно вызвать отдельно. */
sqlrage_result_t sqlrage_fingerprint_waf(sqlrage_session_t *s,
sqlrage_waf_t *out_waf);
/* Прервать текущую probe/extract (thread-safe, async-signal-safe). */
void sqlrage_cancel(sqlrage_session_t *s);
/* Освободить сессию. Вызов после sqlrage_cancel() безопасен. */
void sqlrage_session_destroy(sqlrage_session_t *s);
/* ── corpus ─────────────────────────────────────────────────────────────── */
sqlrage_corpus_t *sqlrage_corpus_open(const char *path);
int sqlrage_corpus_load(sqlrage_corpus_t *c);
int sqlrage_corpus_save(sqlrage_corpus_t *c);
int sqlrage_corpus_discard(sqlrage_corpus_t *c); /* atomic rm */
void sqlrage_corpus_close(sqlrage_corpus_t *c);
/* ── provider info ──────────────────────────────────────────────────────── */
const char *sqlrage_provider_version(void); /* build hash + date */
int sqlrage_abi_version(void); /* = SQLRAGE_ABI_VERSION */
/* ── helpers ────────────────────────────────────────────────────────────── */
const char *sqlrage_result_str(sqlrage_result_t r);
const char *sqlrage_technique_str(sqlrage_technique_t t);
const char *sqlrage_confidence_str(sqlrage_confidence_t c);
const char *sqlrage_waf_str(sqlrage_waf_t w);
include/platx/startup_barrier.h
/* platx/startup_barrier.h — A1-P02: trusted startup readiness barrier.
*
* Nothing outside a very short bootstrap allowlist is allowed to touch the
* world until the profile has been read, authenticated, frozen and every
* mandatory provider has declared itself usable. That moment is the
* barrier. Before it there are no external effects; after it the policy is
* immutable until the next boot.
*
* The phase ladder only ever moves forward, and only one step at a time:
*
* BOOTSTRAP ─► PROFILE_READ ─► PROFILE_VERIFY ─► POLICY_FROZEN
* │ │
* ▼ ▼
* FAILED ◄──── PROVIDERS_READY ──► READY
*
* FAILED is reachable from every phase and is terminal for this attempt.
* plat_startup_reset() starts a new attempt and is the only way back
* (P02-090); it must return every counter to its baseline.
*
* Cards: 081 effect gating, 082 audit handshake, 083 watchdog readiness,
* 084 protection readiness, 085 no partial capability publication,
* 086 verification receipt bound to bytes + generation,
* 087 immutable bytes token for loader handoff, 088 bootstrap
* allowlist, 089 cancellation, 090 idempotent repeated failure.
*/
#define PLAT_STARTUP_NAME_MAX 32
#define PLAT_STARTUP_PROV_MAX 16
#define PLAT_STARTUP_CAP_MAX 32
#define PLAT_STARTUP_DIGEST_LEN 32
/* ── phases ──────────────────────────────────────────────────────────── */
typedef enum {
PLAT_SU_BOOTSTRAP = 0,
PLAT_SU_PROFILE_READ = 1,
PLAT_SU_PROFILE_VERIFY = 2,
PLAT_SU_POLICY_FROZEN = 3,
PLAT_SU_PROVIDERS_READY= 4,
PLAT_SU_READY = 5,
PLAT_SU_FAILED = 6,
} plat_startup_phase_t;
/* ── effect classes (P02-081/088) ────────────────────────────────────── */
/* The bootstrap allowlist is deliberately tiny. Everything else is an
* external effect and is refused until the barrier is accepted. */
typedef enum {
PLAT_EFFECT_READ_PROFILE = 0, /* allowed in BOOTSTRAP: read the blob */
PLAT_EFFECT_READ_ANCHOR = 1, /* allowed in BOOTSTRAP: identity anchor */
PLAT_EFFECT_CLOCK = 2, /* allowed in BOOTSTRAP: monotonic time */
PLAT_EFFECT_NETWORK = 3, /* refused before READY */
PLAT_EFFECT_FS_WRITE = 4, /* refused before READY */
PLAT_EFFECT_EXEC = 5, /* refused before READY */
PLAT_EFFECT_AUDIT_EMIT = 6, /* refused before PROVIDERS_READY */
PLAT_EFFECT_MODULE_INIT = 7, /* refused before POLICY_FROZEN */
PLAT_EFFECT__COUNT = 8,
} plat_effect_class_t;
/* ── provider readiness (P02-082/083/084) ────────────────────────────── */
/* The contract distinguishes three states, because "initialised" and
* "usable" are not the same claim and a consumer that conflates them will
* publish READY over a sink that cannot take a record. */
typedef enum {
PLAT_PROV_UNKNOWN = 0, /* never declared */
PLAT_PROV_INITIALIZED = 1, /* constructed, not yet proven usable */
PLAT_PROV_USABLE = 2, /* proven usable: a probe round-tripped */
PLAT_PROV_FAILED = 3, /* declared failure; no silent retry */
} plat_prov_state_t;
/* ── module bytes token (P02-086/087) ────────────────────────────────── */
/* A pathname is not an identity: the file behind it can be replaced
* between verify and use. Consumers receive this token instead, and a
* receipt is only valid for the exact (len, digest, boot_gen) it names. */
typedef struct plat_module_token {
uint8_t digest[PLAT_STARTUP_DIGEST_LEN];
size_t len;
uint64_t boot_gen;
int valid;
} plat_module_token_t;
/* ── lifecycle ───────────────────────────────────────────────────────── */
/* Begin a startup attempt at BOOTSTRAP for the given boot generation.
* Clears all per-attempt counters. Returns 0, or -1 if an attempt is
* already in flight and has not failed or been cancelled. */
int plat_startup_begin(uint64_t boot_gen);
plat_startup_phase_t plat_startup_phase(void);
uint64_t plat_startup_boot_gen(void);
/* Advance exactly one phase. Any other transition is refused with -1 and
* leaves the phase untouched. */
int plat_startup_advance(plat_startup_phase_t to);
/* P02-085: fail the attempt. Every capability published during this
* attempt is retracted before this returns, so no capability of a module
* that was never admitted stays visible. */
int plat_startup_fail(const char *reason);
/* P02-089: cancellation during profile read or verify. Temporary
* resources are released and READY is never published. Distinguished from
* failure so a caller can tell "we stopped" from "we refused". */
int plat_startup_cancel(const char *reason);
/* P02-090: start over. Every counter this module owns returns to its
* baseline, so N consecutive failed attempts leave the same footprint as
* one. */
void plat_startup_reset(void);
const char *plat_startup_reason(void);
/* ── effect gating (P02-081/088) ─────────────────────────────────────── */
/* Returns 1 if the effect is permitted in the current phase, else 0.
* Every call is counted, permitted or not, so a fixture can assert that
* zero effects were attempted before the barrier. */
int plat_startup_effect_allowed(plat_effect_class_t e);
/* Counters for fixtures. attempted counts every call to
* effect_allowed(); permitted counts the calls that returned 1. */
uint32_t plat_startup_effect_attempted(void);
uint32_t plat_startup_effect_permitted(void);
uint32_t plat_startup_effect_refused(void);
/* ── provider handshake (P02-082/083/084) ────────────────────────────── */
/* Declare a provider mandatory for this profile. Mandatory providers must
* reach PLAT_PROV_USABLE before PROVIDERS_READY is accepted. */
int plat_startup_provider_require(const char *name);
/* Declare the provider's current state. A provider may move
* UNKNOWN → INITIALIZED → USABLE, or to FAILED from anywhere. Moving
* backwards out of FAILED is refused: a failed mandatory provider does
* not silently recover inside one attempt. */
int plat_startup_provider_declare(const char *name, plat_prov_state_t st);
plat_prov_state_t plat_startup_provider_state(const char *name);
/* Returns 1 if every mandatory provider is USABLE. Populates *missing
* with the first provider that is not, when non-NULL. */
int plat_startup_providers_ready(const char **missing);
/* ── capability publication (P02-085) ────────────────────────────────── */
/* Publish a capability for this attempt. Publications are tracked so
* plat_startup_fail() can retract all of them atomically. */
int plat_startup_publish_cap(const char *name);
int plat_startup_cap_visible(const char *name);
uint32_t plat_startup_cap_count(void);
/* ── verification receipts (P02-086/087) ─────────────────────────────── */
/* Non-cryptographic content digest, for change detection ONLY.
* This is NOT an integrity proof and must never be presented as one
* (P02-099). It detects a module blob that changed between verify and
* use; it does not detect an adversary who can compute a preimage. The
* cryptographic verifier is a separate, linked provider. */
void plat_startup_content_digest(const void *bytes, size_t len,
uint8_t out[PLAT_STARTUP_DIGEST_LEN]);
/* Mint a token for a module blob. The token names the bytes, not the
* path, and carries the boot generation it was minted in (P02-087). */
plat_module_token_t plat_startup_token_mint(const void *bytes, size_t len);
/* Record that the verifier admitted these exact bytes in this generation. */
int plat_startup_receipt_record(const plat_module_token_t *tok);
/* P02-086: is the recorded admission still valid for these bytes?
* Returns 1 only if a receipt exists whose digest, length and boot
* generation all match the bytes presented now. Replacing the blob after
* verify makes the admission invalid. */
int plat_startup_receipt_valid(const void *bytes, size_t len);
uint32_t plat_startup_receipt_count(void);
/* ── barrier ─────────────────────────────────────────────────────────── */
/* Accept the barrier and publish READY. Refuses unless the phase is
* PROVIDERS_READY, every mandatory provider is USABLE, and an immutable
* profile snapshot is published. Returns 0 on success, -1 on refusal;
* *why is set to a stable reason when non-NULL. */
int plat_startup_accept_barrier(const char **why);
/* Resource balance for the idempotence fixture (P02-090): the number of
* tracked live resources this module holds. Must be 0 at baseline and 0
* again after any failure or cancellation. */
int plat_startup_resource_balance(void);
/* Temporary decoded-copy accounting (P02-072). A temporary is registered
* while a decoded profile copy is live and released when it is wiped;
* the barrier refuses while any remains outstanding. */
int plat_startup_temp_acquire(void);
int plat_startup_temp_release(void);
int plat_startup_temp_outstanding(void);
include/platx/txn.h
/* txn.h — S423/S428/S442: незавершённое не выдаётся за завершённое.
*
* Поверх кадрированного журнала (reclog) кладутся записи транзакций:
* BEGIN <id> <нагрузка>, затем COMMIT <id> или ABORT <id>.
*
* ТРИ ИСХОДА, А НЕ ДВА
* ────────────────────
* BEGIN без завершающей записи — это НЕ «не выполнено» и НЕ «выполнено».
* Процесс умер между началом операции и записью её итога; что успел сделать
* внешний мир, журнал не знает. Такой транзакции присваивается UNKNOWN, и это
* отдельный исход, который обязан дойти до оператора (S442). Достроить его до
* «не выполнено» значит повторить операцию, которая, возможно, прошла;
* до «выполнено» — потерять её.
*
* ПОВТОР ЗАПРЕЩЁН (S423)
* ──────────────────────
* Транзакция с известным исходом при повторном разборе журнала НЕ
* применяется заново. Перезапуск не имеет права молча переиграть то, что уже
* случилось.
*
* ДУБЛИКАТ — НЕ ПОВОД ТЕРЯТЬ СОСЕДА (S428)
* ────────────────────────────────────────
* Повторный BEGIN с тем же идентификатором отмечается как дубликат, но
* остальные записи разбираются дальше. Отбрасывать хвост журнала из-за одной
* повторной записи значит терять непохожие, вполне действительные операции.
*/
#define PLAT_TXN_ID_MAX 32
#define PLAT_TXN_MAX 256
typedef enum {
PLAT_TXN_UNKNOWN = 0, /* начата, итог не записан — крах посередине */
PLAT_TXN_COMMITTED = 1,
PLAT_TXN_ABORTED = 2
} plat_txn_state_t;
typedef struct {
char id[PLAT_TXN_ID_MAX];
plat_txn_state_t state;
uint32_t begins; /* сколько раз начиналась (дубликаты) */
uint32_t ends; /* сколько раз завершалась */
} plat_txn_entry_t;
typedef struct {
plat_txn_entry_t e[PLAT_TXN_MAX];
size_t n;
uint32_t duplicates; /* повторные BEGIN с известным id */
uint32_t orphan_ends; /* COMMIT/ABORT без BEGIN */
uint32_t overflow; /* не поместившиеся id — НЕ ноль */
} plat_txn_view_t;
struct xio_t;
int plat_txn_begin (const struct xio_t *io, const char *log, const char *id,
const void *payload, size_t len);
int plat_txn_commit(const struct xio_t *io, const char *log, const char *id);
int plat_txn_abort (const struct xio_t *io, const char *log, const char *id);
/* Разобрать журнал в исходы. Идемпотентен: сколько раз ни зови — тот же ответ,
* и ничего не применяет сам. */
int plat_txn_replay(const struct xio_t *io, const char *log,
plat_txn_view_t *out);
const plat_txn_entry_t *plat_txn_find(const plat_txn_view_t *v, const char *id);
include/platx/watchdog.h
/* include/platx/watchdog.h — независимый сторож (A1-P09-421..430).
*
* ЗАЧЕМ ОТДЕЛЬНЫЙ ПОТОК
*
* До этой волны платформенный сторож жил заданием `recovery:tick` на общем
* планировщике (src/core/sched.c, заведено в platform_init.c). Планировщик
* там ОДИН поток, и он выполняет задания ПОСЛЕДОВАТЕЛЬНО:
*
* pthread_mutex_unlock(&s->lock);
* if (fn) fn(ud); <-- задание выполняется здесь
* pthread_mutex_lock(&s->lock);
*
* На том же потоке стоят `supervisor:tick`, `keyring:gc` и — существенно —
* задания, заводимые DSL (`src/dsl/dsl_exec.c`: job_loop_step, job_health),
* то есть код, приходящий из сценария, а не из ядра. Любое из них, зависнув
* в fn(), останавливает сторожа НАВСЕГДА: не задерживает, а лишает его
* следующего тика. Сторож, которого останавливает то, за чем он следит, не
* отличается от отсутствующего.
*
* A3 нашёл ровно это в Windows-части (sweep дедлайнов iocp_pool исполняется
* только из ветки простоя воркера). Linux-часть больна тем же, в более
* тяжёлой форме: там сторожил хотя бы какой-то воркер, здесь поток ровно
* один и второго наблюдателя нет.
*
* Здесь у сторожа СВОЙ поток. Он не берёт ни один лок наблюдаемого, не стоит
* в очереди заданий и не зависит от того, жив ли dispatch.
*
* ЧЕГО ЭТОТ МОДУЛЬ НЕ ДЕЛАЕТ
*
* Он не меняет состояние жизненного цикла и не перезапускает модули
* (A1-P09-425). Он публикует ФАКТ: «heartbeat владельца X просрочен на N мс,
* страйк K». Решение остаётся у восстановления — второго движка
* восстановления здесь не заводится (A1-P09-448).
*
* SPDX-License-Identifier: GPL-2.0
*/
#define PLAT_WD_MAX_WATCH 32u
#define PLAT_WD_RING 64u
/* Род факта. «Здоровье не измерено» — ОТДЕЛЬНЫЙ род, а не разновидность
* «здоров». Это и есть A1-P09-427: снимок, который не сошёлся, ничего не
* подтверждает, и публиковать по нему бодрый heartbeat значит отвечать
* «всё хорошо» на вопрос, на который мы не ответили. */
typedef enum {
PLAT_WD_FACT_SILENT = 1, /* heartbeat просрочен */
PLAT_WD_FACT_UNMEASURABLE = 2, /* наблюдение не сошлось */
PLAT_WD_FACT_SELF_DEGRADED= 3, /* сам сторож не может измерять время */
PLAT_WD_FACT_RECOVERED = 4 /* был SILENT, снова стучит */
} plat_wd_fact_kind_t;
/* Факт. Только числа и идентификаторы; ни одного указателя — он копируется
* в кольцо и переживает наблюдаемого. */
typedef struct plat_wd_fact {
uint32_t kind;
uint32_t strikes; /* сколько подряд просрочек насчитано */
uint64_t owner_id;
uint64_t owner_gen;
uint64_t task_gen; /* поколение наблюдаемой задачи (A1-P09-428) */
uint64_t age_ms; /* возраст последнего удара; 0 если не измерен */
uint64_t seq; /* порядковый номер публикации */
uint64_t mono_ms; /* когда опубликован; 0 если часы отказали */
} plat_wd_fact_t;
/* Исход наблюдения одного владельца. Возвращает наблюдатель. */
typedef enum {
PLAT_WD_OBS_OK = 0, /* стучит, возраст в *age_ms */
PLAT_WD_OBS_SILENT = 1, /* молчит дольше допуска */
PLAT_WD_OBS_UNMEASURABLE = 2, /* снимок не сошёлся / часы отказали */
PLAT_WD_OBS_GONE = 3 /* владельца больше нет — снять с учёта */
} plat_wd_obs_t;
/* Наблюдатель. Обязан быть НЕБЛОКИРУЮЩИМ и не брать локов наблюдаемого:
* весь смысл отдельного потока пропадает, если сторож встаёт в ту же
* очередь (A1-P09-422). Штатная реализация читает plat_task через seqlock. */
typedef plat_wd_obs_t (*plat_wd_observe_fn)(void *ud, uint64_t owner_id,
uint64_t owner_gen,
uint64_t *age_ms,
uint64_t *task_gen);
/* Политика. Снимается ОДИН раз при старте и дальше неизменна (A1-P09-424):
* сторож, у которого интервал можно подвинуть на ходу, не даёт ни одного
* утверждения о сроке обнаружения. */
typedef struct plat_wd_policy {
uint32_t interval_ms; /* период опроса; 0 отвергается */
uint32_t max_strikes; /* просрочек до факта SILENT; 0 отвергается */
uint32_t dedup_repeat; /* сколько одинаковых фактов подряд публиковать
* до подавления; 0 → 1 (A1-P09-426) */
} plat_wd_policy_t;
/* ── жизненный цикл ───────────────────────────────────────────────────── */
/* Запустить сторожа на СВОЁМ потоке. Повторный старт — ошибка, а не
* «уже запущено»: два сторожа считают страйки независимо. */
int plat_wd_start(const plat_wd_policy_t *pol,
plat_wd_observe_fn obs, void *ud);
void plat_wd_stop(void);
int plat_wd_running(void);
/* Действующая политика. Пустой результат (-1), если сторож не запущен.
* Отдельной функции «поменять интервал» здесь НЕТ намеренно. */
int plat_wd_policy_get(plat_wd_policy_t *out);
/* ── подписка ─────────────────────────────────────────────────────────── */
int plat_wd_watch(uint64_t owner_id, uint64_t owner_gen);
int plat_wd_unwatch(uint64_t owner_id, uint64_t owner_gen);
int plat_wd_watch_count(void);
/* ── факты ────────────────────────────────────────────────────────────── */
/* Забрать опубликованные факты. Кольцо ОГРАНИЧЕНО: если потребитель не
* успевает, теряются СТАРЫЕ факты, а сторож не встаёт (A1-P09-430).
* Число потерянных возвращает plat_wd_dropped(). */
int plat_wd_drain(plat_wd_fact_t *out, int max);
uint64_t plat_wd_dropped(void);
uint64_t plat_wd_published(void);
/* Сколько раз сторож прошёл круг. Это и есть проверяемое «сторож жив»:
* счётчик растёт независимо от того, что происходит с наблюдаемыми. */
uint64_t plat_wd_ticks(void);
/* Часы сторожа для фикстур. NULL — штатные монотонные. */
void plat_wd_set_clock_for_test(int (*fn)(uint64_t *out));
/* Задать политику и наблюдателя БЕЗ запуска потока. Только для фикстур:
* штатный plat_wd_start делает первый круг сразу, и для детерминированной
* проверки этот круг — гонка с тестом. */
int plat_wd_arm_for_test(const plat_wd_policy_t *pol,
plat_wd_observe_fn obs, void *ud);
/* Один круг вручную, БЕЗ потока: для детерминированных проверок. Возвращает
* число ОПУБЛИКОВАННЫХ фактов (подавленные повторы не считаются). */
int plat_wd_tick_once(void);
/* Сбросить всю статику. Только для фикстур. */
void plat_wd_reset_for_test(void);
include/platx/watchdog_bind.h
/* include/platx/watchdog_bind.h — продуктовый вход независимого сторожа.
*
* Отделён от watchdog.h намеренно: сам сторож не знает типов платформы и
* проверяется без неё (tests/bounds/t_watchdog.c линкует его с sched.c и
* ничем больше). Здесь — привязка, и только она тянет plat_task и профиль.
*
* SPDX-License-Identifier: GPL-2.0
*/
/* 0 — сторож запущен; -1 — профиль про сторожа не сказал или поток не
* создался. -1 означает «платформа осталась без наблюдения» и обязан быть
* сообщён, а не проглочен. */
int plat_watchdog_bind_start(void);
void plat_watchdog_bind_stop(void);
int plat_watchdog_bind_watch(plat_owner_t owner);
int plat_watchdog_bind_unwatch(plat_owner_t owner);
/* Перенести накопленные факты во вход восстановления. Зовётся из задания
* recovery:tick, то есть там, где живёт ДЕЙСТВИЕ; наблюдение к этому моменту
* уже произошло на отдельном потоке. */
int plat_watchdog_bind_pump(int max);
include/platx/wire.h
/* wire.h — S461/S469/S470: версия на проводе и стойкость к понижению.
*
* S469 У каждой точки входа обрамления должна быть версия и ОГРАНИЧЕННОЕ
* согласование возможностей. Ограниченное — значит: набор битов
* известен заранее, чужие биты отбрасываются, и согласованное никогда
* не больше того, что предложила каждая сторона.
*
* S470 Понижение отвергается. Сторона, однажды говорившая по версии N, не
* принимает предложение N-1: это либо чужая подделка, либо откат, о
* котором надо узнать, а не согласиться молча. Отказ от понижения —
* единственное, что отличает согласование от подчинения.
*
* S461 Событие несёт версию своей полезной нагрузки. Подписчик объявляет,
* какую понимает. Несовместимое НЕ ДОСТАВЛЯЕТСЯ — отказ происходит до
* вызова обработчика, а не внутри него: обработчик, получивший чужой
* формат, уже прочитал его своими полями.
*/
/* ── рукопожатие ─────────────────────────────────────────────────────────── */
#define PLAT_WIRE_VERSION_MIN 1u
#define PLAT_WIRE_VERSION_MAX 3u
/* Известные возможности. Чужие биты в предложении отбрасываются. */
#define PLAT_WIRE_FEAT_AEAD 0x0001u
#define PLAT_WIRE_FEAT_COMPRESS 0x0002u
#define PLAT_WIRE_FEAT_MULTIPART 0x0004u
#define PLAT_WIRE_FEAT_KNOWN 0x0007u
typedef struct {
uint16_t version; /* согласованная */
uint16_t features; /* согласованные возможности */
uint16_t floor; /* ниже этой версии не опускаемся */
uint8_t established; /* 1 — рукопожатие состоялось */
} plat_wire_t;
typedef enum {
PLAT_WIRE_OK = 0,
PLAT_WIRE_TOO_OLD = 1, /* ниже минимума */
PLAT_WIRE_TOO_NEW = 2, /* выше максимума */
PLAT_WIRE_DOWNGRADE = 3 /* попытка опустить ниже пола */
} plat_wire_verdict_t;
/* Начать: пол равен floor (0 — минимум протокола). */
void plat_wire_init(plat_wire_t *w, uint16_t floor);
/* Согласовать с предложением той стороны. */
plat_wire_verdict_t plat_wire_negotiate(plat_wire_t *w, uint16_t peer_version,
uint16_t peer_features);
const char *plat_wire_verdict_name(plat_wire_verdict_t v);
/* ── версии событий (S461) ───────────────────────────────────────────────── */
typedef struct {
uint32_t type;
uint16_t payload_version;
} plat_event_hdr_t;
/* 1 — доставлять, 0 — нет. Подписчик объявляет минимальную и максимальную
* версию нагрузки, которую он умеет читать. */
int plat_event_deliverable(const plat_event_hdr_t *e,
uint16_t sub_min, uint16_t sub_max);
include/platx/witness_provider.h
/* platx/witness_provider.h — witness_provider_v1 (D053).
*
* A witness does not stream facts. It answers a bounded question about state
* that something else claims: "which tasks exist", "is this page still the
* one that was measured", "which modules are loaded". Its value is that it
* looks from somewhere else — an LKM below the process, a hypervisor below
* the kernel, a firmware measurement made before either existed.
*
* Two rules make it worth having:
* 1. A claim carries what it is a claim ABOUT and where it was made from.
* A claim without a subject is an opinion (D086, D088).
* 2. The consistency of a snapshot is measured, not declared. A coordinator
* that lets a provider label a best-effort walk "atomic" has built a
* quorum out of one opinion repeated (D064).
*
* ABI freeze: WITNESS_PROVIDER_ABI 1 (D053 sealed 2026-09-01)
*/
#define WITNESS_PROVIDER_ABI 1u
#define WITNESS_SUBJECT_MAX 96u
#define WITNESS_CLAIMS_MAX 64u /* per snapshot, per provider */
#define WITNESS_EVIDENCE_MAX 64u /* bytes of an evidence reference */
/* ── Consistency of a snapshot (D063) ────────────────────────────────────
* ATOMIC one freeze point; nothing changed under the walk, and the
* provider can prove it (a stop-the-world token, a generation
* that did not move).
* COORDINATED several providers walked inside one bounded window and the
* window is stated. Deltas during the window are reported.
* BEST_EFFORT the walk was live; boundaries are unknown.
* OFFLINE the subject could not be observed at all. Not an empty result:
* an empty ATOMIC snapshot claims "nothing exists", an OFFLINE
* one claims nothing.
*/
typedef enum witness_consistency {
WITNESS_CONS_OFFLINE = 0,
WITNESS_CONS_BEST_EFFORT = 1,
WITNESS_CONS_COORDINATED = 2,
WITNESS_CONS_ATOMIC = 3,
WITNESS_CONS_MAX
} witness_consistency_t;
/* ── What a claim is about ───────────────────────────────────────────────── */
typedef enum witness_subject {
WITNESS_SUBJ_NONE = 0,
WITNESS_SUBJ_TASK = 1, /* a task/thread the kernel knows */
WITNESS_SUBJ_MODULE = 2, /* a loaded kernel module */
WITNESS_SUBJ_SOCKET = 3, /* an open socket */
WITNESS_SUBJ_PAGE = 4, /* a physical/guest page under protection */
WITNESS_SUBJ_FILE = 5, /* a file identity, not its content */
WITNESS_SUBJ_MEASURE = 6, /* a measurement/baseline entry (IMA, TPM) */
WITNESS_SUBJ_MAX
} witness_subject_t;
/* ── Evidence reference (D091) ───────────────────────────────────────────
* A handle, never a blob. SENSE stores references to evidence held by
* FORENSIC; copying the bytes into the observation plane would make every
* consumer a custodian of material it cannot seal.
*/
typedef struct witness_evidence_ref {
uint32_t kind; /* forensic store class */
uint32_t len; /* bytes used in `ref` */
uint8_t ref[WITNESS_EVIDENCE_MAX]; /* opaque handle */
uint8_t digest[32]; /* SHA-256 of the referenced object */
uint64_t sealed_at_ns; /* 0 = not sealed */
} witness_evidence_ref_t;
/* ── One claim ───────────────────────────────────────────────────────────── */
typedef struct witness_claim {
uint32_t abi_version;
uint32_t struct_size;
uint32_t subject; /* witness_subject_t */
uint32_t flags;
char subject_id[WITNESS_SUBJECT_MAX]; /* stable subject identity */
uint64_t observed_at_ns;
uint64_t epoch; /* provider epoch of the observation */
uint64_t value_hi; /* subject-specific measurement */
uint64_t value_lo;
uint32_t independence_group; /* copied from the envelope */
uint32_t bridge_id; /* copied from the envelope (D089) */
witness_evidence_ref_t evidence; /* zeroed when there is none */
} witness_claim_t;
#define WITNESS_CLAIM_F_PRESENT (1u << 0) /* the subject exists */
#define WITNESS_CLAIM_F_ABSENT (1u << 1) /* the subject provably does not */
#define WITNESS_CLAIM_F_HIDDEN (1u << 2) /* present here, absent elsewhere */
#define WITNESS_CLAIM_F_MEASURED (1u << 3) /* value_* is a measurement */
#define WITNESS_CLAIM_F_SEALED (1u << 4) /* evidence.sealed_at_ns is set */
/* ── A snapshot as it was actually taken ─────────────────────────────────── */
typedef struct witness_snapshot {
uint32_t abi_version;
uint32_t struct_size;
uint32_t consistency; /* witness_consistency_t ACHIEVED, not asked */
uint32_t n_claims;
uint64_t window_begin_ns; /* both zero only for OFFLINE */
uint64_t window_end_ns;
uint64_t deltas_during; /* changes observed inside the window */
uint64_t freeze_token; /* non-zero only when the walk was frozen */
witness_claim_t claims[WITNESS_CLAIMS_MAX];
} witness_snapshot_t;
/* ── The vtable ──────────────────────────────────────────────────────────── */
typedef struct witness_provider_v1 {
uint32_t abi_version; /* WITNESS_PROVIDER_ABI */
uint32_t struct_size;
void *ctx;
int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);
int (*negotiate)(void *ctx, const prov_negotiate_req_t *req,
prov_negotiate_result_t *out);
/* Best consistency this provider can actually deliver right now. The
* coordinator asks before it promises, and never promises more. */
int (*max_consistency)(void *ctx, uint32_t subject,
uint32_t *consistency_out, prov_status_t *st);
/* Take a bounded snapshot of one subject class under a budget. The
* provider fills `consistency` with what it achieved; claiming more than
* max_consistency() reported is a conformance failure (D064). */
int (*snapshot)(void *ctx, uint32_t subject, const prov_budget_t *budget,
witness_snapshot_t *out, prov_status_t *st);
int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);
int (*selftest)(void *ctx, prov_selftest_t *out, prov_status_t *st);
void *reserved[8];
} witness_provider_v1_t;
/* Name of a consistency level, for evidence and logs. Never NULL. */
const char *witness_consistency_name(uint32_t c);
const char *witness_subject_name(uint32_t s);
include/platx/zeroize.h
/* include/platx/zeroize.h — ограниченная очистка Core-owned контекстов
* (A1-P09-431..438).
*
* ЧТО ЭТОТ МОДУЛЬ ОБЕЩАЕТ И ЧЕГО НЕ ОБЕЩАЕТ
*
* Обещает: перечисленные ЗДЕСЬ буферы будут затёрты неустранимой записью, и
* порядок будет такой, что новое использование запрещено ДО начала медленной
* уборки, а не после неё.
*
* Не обещает ничего про копии, которых этот модуль не видит: страницы,
* отданные ядру, буферы аллокатора, срезы в стеке чужих функций, содержимое
* swap и core-дампа. Их сюда не записывают и «очищенными» не называют
* (A1-P09-432). Список областей — это ИНВЕНТАРЬ ПОДКОНТРОЛЬНОГО, а не
* утверждение «все секреты в системе стёрты»; выдать первое за второе —
* ровно тот способ соврать, от которого карточка 449 предостерегает.
*
* ПОЧЕМУ ПОРЯДОК ИМЕННО ТАКОЙ
*
* tamper -> revoke -> drain -> zeroize
*
* Отзыв допуска стоит ПЕРВЫМ и он мгновенный. Затирание — медленное: оно
* идёт по областям и ограничено шагом. Если сначала затирать, а потом
* закрывать вход, то всё время уборки контекст остаётся доступным, и заявка,
* пришедшая в середине, прочитает наполовину затёртый буфер. Поэтому вход
* закрывается одной записью состояния, и только затем начинается уборка.
*
* SPDX-License-Identifier: GPL-2.0
*/
#define PLAT_ZX_MAX_REGIONS 32u
#define PLAT_ZX_TAG_MAX 24u
/* Состояние контекста. Порядок значений совпадает с порядком перехода;
* назад состояние не идёт никогда, кроме явного rearm с НОВЫМ поколением. */
typedef enum {
PLAT_ZX_LIVE = 0, /* обычная работа */
PLAT_ZX_REVOKED = 1, /* tamper записан, вход закрыт, уборка не начата */
PLAT_ZX_DRAINING = 2, /* уборка идёт, ограничена шагами */
PLAT_ZX_ZEROIZED = 3, /* уборка закончена; терминальное */
PLAT_ZX_KILLED = 4 /* финальный kill-switch (A1-P09-437) */
} plat_zx_state_t;
/* Причина терминального исхода. Первая записанная НЕ перезаписывается:
* повторный tamper во время уборки не меняет ответ на вопрос «почему»
* (A1-P09-436). */
typedef enum {
PLAT_ZX_REASON_NONE = 0,
PLAT_ZX_REASON_TAMPER = 1,
PLAT_ZX_REASON_KILL = 2,
PLAT_ZX_REASON_SHUTDOWN = 3
} plat_zx_reason_t;
/* Класс заявки на допуск (A1-P09-437). Только уборка Координатора проходит
* после kill; всё остальное отвергается. */
typedef enum {
PLAT_ZX_ADMIT_EFFECT = 0, /* обычный эффект ACT */
PLAT_ZX_ADMIT_CLEANUP = 1 /* разрешённая уборка */
} plat_zx_admit_class_t;
typedef enum {
PLAT_ZX_ADMIT_OK = 0,
PLAT_ZX_ADMIT_REVOKED = 1, /* контекст отозван */
PLAT_ZX_ADMIT_KILLED = 2, /* сработал kill-switch */
PLAT_ZX_ADMIT_STALE = 3 /* поколение заявителя не то */
} plat_zx_admit_t;
/* Исход по ОДНОЙ области. Различие проверенного и непроверенного —
* содержательное: «я записал нули» и «я перечитал и увидел нули» это разные
* утверждения, и второе доступно не всегда (A1-P09-438, 449). */
typedef enum {
PLAT_ZX_OUT_PENDING = 0,
PLAT_ZX_OUT_VERIFIED = 1, /* затёрто и перечитано */
PLAT_ZX_OUT_WRITTEN = 2, /* затёрто, перечитать нечем */
PLAT_ZX_OUT_HELD = 3 /* НЕ затёрто: область удерживается кем-то */
} plat_zx_outcome_t;
typedef struct plat_zx_region_report {
char tag[PLAT_ZX_TAG_MAX];
size_t len;
uint32_t outcome;
} plat_zx_region_report_t;
/* Итог уборки. Ни одного поля вида «успех»: слово SUCCEEDED здесь не
* употребляется намеренно (A1-P09-438). Есть числа, и по ним видно, что
* именно проверено, а что только записано или вовсе удержано. */
typedef struct plat_zx_report {
uint32_t state;
uint32_t reason;
uint32_t regions_total;
uint32_t regions_verified;
uint32_t regions_written;
uint32_t regions_held; /* честный остаток: см. A1-P09-446 */
uint32_t steps_used;
uint32_t steps_budget;
uint32_t tamper_count; /* включая повторные во время уборки */
uint32_t restarts; /* обязан остаться 0 (A1-P09-436) */
uint64_t generation;
} plat_zx_report_t;
/* ── инвентарь (A1-P09-432) ───────────────────────────────────────────── */
/* Внести подконтрольную область. Только память, которой владеет Core и
* которую он может затереть сам. Всё, что «наверное, тоже копия», сюда не
* вносится: инвентарь, в котором есть недоказанное, перестаёт быть
* инвентарём. */
int plat_zx_register(void *addr, size_t len, const char *tag);
/* То же, но с явным указанием, можно ли ПЕРЕЧИТАТЬ область после записи.
*
* verifiable == 0 — область записать можно, прочитать обратно нельзя
* (write-only отображение, буфер устройства, чужая страница). Такая область
* получает исход WRITTEN, а не VERIFIED, и разница эта содержательная:
* «я записал нули» и «я перечитал и увидел нули» — разные утверждения, и
* складывать их в одно число значит приписать отчёту проверку, которой не
* было (A1-P09-438, 449). plat_zx_register == register_ex с verifiable = 1. */
int plat_zx_register_ex(void *addr, size_t len, const char *tag,
int verifiable);
int plat_zx_region_count(void);
/* ── жизненный цикл (A1-P09-431, 435, 436) ────────────────────────────── */
int plat_zx_arm(uint64_t generation);
/* Записать tamper. Вход закрывается ЗДЕСЬ и немедленно; уборка не начата.
* Повторный вызов во время уборки считается, но не перезапускает её и не
* переписывает первую причину (A1-P09-436). */
int plat_zx_tamper(plat_zx_reason_t reason);
/* Финальный kill-switch (A1-P09-437). */
int plat_zx_kill(void);
/* Один шаг уборки. Возвращает число обработанных областей, 0 — больше нечего,
* -1 — уборка не начата. Шаги ограничены: cleanup не может стать бесконечным
* ни при каком поведении наблюдаемого. */
int plat_zx_drain_step(void);
/* Довести уборку до конца в пределах бюджета шагов. */
int plat_zx_drain_all(void);
/* Заново вооружить контекст. Разрешено ТОЛЬКО из терминального состояния и
* ТОЛЬКО с поколением строго больше прежнего (A1-P09-435): затёртый
* контекст не оживает по тому же адресу и по старым токенам. */
int plat_zx_rearm(uint64_t new_generation);
/* ── допуск и отчёт ───────────────────────────────────────────────────── */
plat_zx_admit_t plat_zx_admit(uint64_t generation, plat_zx_admit_class_t cls);
int plat_zx_report(plat_zx_report_t *out);
int plat_zx_regions(plat_zx_region_report_t *out, int max);
plat_zx_state_t plat_zx_state(void);
/* Неустранимая запись нулей поверх области (A1-P09-433). Отдельно
* экспортирована, чтобы её можно было проверить в оптимизированной сборке. */
void plat_zx_wipe(void *addr, size_t len);
/* Пометить область как удерживаемую: её нельзя затирать, пока владелец не
* отпустит. Существует ради честности отчёта — удержанная область попадёт в
* regions_held, а не будет молча пропущена (A1-P09-446). */
int plat_zx_hold(const char *tag, int held);
void plat_zx_reset_for_test(void);