nx_cgroup_v2_probe.nx source
↩ module page · 164 lines · 6273 B
1// nx_cgroup_v2_probe.nx -- Linux cgroups v2 host detection.
2//
3// Stage A foundation per NISHI_ELDER_AI_OFF_DOCKER_2026_05_20.md.
4// To enforce nx_budget ceilings via the host kernel (memory.max,
5// pids.max, cpu.weight), the substrate must FIRST detect whether
6// cgroups v2 is available on the host. This primitive is the
7// inspection-only foundation; mutation primitives (mkdir cgroup,
8// write memory.max, attach pid) ship in a later session ON A
9// LINUX HOST where their smokes can actually run.
10//
11// Detection works by attempting to read /sys/fs/cgroup/cgroup.controllers:
12// - File exists + readable: cgroups v2 is mounted; returns 1
13// - File missing / unreadable: cgroups v2 unavailable; returns 0
14//
15// Host coverage:
16// Linux with cgroup v2 -> available()=1
17// Linux with cgroup v1 -> available()=0 (no v2 root)
18// Cygwin / Windows-mounted bash -> available()=0 (no /sys at all)
19// macOS -> available()=0 (no cgroups)
20// qemu-user-static -> passthrough to host kernel; if host
21// has cgroups v2, returns 1
22//
23// HONEST scope this V1:
24// - Read-only probe; no mutation
25// - Single canonical file check (cgroup.controllers)
26// - No delegated-controllers parsing (queued)
27// - No process-self-cgroup parsing (queued; reads /proc/self/cgroup)
28// - No mutation primitives (queued for Linux-host session)
29//
30// genealogy_id: linux_cgroup_v2_2018 + systemd_cgroup_delegation_2017 +
31// cardinal_2026-05-20_elder_ai_off_docker
32// lineage_id: substrate_cgroup_v2_probe_v1
33//
34// nx_capability_manifest:
35// variant_class: cgroup_v2_probe
36// variant_id: cgroup_v2_probe_v1_read_only
37// requires_isa: [rv64imac, x86_64, aarch64]
38// requires_syscalls: [openat, read, close, mmap]
39// requires_ram_min_b: 4096
40// tier_floor: NX_TIER_INF_MOBILE
41// tier_ceiling: NX_TIER_INF_HPC
42// cost_model:
43// flops_per_n: 0.0
44// bytes_per_n: 256.0 // controllers file < 256 bytes
45// syscalls_per_n: 3.0 // openat + read + close
46// adversary_class: THREAT_OPPORTUNISTIC
47//
48// nx_safety_envelope:
49// intended_use: "Host cgroups v2 detection; read-only probe;
50// foundation for nx_budget enforcement wiring"
51// sil_target: SIL2
52// evidence: [read_only, no_mutation, honest_skip_on_unavailable]
53// verdict: NOT_YET_EVALUATED
54
55import "nx_syscalls.nx"
56
57// ===== Verdicts ==================================================
58const NX_CG_AVAILABLE: i64 = 1
59const NX_CG_UNAVAILABLE: i64 = 0
60const NX_CG_PROBE_BAD_INPUT: i64 = -1
61
62// ===== Path constructor ==========================================
63// Builds "/sys/fs/cgroup/cgroup.controllers" as a NUL-terminated
64// byte string. Each byte written explicitly to keep the path
65// auditable and reproducible across builds (no string literal
66// optimizer surprises).
67func _cg_path_controllers(out: *u8) -> i64 {
68 // "/sys/fs/cgroup/cgroup.controllers" = 33 chars + NUL
69 out[0] = 47 as u8 // '/'
70 out[1] = 115 as u8 // 's'
71 out[2] = 121 as u8 // 'y'
72 out[3] = 115 as u8 // 's'
73 out[4] = 47 as u8 // '/'
74 out[5] = 102 as u8 // 'f'
75 out[6] = 115 as u8 // 's'
76 out[7] = 47 as u8 // '/'
77 out[8] = 99 as u8 // 'c'
78 out[9] = 103 as u8 // 'g'
79 out[10] = 114 as u8 // 'r'
80 out[11] = 111 as u8 // 'o'
81 out[12] = 117 as u8 // 'u'
82 out[13] = 112 as u8 // 'p'
83 out[14] = 47 as u8 // '/'
84 out[15] = 99 as u8 // 'c'
85 out[16] = 103 as u8 // 'g'
86 out[17] = 114 as u8 // 'r'
87 out[18] = 111 as u8 // 'o'
88 out[19] = 117 as u8 // 'u'
89 out[20] = 112 as u8 // 'p'
90 out[21] = 46 as u8 // '.'
91 out[22] = 99 as u8 // 'c'
92 out[23] = 111 as u8 // 'o'
93 out[24] = 110 as u8 // 'n'
94 out[25] = 116 as u8 // 't'
95 out[26] = 114 as u8 // 'r'
96 out[27] = 111 as u8 // 'o'
97 out[28] = 108 as u8 // 'l'
98 out[29] = 108 as u8 // 'l'
99 out[30] = 101 as u8 // 'e'
100 out[31] = 114 as u8 // 'r'
101 out[32] = 115 as u8 // 's'
102 out[33] = 0 as u8 // NUL
103 return 33
104}
105
106// ===== Available probe ==========================================
107// Returns 1 if /sys/fs/cgroup/cgroup.controllers can be opened
108// AND at least one byte read. Returns 0 on ENOENT, EACCES, or
109// any other failure mode. No mutation; safe to call on any host.
110
111func nx_cgroup_v2_available() -> i64 {
112 let path: *u8 = sys_mmap(64)
113 _cg_path_controllers(path)
114 let fd: i64 = sys_openat_rd(path)
115 if fd < 0 { return NX_CG_UNAVAILABLE }
116 // Try to read at least one byte. Cgroup v2 controllers file
117 // is never empty on a working v2 host; if read returns <= 0
118 // the host is not properly delegating v2.
119 let probe_buf: *u8 = sys_mmap(8)
120 let n: i64 = sys_read(fd, probe_buf, 1)
121 sys_close(fd)
122 if n <= 0 { return NX_CG_UNAVAILABLE }
123 return NX_CG_AVAILABLE
124}
125
126// ===== Read controllers file ====================================
127// Reads /sys/fs/cgroup/cgroup.controllers into out_buf (up to
128// max_len bytes). Returns bytes read, or -1 on
129// unavailable / null-input.
130//
131// On a typical v2 host the contents are a space-separated list
132// like "cpuset cpu io memory pids" with a trailing newline,
133// typically under 100 bytes.
134
135func nx_cgroup_v2_controllers(out_buf: *u8, max_len: i64) -> i64 {
136 if (out_buf as i64) == 0 { return -1 }
137 if max_len <= 0 { return -1 }
138 let path: *u8 = sys_mmap(64)
139 _cg_path_controllers(path)
140 let fd: i64 = sys_openat_rd(path)
141 if fd < 0 { return -1 }
142 var total: i64 = 0
143 var go: i64 = 1
144 while go == 1 {
145 let remaining: i64 = max_len - total
146 if remaining <= 0 { go = 0 }
147 if go == 1 {
148 let tail: *u8 = ((out_buf as i64) + total) as *u8
149 let n: i64 = sys_read(fd, tail, remaining)
150 if n <= 0 { go = 0 }
151 if n > 0 { total = total + n }
152 }
153 }
154 sys_close(fd)
155 return total
156}
157
158// ===== Verdict accessor =========================================
159// Returns 1 if 'v' is a valid NX_CG_* verdict.
160func nx_cg_verdict_is_valid(v: i64) -> i64 {
161 if v == NX_CG_AVAILABLE { return 1 }
162 if v == NX_CG_UNAVAILABLE { return 1 }
163 return 0
164}