|
root / src / contract.reef
contract.reef Reef 502 lines 15.0 KB
  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