nx_http_probe_lib.nx source
↩ module page · 414 lines · 20417 B
1// nx_http_probe_lib.nx -- THE BOUNDED LOOPBACK HTTP LIVENESS PROBE + DEAD-MAN HEARTBEAT STAMPER.
2//
3// WHY THIS EXISTS (2026-08-20). The estate's only compensating control over a FAIL-OPEN authentication
4// path was a curl one-liner in cron.reg:
5// curl -s --max-time 8 http://127.0.0.1:18096/api/cap/status | grep -q '"secure":true' \
6// && date +ts=%s > knowledge/status/capplane.log
7// The cron row FIRED and the command FAILED, and because the row DISCARDS ALL OUTPUT its own failure was
8// unobservable: 7 of 8 conjuncts were proven true and the 8th ("does curl run") was structurally
9// unmeasurable. The cap-plane watch went stale at 10.9x its budget and nobody could say which link broke.
10// **** A PROBE THAT FAILS SILENTLY IS NOT A CONTROL, IT IS A DECORATION. ****
11// So the single disqualifying property for the replacement is a silent failure mode. EVERY exit path here
12// carries a NAMED reason, printed on stdout AND stderr, and the verdict line is LAST so a positional
13// reader (gv_last_line, nx_gate_green) can anchor on it without matching text anywhere else.
14//
15// WHY A LIB AND NOT ONE FILE: the CLI (nx_http_probe.nx) and the gate (nx_http_probe_gate.nx) both need
16// the same logic, and a gate that re-implements its subject proves nothing. One ruler, two callers.
17//
18// LOOPBACK-ONLY BY CONSTRUCTION -- THIS IS A SAFETY PROPERTY, NOT A LIMITATION. The destination address
19// is hard-pinned to 127.0.0.1 and cannot be passed in. cron.reg's own cap-plane comment records why:
20// during the 2026-08-06 outage the PUBLIC url returned insecure_placeholder/secure=false while loopback
21// :18096 returned provisioned/secure=true, because the public :443 answer came from a DIFFERENT BINARY
22// (DSM nginx co-squat, D008) -- and that sent the diagnosis down a false path for an hour.
23// **** A HEALTH ENDPOINT ANSWERS FOR THE PROCESS THAT SERVED IT, NEVER FOR THE SERVICE. ****
24// A sibling seat had to re-derive that this session after taking the convenient answer. Pinning the
25// address makes the mistake unrepresentable rather than merely documented.
26//
27// COMPOSED, NOT RE-IMPLEMENTED. Three proven primitives do the work:
28// nx_connect_bounded (nx_connect.nx) -- SO_RCVTIMEO does NOT bound connect(); this does
29// nx_http_client_build_request(nx_http_client.nx) -- the ONE request-format definition in the estate
30// sys_set_socket_timeout (nx_syscalls.nx) -- SO_RCVTIMEO/SO_SNDTIMEO
31// The only new code is a DEADLINE-AWARE drain. nx_http_client_get's own _drain has no read bound, so a
32// daemon that accepts and then wedges -- WHICH IS EXACTLY THE 2026-08-06 INCIDENT -- parks the probe
33// forever. On a */5 beat that piles up processes, which is a resource bug even when the feature works.
34// nx_http_client_get is otherwise the right organ and is deliberately left byte-untouched: editing a lib
35// with ~100 consumers to add one caller's timeout is a fleet-wide blast radius for a local need.
36//
37// THE BUDGET IS AN ARGUMENT, NOT A CONSTANT. Rule 11: no magic numbers. The caller states the budget and
38// states its derivation at the call site (cron.reg). Worst-case wall time is ~2x budget: the connect leg
39// and the receive leg each get the full budget, deliberately, so that "slow to connect" and "slow to
40// answer" are separately diagnosable instead of sharing one squeezed allowance. Pick budget <= beat/2.
41//
42// NO SILENT CAPS. HP_BODY_CAP bounds a NETWORK body, whose size is genuinely unknowable in advance --
43// the one case the no-buffer-caps law permits a bound. It is named for that single purpose and its
44// truncation ANNOUNCES: a pattern that is absent from a TRUNCATED body is UNPROVEN, not absent, and it
45// gets its own exit code rather than being reported as a clean miss.
46//
47// expect_exit: 0 license_tier: ORIGINAL No hw writes (Rule 26).
48// Imports are named EXPLICITLY rather than relied on transitively. nx_http_client.nx does pull in
49// nx_syscalls.nx and nx_connect.nx today, but a consumer that depends on somebody else's import list is
50// one refactor away from a build break it did not cause.
51import "nx_syscalls.nx"
52import "nx_connect.nx"
53import "nx_http_client.nx"
54import "nx_gate.nx"
55
56// ---- NAMED EXIT CODES. Every one of these is a DIAGNOSIS, never a bare failure. -------------------
57// 0/1/2/3 are the values the pre-2026-08-20 organ already returned; keeping them is Rule 19 (additive).
58const HP_EXIT_OK: i64 = 0
59const HP_EXIT_USAGE: i64 = 1
60const HP_EXIT_CONNECT_FAILED: i64 = 2
61const HP_EXIT_PATTERN_ABSENT: i64 = 3
62const HP_EXIT_STATUS_NOT_2XX: i64 = 4
63const HP_EXIT_STAMP_WRITE_FAILED:i64 = 5
64const HP_EXIT_RECV_TIMEOUT: i64 = 6
65const HP_EXIT_SEND_FAILED: i64 = 7
66const HP_EXIT_BODY_TRUNCATED: i64 = 8
67const HP_EXIT_PATH_TOO_LONG: i64 = 9
68const HP_EXIT_SOCKET_FAILED: i64 = 10
69const HP_EXIT_EMPTY_RESPONSE: i64 = 11
70const HP_EXIT_NOT_HTTP: i64 = 12
71
72// The legacy positional form defaulted to this port (redirect.elf). Preserved so an existing caller of
73// the two-and-three-argument form keeps its meaning; the `watch` verb takes the port explicitly.
74const HP_LEGACY_DEFAULT_PORT: i64 = 8080
75
76// A NETWORK BODY IS THE ONE SIZE YOU CANNOT KNOW IN ADVANCE. Bounded, named for this one purpose, and
77// truncation is ANNOUNCED and given its own exit code -- never a silent short read.
78const HP_BODY_CAP: i64 = 4194304
79
80// Matches the request buffer nx_http_client_get itself allocates for the SAME builder. This is adopting
81// the incumbent's declared bound, not making a second independent guess about the same thing.
82const HP_REQ_CAP: i64 = 4096
83// Half of HP_REQ_CAP, so the builder's fixed header block (request line + Host + UA + derived Accept
84// caps + Connection: close, a few hundred bytes) can never push a legal path past the buffer. A path
85// longer than this is REFUSED BY NAME rather than silently overflowing.
86const HP_PATH_MAX: i64 = 2048
87
88const HP_ADDR_BYTES: i64 = 16
89const HP_STAMP_CAP: i64 = 64
90const HP_MODE_0644: i64 = 420
91const HP_FD_STDOUT: i64 = 1
92const HP_FD_STDERR: i64 = 2
93const HP_ASCII_ZERO: i64 = 48
94const HP_ASCII_NINE: i64 = 57
95const HP_ASCII_SPACE: i64 = 32
96const HP_ASCII_NL: i64 = 10
97const HP_B10: i64 = 10
98const HP_STATUS_DIGITS: i64 = 3
99const HP_STATUS_2XX_LO: i64 = 200
100const HP_STATUS_2XX_HI: i64 = 300
101const HP_MS_PER_SEC: i64 = 1000
102const HP_FLAG_SLOTS: i64 = 32
103// The destination, pinned. See the LOOPBACK-ONLY paragraph in the header.
104const HP_LOOPBACK_A: i64 = 127
105const HP_LOOPBACK_B: i64 = 0
106const HP_LOOPBACK_C: i64 = 0
107const HP_LOOPBACK_D: i64 = 1
108
109// drain outcome slots, named so a reader never has to count array indices
110const HP_FL_CLEAN_EOF: i64 = 0
111const HP_FL_CAP_HIT: i64 = 1
112const HP_FL_DEADLINE: i64 = 2
113const HP_FL_READS: i64 = 3
114
115func hp_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
116
117func hp_werr(s: *u8) -> i64 { sys_write(HP_FD_STDERR, s, hp_strlen(s)); return 0 }
118
119// exact NUL-terminated equality. Used to recognise the `watch` verb: a SUBSTRING test would accept any
120// argument containing the word, which is the loose-matching class this estate keeps paying for.
121func hp_streq(a: *u8, b: *u8) -> i64 {
122 var i: i64 = 0
123 while a[i] != (0 as u8) {
124 if b[i] != a[i] { return 0 }
125 i = i + 1
126 }
127 if b[i] != (0 as u8) { return 0 }
128 return 1
129}
130
131func hp_nerr(v: i64) -> i64 {
132 let t: *u8 = sys_mmap(HP_STAMP_CAP)
133 let o: *u8 = sys_mmap(HP_STAMP_CAP)
134 var m: i64 = v
135 var w: i64 = 0
136 if m < 0 { o[0] = 45 as u8; w = 1; m = 0 - m }
137 var k: i64 = 0
138 if m == 0 { t[0] = HP_ASCII_ZERO as u8; k = 1 }
139 while m > 0 { t[k] = (HP_ASCII_ZERO + (m % HP_B10)) as u8; m = m / HP_B10; k = k + 1 }
140 var q: i64 = k - 1
141 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 }
142 sys_write(HP_FD_STDERR, o, w)
143 sys_munmap(t, HP_STAMP_CAP)
144 sys_munmap(o, HP_STAMP_CAP)
145 return 0
146}
147
148// parse up to `len` decimal digits at buf[off..]; stops at the first non-digit
149func hp_atoi(buf: *u8, off: i64, len: i64) -> i64 {
150 var v: i64 = 0
151 var i: i64 = 0
152 while i < len {
153 let d: i64 = buf[off + i] as i64
154 if d >= HP_ASCII_ZERO {
155 if d <= HP_ASCII_NINE { v = v * HP_B10 + (d - HP_ASCII_ZERO) } else { i = len }
156 } else { i = len }
157 i = i + 1
158 }
159 return v
160}
161
162// 1 if needle occurs in buf[0..n). An empty needle trivially occurs (callers gate on length first).
163func hp_contains(buf: *u8, n: i64, needle: *u8) -> i64 {
164 let nl: i64 = hp_strlen(needle)
165 if nl == 0 { return 1 }
166 var i: i64 = 0
167 while i + nl <= n {
168 var m: i64 = 1
169 var j: i64 = 0
170 while j < nl { if buf[i+j] != needle[j] { m = 0; j = nl } else { j = j + 1 } }
171 if m == 1 { return 1 }
172 i = i + 1
173 }
174 return 0
175}
176
177// THE STATUS CODE, DERIVED -- never a hand-counted offset beside a string literal. The old organ read
178// buf[9..12] with 9 hand-counted from "HTTP/1.1 ": change the prefix, forget the number, and the parser
179// silently reads the wrong window while still compiling. This binds the prefix once and scans for the
180// separator, so it also accepts HTTP/1.0. Returns the code, or -1 if this is not an HTTP status line.
181func hp_status_of(buf: *u8, n: i64) -> i64 {
182 let pfx: *u8 = "HTTP/" as *u8
183 let pl: i64 = hp_strlen(pfx)
184 if n < pl { return 0 - 1 }
185 var i: i64 = 0
186 while i < pl { if buf[i] != pfx[i] { return 0 - 1 } i = i + 1 }
187 var s: i64 = pl
188 var at: i64 = 0 - 1
189 while s < n {
190 if buf[s] == (HP_ASCII_SPACE as u8) { at = s + 1; s = n } else { s = s + 1 }
191 }
192 if at < 0 { return 0 - 1 }
193 if at + HP_STATUS_DIGITS > n { return 0 - 1 }
194 var v: i64 = 0
195 var k: i64 = 0
196 while k < HP_STATUS_DIGITS {
197 let d: i64 = buf[at + k] as i64
198 if d < HP_ASCII_ZERO { return 0 - 1 }
199 if d > HP_ASCII_NINE { return 0 - 1 }
200 v = v * HP_B10 + (d - HP_ASCII_ZERO)
201 k = k + 1
202 }
203 return v
204}
205
206// THE DEADLINE-AWARE DRAIN -- the only genuinely new code in this organ, and the reason it exists.
207// nx_http_client's _drain reads until EOF with no bound: a daemon that ACCEPTS and then wedges parks the
208// caller forever, which is precisely the 2026-08-06 failure this probe is the detector for.
209// Distinguishes the three ways a read loop can end, because they have different remedies:
210// CLEAN_EOF peer closed -> the response is COMPLETE (the builder sends Connection: close)
211// CAP_HIT buffer full -> the response is TRUNCATED; an absent pattern is UNPROVEN, not absent
212// DEADLINE budget spent -> the response is INCOMPLETE; nothing about the body can be concluded
213func hp_drain(fd: i64, buf: *u8, cap: i64, deadline_sec: i64, fl: *i64) -> i64 {
214 fl[HP_FL_CLEAN_EOF] = 0
215 fl[HP_FL_CAP_HIT] = 0
216 fl[HP_FL_DEADLINE] = 0
217 fl[HP_FL_READS] = 0
218 var off: i64 = 0
219 var keep: i64 = 1
220 while keep == 1 {
221 if off >= cap { fl[HP_FL_CAP_HIT] = 1; keep = 0 }
222 else {
223 if sys_now_realtime_sec() >= deadline_sec { fl[HP_FL_DEADLINE] = 1; keep = 0 }
224 else {
225 let r: i64 = sys_read(fd, ((buf as i64) + off) as *u8, cap - off)
226 fl[HP_FL_READS] = fl[HP_FL_READS] + 1
227 if r == 0 { fl[HP_FL_CLEAN_EOF] = 1; keep = 0 }
228 else {
229 // r < 0 is SO_RCVTIMEO expiry (EAGAIN) or a socket error. Either way this read loop
230 // did not observe a complete response, so it is reported as budget-exhausted rather
231 // than quietly returning a partial body that reads like a whole one.
232 if r < 0 { fl[HP_FL_DEADLINE] = 1; keep = 0 } else { off = off + r }
233 }
234 }
235 }
236 }
237 return off
238}
239
240// THE HEARTBEAT. Format is `ts=<epoch>\n`, byte-compatible with the `date +ts=%s` the cron one-liner
241// wrote, because nx_cron_watch's cw_num_after parses the marker `ts=` and the cap-plane watch row
242// declares that marker. CHANGING THE MARKER WOULD LEAVE THE WATCH PERMANENTLY STALE WHILE THE STAMPER
243// LOOKED HEALTHY, which is the failure one row above it in the plane (surfsentinel) already warns about.
244//
245// PLAIN TRUNCATE-AND-WRITE, DELIBERATELY, NOT A TEMP+RENAME. The failure mode of a torn read here is a
246// SHORTER number, i.e. a SMALLER epoch, i.e. a LARGER age -- so a reader that catches a partial write
247// reads STALE. That fails in the safe direction for a dead-man's switch, and it leaves no `.nxnew`
248// litter behind a crash. Returns bytes written, or -1 -- and -1 is a NAMED exit for the caller, never
249// a silent no-op: a stamper that cannot write is exactly as blind as one that never ran.
250func hp_write_stamp(path: *u8, ts: i64) -> i64 {
251 let fd: i64 = sys_openat_wr(path, HP_MODE_0644)
252 if fd < 0 { return 0 - 1 }
253 let b: *u8 = sys_mmap(HP_STAMP_CAP)
254 let t: *u8 = sys_mmap(HP_STAMP_CAP)
255 let pfx: *u8 = "ts=" as *u8
256 var o: i64 = 0
257 var i: i64 = 0
258 while pfx[i] != (0 as u8) { b[o] = pfx[i]; o = o + 1; i = i + 1 }
259 var m: i64 = ts
260 var k: i64 = 0
261 if m == 0 { t[0] = HP_ASCII_ZERO as u8; k = 1 }
262 while m > 0 { t[k] = (HP_ASCII_ZERO + (m % HP_B10)) as u8; m = m / HP_B10; k = k + 1 }
263 var q: i64 = k - 1
264 while q >= 0 { b[o] = t[q]; o = o + 1; q = q - 1 }
265 b[o] = HP_ASCII_NL as u8
266 o = o + 1
267 let w: i64 = sys_write(fd, b, o)
268 sys_fsync(fd)
269 sys_close(fd)
270 sys_munmap(b, HP_STAMP_CAP)
271 sys_munmap(t, HP_STAMP_CAP)
272 if w != o { return 0 - 1 }
273 return o
274}
275
276// THE REASON TRAVELS WITH THE EXIT CODE. A caller reading only the number still gets a diagnosis,
277// and the log line names the conjunct that failed instead of leaving the reader to guess the alarming
278// third. `connect-failed` deliberately covers refused / unreachable / connect-budget-exceeded together:
279// nx_connect_bounded returns -1 for all three by design and the estate has no variant that reports
280// which, so splitting them here would be a distinction this organ cannot actually make. NAMED, not
281// pretended.
282func hp_reason(code: i64) -> *u8 {
283 if code == HP_EXIT_OK { return "none" as *u8 }
284 if code == HP_EXIT_USAGE { return "usage" as *u8 }
285 if code == HP_EXIT_CONNECT_FAILED { return "connect-failed" as *u8 }
286 if code == HP_EXIT_PATTERN_ABSENT { return "pattern-absent" as *u8 }
287 if code == HP_EXIT_STATUS_NOT_2XX { return "status-not-2xx" as *u8 }
288 if code == HP_EXIT_STAMP_WRITE_FAILED { return "stamp-write-failed" as *u8 }
289 if code == HP_EXIT_RECV_TIMEOUT { return "recv-timeout" as *u8 }
290 if code == HP_EXIT_SEND_FAILED { return "send-failed" as *u8 }
291 if code == HP_EXIT_BODY_TRUNCATED { return "body-truncated-pattern-unproven" as *u8 }
292 if code == HP_EXIT_PATH_TOO_LONG { return "path-too-long" as *u8 }
293 if code == HP_EXIT_SOCKET_FAILED { return "socket-failed" as *u8 }
294 if code == HP_EXIT_EMPTY_RESPONSE { return "empty-response" as *u8 }
295 if code == HP_EXIT_NOT_HTTP { return "not-http" as *u8 }
296 return "unknown" as *u8
297}
298
299// THE VERDICT LINE IS LAST AND IT IS ALSO ON STDERR WHEN IT FAILS. The cron row this replaces threw its
300// output away; a replacement whose failure is only visible to someone who kept stdout would inherit the
301// exact defect. stderr survives `>/dev/null` and lands in the mail/log of every cron implementation.
302// THERE IS DELIBERATELY NO QUIET FLAG. The defect being retired is a probe with no observable outcome;
303// a switch that restores that silence is the defect with a config key. Every run says what happened.
304func hp_emit(code: i64) -> i64 {
305 gw("NX-HTTP-PROBE verdict=" as *u8)
306 if code == HP_EXIT_OK { gw("OK" as *u8) } else { gw("FAIL" as *u8) }
307 gw(" reason=" as *u8); gw(hp_reason(code))
308 gw(" exit=" as *u8); gn(code); gw("\n" as *u8)
309 if code != HP_EXIT_OK {
310 hp_werr("NX-HTTP-PROBE verdict=FAIL reason=" as *u8)
311 hp_werr(hp_reason(code))
312 hp_werr(" exit=" as *u8); hp_nerr(code); hp_werr("\n" as *u8)
313 }
314 return code
315}
316
317// ---- THE ORGAN. One function, one job, callable from the CLI and from the gate. --------------------
318// expect: empty string = no pattern assertion. stamp_path: empty string = no heartbeat.
319// verbose: 1 prints the per-leg progress lines before the verdict; 0 prints the verdict only.
320// ORDER IS THE CONTRACT: transport, then status, then pattern, THEN stamp. The heartbeat is the LAST
321// thing that happens and only on a fully clean run, so every named failure leaves the watch to go
322// STALE rather than quietly reading healthy. That ordering is the whole safety property and the gate
323// asserts the ABSENCE of the stamp on every failing path, not merely the exit code.
324func hp_run(port: i64, path: *u8, expect: *u8, budget_sec: i64, stamp_path: *u8, verbose: i64) -> i64 {
325 let plen: i64 = hp_strlen(path)
326 if plen > HP_PATH_MAX { return hp_emit(HP_EXIT_PATH_TOO_LONG) }
327 if budget_sec <= 0 { return hp_emit(HP_EXIT_USAGE) }
328
329 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
330 if fd < 0 { return hp_emit(HP_EXIT_SOCKET_FAILED) }
331
332 let addr: *u8 = sys_mmap(HP_ADDR_BYTES)
333 nx_http_client_sockaddr_ipv4(addr, HP_LOOPBACK_A, HP_LOOPBACK_B, HP_LOOPBACK_C, HP_LOOPBACK_D, port)
334 if nx_connect_bounded(fd, addr, HP_ADDR_BYTES, budget_sec * HP_MS_PER_SEC) != 0 {
335 sys_close(fd)
336 sys_munmap(addr, HP_ADDR_BYTES)
337 return hp_emit(HP_EXIT_CONNECT_FAILED)
338 }
339 sys_set_socket_timeout(fd, budget_sec)
340
341 let host: *u8 = "localhost" as *u8
342 let req: *u8 = sys_mmap(HP_REQ_CAP)
343 let rl: i64 = nx_http_client_build_request(path, plen, host, hp_strlen(host), req)
344 let wr: i64 = sys_write(fd, req, rl)
345 sys_munmap(req, HP_REQ_CAP)
346 if wr != rl {
347 sys_close(fd)
348 sys_munmap(addr, HP_ADDR_BYTES)
349 return hp_emit(HP_EXIT_SEND_FAILED)
350 }
351
352 let deadline: i64 = sys_now_realtime_sec() + budget_sec
353 let buf: *u8 = sys_mmap(HP_BODY_CAP)
354 let fl: *i64 = sys_mmap(HP_FLAG_SLOTS) as *i64
355 let n: i64 = hp_drain(fd, buf, HP_BODY_CAP, deadline, fl)
356 sys_close(fd)
357 sys_munmap(addr, HP_ADDR_BYTES)
358
359 var code: i64 = HP_EXIT_OK
360 var status: i64 = 0 - 1
361
362 if fl[HP_FL_DEADLINE] == 1 { code = HP_EXIT_RECV_TIMEOUT }
363 if code == HP_EXIT_OK { if n == 0 { code = HP_EXIT_EMPTY_RESPONSE } }
364 if code == HP_EXIT_OK {
365 status = hp_status_of(buf, n)
366 if status < 0 { code = HP_EXIT_NOT_HTTP }
367 }
368 if code == HP_EXIT_OK {
369 if status < HP_STATUS_2XX_LO { code = HP_EXIT_STATUS_NOT_2XX }
370 if status >= HP_STATUS_2XX_HI { code = HP_EXIT_STATUS_NOT_2XX }
371 }
372
373 if verbose == 1 {
374 gw("GET " as *u8); gw(path); gw(" -> status=" as *u8); gn(status)
375 gw(" bytes=" as *u8); gn(n)
376 gw(" reads=" as *u8); gn(fl[HP_FL_READS])
377 gw(" clean_eof=" as *u8); gn(fl[HP_FL_CLEAN_EOF])
378 gw(" truncated=" as *u8); gn(fl[HP_FL_CAP_HIT])
379 gw(" deadline_hit=" as *u8); gn(fl[HP_FL_DEADLINE])
380 gw("\n" as *u8)
381 }
382
383 if code == HP_EXIT_OK {
384 if hp_strlen(expect) > 0 {
385 let found: i64 = hp_contains(buf, n, expect)
386 if verbose == 1 {
387 gw(" expect '" as *u8); gw(expect); gw("': " as *u8)
388 if found == 1 { gw("FOUND\n" as *u8) } else { gw("MISSING\n" as *u8) }
389 }
390 if found == 0 {
391 // ABSENCE NEEDS EXHAUSTIVE COVERAGE. A body that hit the cap was not fully read, so
392 // "the pattern is not there" is not a conclusion this run is entitled to publish.
393 if fl[HP_FL_CAP_HIT] == 1 { code = HP_EXIT_BODY_TRUNCATED } else { code = HP_EXIT_PATTERN_ABSENT }
394 }
395 }
396 }
397 sys_munmap(buf, HP_BODY_CAP)
398
399 if code == HP_EXIT_OK {
400 if hp_strlen(stamp_path) > 0 {
401 let now: i64 = sys_now_realtime_sec()
402 let wrote: i64 = hp_write_stamp(stamp_path, now)
403 if wrote < 0 { code = HP_EXIT_STAMP_WRITE_FAILED }
404 else {
405 if verbose == 1 {
406 gw(" stamp " as *u8); gw(stamp_path); gw(": WROTE ts=" as *u8); gn(now)
407 gw(" bytes=" as *u8); gn(wrote); gw("\n" as *u8)
408 }
409 }
410 }
411 }
412 sys_munmap(fl as *u8, HP_FLAG_SLOTS)
413 return hp_emit(code)
414}