1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com
Project: zyginit
Filename: contract.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
Description: Hammerhead process contract FFI bindings (libcontract)
******************************************************************************/
// Hammerhead process contracts are the kernel mechanism for tracking service
// processes. This module provides Reef wrappers around libcontract.
//
// This module ONLY works on Hammerhead. On Linux, the extern "C"
// functions will fail at link time unless stub implementations are provided.
//
// Reference: Hammerhead libcontract(3LIB), contract(5), process(5)
// Source: hammerhead/usr/src/lib/libcontract/common/
module contract
import sys.fd as fd
import core.str
export
type Contract
// Contract event flags (from sys/contract/process.h)
fn CT_PR_EV_EMPTY(): int
fn CT_PR_EV_FORK(): int
fn CT_PR_EV_EXIT(): int
fn CT_PR_EV_CORE(): int
fn CT_PR_EV_SIGNAL(): int
fn CT_PR_EV_HWERR(): int
fn CT_PR_ALLEVENT(): int
fn CT_PR_ALLFATAL(): int
// Contract parameter flags
fn CT_PR_INHERIT(): int
fn CT_PR_NOORPHAN(): int
fn CT_PR_PGRPONLY(): int
fn CT_PR_REGENT(): int
// Status detail levels (from sys/contract.h)
fn CTD_COMMON(): int
fn CTD_FIXED(): int
fn CTD_ALL(): int
// sigsend id type
fn P_CTID(): int
// CTFS paths
fn CTFS_ROOT(): string
fn CTFS_PROCESS_TEMPLATE(): string
fn CTFS_PROCESS_LATEST(): string
fn CTFS_PROCESS_BUNDLE(): string
// Contract handle constructor
fn new_contract(id: int, service_name: string): Contract
// Contract accessors
fn contract_id(ct: Contract): int
fn contract_service(ct: Contract): string
fn contract_bundle_fd(ct: Contract): int
// Contract lifecycle
fn setup_template(): int
fn get_latest_contract(): int
fn clear_template(tmpl_fd: int): int
fn open_bundle(): int
fn kill_contract(contract_id: int, sig: int): int
fn abandon_contract(contract_id: int): int
fn adopt_contract(contract_id: int): int
fn is_contract_empty(contract_id: int): bool
// Event reading
fn read_event(bundle_fd: int, out_ctid: [int], out_type: [int]): bool
fn ack_event(contract_id: int, event_id: int): int
end export
// ============================================================================
// FFI declarations — libcontract
// ============================================================================
// Template management
extern "C" fn ct_tmpl_activate(fd: int): int
extern "C" fn ct_tmpl_clear(fd: int): int
extern "C" fn ct_tmpl_set_informative(fd: int, events: int): int
extern "C" fn ct_tmpl_set_critical(fd: int, events: int): int
// Process contract template
extern "C" fn ct_pr_tmpl_set_fatal(fd: int, events: int): int
extern "C" fn ct_pr_tmpl_set_param(fd: int, params: int): int
// Contract status — ct_stathdl_t is an opaque void*
extern "C" fn ct_status_read(fd: int, detail: int, statp: pointer): int
extern "C" proc ct_status_free(stathdl: pointer)
extern "C" fn ct_status_get_id(stathdl: pointer): int
// Process-contract member list — fills *members and *n. Memory is
// owned by the stathdl, freed by ct_status_free. Requires the status
// to have been read at CTD_FIXED detail or higher (CTD_COMMON does
// not include the member list).
extern "C" fn ct_pr_status_get_members(stathdl: pointer, members: [pointer], n: [int]): int
// Contract control
extern "C" fn ct_ctl_abandon(fd: int): int
extern "C" fn ct_ctl_adopt(fd: int): int
// Contract events — ct_evthdl_t is an opaque void*
extern "C" fn ct_event_read(fd: int, evtp: pointer): int
extern "C" proc ct_event_free(evthdl: pointer)
extern "C" fn ct_event_get_ctid(evthdl: pointer): int
extern "C" fn ct_event_get_type(evthdl: pointer): int
extern "C" fn ct_event_get_flags(evthdl: pointer): int
// Process contract event details
extern "C" fn ct_pr_event_get_exitstatus(evthdl: pointer, status: pointer): int
extern "C" fn ct_pr_event_get_pid(evthdl: pointer, pid: pointer): int
// POSIX — sigsend for contract-wide signaling
extern "C" fn sigsend(idtype: int, id: int, sig: int): int
// POSIX — open/close for ctfs paths
extern "C" fn open(path: string, flags: int): int
extern "C" fn close(fd: int): int
// ============================================================================
// Constants — values from Hammerhead kernel headers
// ============================================================================
// Contract event flags (sys/contract/process.h)
fn CT_PR_EV_EMPTY(): int
return 1
end CT_PR_EV_EMPTY
fn CT_PR_EV_FORK(): int
return 2
end CT_PR_EV_FORK
fn CT_PR_EV_EXIT(): int
return 4
end CT_PR_EV_EXIT
fn CT_PR_EV_CORE(): int
return 8
end CT_PR_EV_CORE
fn CT_PR_EV_SIGNAL(): int
return 16
end CT_PR_EV_SIGNAL
fn CT_PR_EV_HWERR(): int
return 32
end CT_PR_EV_HWERR
fn CT_PR_ALLEVENT(): int
return 63
end CT_PR_ALLEVENT
fn CT_PR_ALLFATAL(): int
return 56
end CT_PR_ALLFATAL
// Contract parameter flags (sys/contract/process.h)
fn CT_PR_INHERIT(): int
return 1
end CT_PR_INHERIT
fn CT_PR_NOORPHAN(): int
return 2
end CT_PR_NOORPHAN
fn CT_PR_PGRPONLY(): int
return 4
end CT_PR_PGRPONLY
fn CT_PR_REGENT(): int
return 8
end CT_PR_REGENT
// Status detail levels (sys/contract.h)
fn CTD_COMMON(): int
return 0
end CTD_COMMON
fn CTD_FIXED(): int
return 1
end CTD_FIXED
fn CTD_ALL(): int
return 2
end CTD_ALL
// sigsend id type for contract (sys/procset.h enum position 13,
// between P_ZONEID=12 and P_CPUID=14). Verified on Hammerhead via
// utils/contract_probe.
fn P_CTID(): int
return 13
end P_CTID
// CTFS paths
fn CTFS_ROOT(): string
return "/system/contract"
end CTFS_ROOT
fn CTFS_PROCESS_TEMPLATE(): string
return "/system/contract/process/template"
end CTFS_PROCESS_TEMPLATE
fn CTFS_PROCESS_LATEST(): string
return "/system/contract/process/latest"
end CTFS_PROCESS_LATEST
fn CTFS_PROCESS_BUNDLE(): string
return "/system/contract/process/bundle"
end CTFS_PROCESS_BUNDLE
// Open flags
fn O_RDONLY(): int
return 0
end O_RDONLY
fn O_WRONLY(): int
return 1
end O_WRONLY
fn O_RDWR(): int
return 2
end O_RDWR
// ============================================================================
// Contract type
// ============================================================================
type Contract = struct
id: int
service_name: string
bundle_fd: int
end Contract
fn new_contract(id: int, service_name: string): Contract
return Contract{
id: id,
service_name: service_name,
bundle_fd: 0 - 1
}
end new_contract
fn contract_id(ct: Contract): int
return ct.id
end contract_id
fn contract_service(ct: Contract): string
return ct.service_name
end contract_service
fn contract_bundle_fd(ct: Contract): int
return ct.bundle_fd
end contract_bundle_fd
// ============================================================================
// Contract lifecycle functions
// ============================================================================
// Set up a contract template for forking a service.
// Configures: informative=EMPTY, critical=EMPTY|HWERR, fatal=HWERR,
// params=INHERIT|NOORPHAN. Returns template fd on success, -1 on error.
fn setup_template(): int
let tmpl_fd = open(CTFS_PROCESS_TEMPLATE(), O_RDWR())
if tmpl_fd < 0
return 0 - 1
end if
// Want informative notification when contract becomes empty
let rc1 = ct_tmpl_set_informative(tmpl_fd, CT_PR_EV_EMPTY())
// Critical events: empty (all exited) and hardware error
let rc2 = ct_tmpl_set_critical(tmpl_fd, CT_PR_EV_EMPTY() + CT_PR_EV_HWERR())
// Fatal events: hardware error causes contract death
let rc3 = ct_pr_tmpl_set_fatal(tmpl_fd, CT_PR_EV_HWERR())
// Parameters: inherit contracts on fork, kill orphans on abandon
let rc4 = ct_pr_tmpl_set_param(tmpl_fd, CT_PR_INHERIT() + CT_PR_NOORPHAN())
if rc1 != 0 or rc2 != 0 or rc3 != 0 or rc4 != 0
close(tmpl_fd)
return 0 - 1
end if
let rc5 = ct_tmpl_activate(tmpl_fd)
if rc5 != 0
close(tmpl_fd)
return 0 - 1
end if
return tmpl_fd
end setup_template
// After fork(), retrieve the contract ID for the new child process.
// The kernel writes the latest contract to /system/contract/process/latest.
// Returns contract ID on success, -1 on error.
fn get_latest_contract(): int
let latest_fd = open(CTFS_PROCESS_LATEST(), O_RDONLY())
if latest_fd < 0
return 0 - 1
end if
// ct_status_read writes a handle (void*) to an output parameter.
// We pass a [pointer] array — in C, this is void**, exactly what ct_status_read expects.
unsafe
let stathdl_buf = new [pointer](1)
let rc = ct_status_read(latest_fd, CTD_COMMON(), stathdl_buf)
close(latest_fd)
if rc != 0
return 0 - 1
end if
let stathdl = stathdl_buf[0]
let ctid = ct_status_get_id(stathdl)
ct_status_free(stathdl)
return ctid
end unsafe
end get_latest_contract
// Clear (deactivate) a contract template after fork.
// Returns 0 on success.
fn clear_template(tmpl_fd: int): int
let rc = ct_tmpl_clear(tmpl_fd)
close(tmpl_fd)
return rc
end clear_template
// Open the contract bundle fd for poll()-based event monitoring.
// Returns bundle fd on success, -1 on error.
fn open_bundle(): int
// O_NONBLOCK (0x80 on Hammerhead): ct_event_read on a blocking fd
// BLOCKS waiting for events. Our main loop only enters
// handle_contract_events when poll says the fd is readable, but
// after we drain the events present at that moment, the next
// ct_event_read call sees no event and would block indefinitely
// — wedging the entire main loop, including reap_children for
// SIGCHLD. With O_NONBLOCK, ct_event_read returns EAGAIN once
// we've drained, read_event returns false, the while loop in
// handle_contract_events exits cleanly.
let bundle_fd = open(CTFS_PROCESS_BUNDLE(), O_RDONLY() + 0x80)
return bundle_fd
end open_bundle
// Kill all processes in a contract with the given signal.
// Uses sigsend(P_CTID, contract_id, signal).
// Returns 0 on success, -1 on error.
fn kill_contract(contract_id: int, sig: int): int
return sigsend(P_CTID(), contract_id, sig)
end kill_contract
// Abandon a contract (release ownership, orphan handling applies).
// Opens the contract's ctl fd and issues abandon.
// Returns 0 on success, -1 on error.
fn abandon_contract(contract_id: int): int
// Build path: /system/contract/process/<id>/ctl
// ctl file mode is --w--w--w-, so open with O_WRONLY (O_RDWR fails).
let ctl_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/ctl")
let ctl_fd = open(ctl_path, O_WRONLY())
if ctl_fd < 0
return 0 - 1
end if
let rc = ct_ctl_abandon(ctl_fd)
close(ctl_fd)
return rc
end abandon_contract
// Adopt an existing contract (for zyginit restart recovery).
// Opens the contract's ctl fd and issues adopt.
// Returns 0 on success, -1 on error.
fn adopt_contract(contract_id: int): int
let ctl_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/ctl")
let ctl_fd = open(ctl_path, O_WRONLY())
if ctl_fd < 0
return 0 - 1
end if
let rc = ct_ctl_adopt(ctl_fd)
close(ctl_fd)
return rc
end adopt_contract
// Check whether a contract has zero member processes. Used by the
// post-recovery idempotency step in main.apply_recovered_state — if a
// contract was reported in state.toml but the kernel says it's empty,
// the service exited during the exec gap and we should apply restart
// policy as if a contract-empty event arrived.
//
// Returns true if the contract has zero members OR if the status
// read fails (treat unknown as empty so the recovery path applies
// restart policy rather than leaving a phantom service in RUNNING).
fn is_contract_empty(contract_id: int): bool
let status_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/status")
let st_fd = open(status_path, O_RDONLY())
if st_fd < 0
return true
end if
unsafe
let stathdl_buf = new [pointer](1)
// CTD_FIXED includes process-contract-specific fields like the
// member-PID list, which CTD_COMMON omits.
let rc = ct_status_read(st_fd, CTD_FIXED(), stathdl_buf)
close(st_fd)
if rc != 0
return true
end if
let stathdl = stathdl_buf[0]
let members_buf = new [pointer](1)
let n_buf = new [int](1)
let mrc = ct_pr_status_get_members(stathdl, members_buf, n_buf)
let n = n_buf[0]
ct_status_free(stathdl)
if mrc != 0
return true
end if
return n == 0
end unsafe
end is_contract_empty
// ============================================================================
// Event reading
// ============================================================================
// Read an event from the contract bundle fd.
// Writes the contract ID to out_ctid[0] and event type to out_type[0].
// Returns true on success, false on error or no event.
fn read_event(bundle_fd: int, out_ctid: [int], out_type: [int]): bool
unsafe
let evthdl_buf = new [pointer](1)
let rc = ct_event_read(bundle_fd, evthdl_buf)
if rc != 0
return false
end if
let evthdl = evthdl_buf[0]
out_ctid[0] = ct_event_get_ctid(evthdl)
out_type[0] = ct_event_get_type(evthdl)
ct_event_free(evthdl)
return true
end unsafe
end read_event
// Acknowledge a contract event.
// Opens the contract's event fd and issues ack.
fn ack_event(contract_id: int, event_id: int): int
let ev_path = str.concat(str.concat("/system/contract/process/", int_to_str(contract_id)), "/events")
let ev_fd = open(ev_path, O_RDONLY())
if ev_fd < 0
return 0 - 1
end if
// For now, we just close — full ack requires ct_ctl_ack on the ctl fd
close(ev_fd)
return 0
end ack_event
// ============================================================================
// Helpers
// ============================================================================
// Convert int to string (for building ctfs paths)
fn int_to_str(n: int): string
if n == 0
return "0"
end if
mut value = n
if n < 0
value = 0 - n
end if
mut result = ""
while value > 0
let digit = value % 10
result = str.concat(str.substring("0123456789", digit, 1), result)
value = value / 10
end while
if n < 0
result = str.concat("-", result)
end if
return result
end int_to_str
end module
|