A dyn value the inspector reads is rendered on the program's own thread, so one that traps or faults is a nested break and the daemon keeps answering.

This commit is contained in:
Joseph Ferano 2026-09-25 20:24:34 +07:00
parent ae23787c04
commit 72c35ff05c
5 changed files with 182 additions and 58 deletions

View File

@ -2186,30 +2186,41 @@ let backtrace_op t =
the reader never saw. So a write names the generation it was built against, the reader never saw. So a write names the generation it was built against,
and the agent compares it against the stop actually in force at the moment and the agent compares it against the stop actually in force at the moment
it claims the job, and drops it otherwise. *) it claims the job, and drops it otherwise. *)
let run_render_thunk ?at_stop t ~tag let rec run_render_thunk ?at_stop t ~tag
~(c : Session.change) : (string, string) result = ~(c : Session.change) : (string, string) result =
let before = match result t with Some (g, _) -> g | None -> 0L in (* The counters are read *before* the build, not before the wait: the
(* Read *before* the build, not before the wait: the resume this is watching resume this is watching for can land while llc is still running, and the
for can land while llc is still running, and the job it kills is this one. job it kills is this one. *)
[None] when the program cannot say, in which case nothing below compares let mark = job_mark ?at_stop t in
against it — a missing count is no evidence either way. *)
let watched = at_stop <> None in
let refused_before = if watched then refusals t else None in
let resumed () =
match (refused_before, if watched then refusals t else None) with
| Some (before, _), Some (now, why) when now > before -> Some why
| _ -> None
in
t.n <- t.n + 1; t.n <- t.n + 1;
let out = Filename.concat t.dir (Printf.sprintf "%s%d.so" tag t.n) in let out = Filename.concat t.dir (Printf.sprintf "%s%d.so" tag t.n) in
match build_module c ~debug:t.session.Session.debug ~out with match build_module c ~debug:t.session.Session.debug ~out with
| exception Failure m -> Error m | exception Failure m -> Error m
| _ -> | _ ->
(match await_job mark t (fun () ->
(match at_stop with match at_stop with
| Some gen -> deliver_at_stop t ~gen out | Some gen -> deliver_at_stop t ~gen out
| None -> deliver t out) | None -> deliver t out)
with
(* What a wait compares against: the result generation, and the agent's
count of dropped jobs when the job names a stop. [None] where the program
cannot say, in which case nothing compares against it — a missing count is
no evidence either way. *)
and job_mark ?at_stop t =
let before = match result t with Some (g, _) -> g | None -> 0L in
let refused_before = if at_stop <> None then refusals t else None in
(before, refused_before, stop_gen t)
(* Hand a job to the program with [send], and wait for the value it writes
into the result buffer. Shared by a compiled thunk and by the [dyn] verb,
which renders a dyn value on the program's own thread. *)
and await_job (before, refused_before, gen) t send : (string, string) result =
let resumed () =
match (refused_before, if refused_before <> None then refusals t else None) with
| Some (before, _), Some (now, why) when now > before -> Some why
| _ -> None
in
(match send () with
| exception Unix.Unix_error (e, _, _) -> | exception Unix.Unix_error (e, _, _) ->
Error (unreachable t e) Error (unreachable t e)
| "ok" -> | "ok" ->
@ -2233,6 +2244,15 @@ let run_render_thunk ?at_stop t ~tag
| _ -> | _ ->
match resumed () with match resumed () with
| Some why -> Error why | Some why -> Error why
| None when (match (gen, stop_gen t) with
| Some g, Some g' -> g' > g
| _ -> false) ->
(* The job itself stopped the program — a trap in the value it
was rendering — and the break it pushed is holding the thread.
Said now rather than after the five seconds. *)
Error
"the program stopped again while producing the value, and the \
break buffer is holding it: take a restart there, or abort"
| None -> | None ->
(* Two sentences for the same silence, because there are two (* Two sentences for the same silence, because there are two
causes and each names a different thing to go and look at. causes and each names a different thing to go and look at.
@ -2338,11 +2358,19 @@ let agent_mem t : Inspect.mem =
"<ptr", as the thunk's [reg-emit] wrote it. *) "<ptr", as the thunk's [reg-emit] wrote it. *)
| Ok l when starts l "dead" -> Inspect.Dead (after l "dead") | Ok l when starts l "dead" -> Inspect.Dead (after l "dead")
| _ -> Inspect.Unknown); | _ -> Inspect.Unknown);
(* Rendered by the program, on its own thread, as an evaluation is: the
dyn printer follows pointers and can trap, and a trap there is a nested
break rather than a daemon that never answers again. See the agent's
[dyn] verb. *)
dyn = dyn =
(fun w -> (fun w ->
match agent_line t (Printf.sprintf "dyn %Lu" w) with let mark = job_mark ~at_stop:1 t in
| Ok l when starts l "ok " -> unhex (after l "ok ") match
| Ok l | Error l -> raise (Inspect.Unreadable ("a dyn value: " ^ l))) } await_job mark t (fun () ->
String.trim (request t (Printf.sprintf "dyn %Lu" w)))
with
| Ok v -> v
| Error m -> raise (Inspect.Unreadable ("a dyn value: " ^ m))) }
let reader t = let reader t =
Inspect.make ~program:t.session.Session.program Inspect.make ~program:t.session.Session.program
@ -2368,9 +2396,13 @@ let one_stop t f =
let r = f () in let r = f () in
if stop_gen t = before then r if stop_gen t = before then r
else else
match r with
| Error _ -> r
| Ok _ ->
Error Error
"the program moved on while it was being read, so what was read may \ "the program stopped somewhere else while it was being read — a value \
mix two stops; ask again" it was asked to render may have trapped, and the break buffer is \
holding that stop — so what was read may mix two stops; ask again"
(* The frame checks, which every verb that reads a *frame* has to make and (* The frame checks, which every verb that reads a *frame* has to make and
must make the same way. [inspect] exists precisely because the listing is must make the same way. [inspect] exists precisely because the listing is

View File

@ -735,11 +735,6 @@ void flan_dyn_print(flan_dyn v) { render(flan_write_stdout, v, 0, 0); }
void flan_dyn_emit_dev(flan_dyn v) { render(flan_dev_emit, v, 0, 1); } void flan_dyn_emit_dev(flan_dyn v) { render(flan_dev_emit, v, 0, 1); }
void flan_dyn_emit_watch(flan_dyn v) { render(flan_dev_watch_emit, v, 0, 1); } void flan_dyn_emit_watch(flan_dyn v) { render(flan_dev_watch_emit, v, 0, 1); }
/* And into whatever sink the caller hands over — the agent's, when the
* daemon's inspector reads a dyn word out of a stopped program's memory and
* wants its rendering as a reply rather than in the result buffer. */
void flan_dyn_emit_to(dyn_sink w, flan_dyn v) { render(w, v, 0, 1); }
/* And into a condition's message, which flan_rt.c's sink bounds. */ /* And into a condition's message, which flan_rt.c's sink bounds. */
void flan_msg_emit(const uint8_t *p, int64_t n); void flan_msg_emit(const uint8_t *p, int64_t n);
void flan_dyn_emit_msg(flan_dyn v) { render(flan_msg_emit, v, 0, 1); } void flan_dyn_emit_msg(flan_dyn v) { render(flan_msg_emit, v, 0, 1); }

View File

@ -0,0 +1,34 @@
;;;; Two dyn values the dyn printer cannot print. [dv] is a dyn view of a Vec
;;;; whose arena was freed, which traps DynRange when printed; the first
;;;; element of [s] is a word that reads as a boxed pointer to address 0x10,
;;;; which faults when printed as a dyn. The inspector hands each to the
;;;; program's own thread to render, so both become a nested break and the
;;;; daemon keeps answering.
(import agent "vendor:agent")
(defstruct Boom [why i32])
(defonce tv (Vec i64))
(defonce dv dyn)
(defonce n i32)
(defn as-dyn [d dyn] dyn d)
(defn inner [s [i64]] i64
(set dv dv) (set n n)
(error (Boom {.why 3}))
(at s 0))
(defn main [] i32
(agent/start "/tmp/flan-dev-dyn-trap-fallback.sock")
(let [ar (arena-new 4096)]
(set tv (vec-new i64 ar))
(push tv 7)
(set dv (as-dyn tv))
(free-all ar))
(set n 5)
(let [v (vec-new i64)]
(push v -1407374883553264)
(push v 5)
(print (inner (slice v))) (println ""))
(dotimes [i 4000] (agent/wait 5))
0)

View File

@ -6347,6 +6347,64 @@ let () =
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ psock; pout ]) List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ psock; pout ])
[ "--llvm"; "--x86" ]; [ "--llvm"; "--x86" ];
(* ── A dyn value that traps or faults while it is printed ─────────── *)
(* The reader hands a dyn word to the program's own thread to render,
because the dyn printer follows pointers: a view of a freed arena
traps, and a scribbled word faults. Rendered on the agent's thread
either one hung the daemon for good or killed it. Here each must come
back as a refusal, and the daemon must go on answering. *)
let dsock = tmp "dyntrap.sock" and dout = tmp "dyntrap.out" in
(try Sys.remove dsock with Sys_error _ -> ());
let dfd = Unix.openfile dout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let dpid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-dyn-trap.flan"; "-s"; dsock; "--llvm" |]
Unix.stdin dfd Unix.stderr
in
Unix.close dfd;
if not (listening ~pid:dpid dsock) then begin
fail "the dyn-trap daemon %s" !listen_why;
(try Unix.kill dpid Sys.sigkill with Unix.Unix_error _ -> ())
end
else begin
let c = connect dsock in
let stopped r =
match Wire.field r "stopped" with
| Some { Form.v = Form.Sym "t"; _ } -> true
| _ -> false
in
let answers what =
let r = request c "(:op \"eval-expr\" :code \"n\")" in
if Wire.string_field r "value" <> Some "5" then
fail "after %s the daemon no longer answers: %s" what
(Option.value ~default:(status r) (Wire.string_field r "message"))
in
if not (await (fun () -> stopped (request c "(:op \"describe\")"))) then
fail "the dyn-trap program never stopped"
else begin
let r = request c "(:op \"inspect\" :frame 0 :slot 0 :path (0))" in
(match Wire.field r "addr" with
| Some { Form.v = Form.Int a; _ } ->
let r =
request c (Printf.sprintf "(:op \"at\" :addr %Ld :type \"dyn\")" a)
in
if status r <> "error" then
fail "a dyn word pointing at 0x10 rendered: %s"
(Option.value ~default:"" (Wire.string_field r "value"));
answers "a dyn word that faults"
| _ -> fail "inspecting the slice element gave no :addr");
let r = request c "(:op \"globals\")" in
if status r <> "error" then
fail "a dyn view of a freed arena rendered in the globals section";
answers "a dyn value that traps"
end;
ignore (request c "(:op \"close\")");
(try Unix.close c with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] dpid) with Unix.Unix_error _ -> ())
end;
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ dsock; dout ];
(* ── What a half-finished assignment looks like from the break ────── *) (* ── What a half-finished assignment looks like from the break ────── *)
(* A condition signalled from inside the value being assigned stops the (* A condition signalled from inside the value being assigned stops the

View File

@ -127,11 +127,12 @@ int flan_dev_reg_overflowed(void);
int32_t flan_dev_reg_live(const void *p); int32_t flan_dev_reg_live(const void *p);
int32_t flan_dev_reg_epitaph(const void *p, char *buf, int64_t cap); int32_t flan_dev_reg_epitaph(const void *p, char *buf, int64_t cap);
void *flan_dev_global_find(const char *name); void *flan_dev_global_find(const char *name);
/* A dyn word's rendering, into a sink of the caller's. Weak: flan_dyn.c is /* A dyn word's rendering, into the result buffer. Weak: flan_dyn.c is linked
* linked only into a program that uses a dyn operation, and a program with no * only into a program that uses a dyn operation, and a program with no dyn
* dyn value has nothing to ask it about. */ * value has nothing to ask it about. */
void flan_dyn_emit_to(void (*w)(const uint8_t *, int64_t), uint64_t v) void flan_dyn_emit_dev(uint64_t v) __attribute__((weak));
__attribute__((weak)); void flan_dev_result_begin(void);
void flan_dev_result_end(void);
void flan_free_temp(void); void flan_free_temp(void);
void *flan_temp_scratch_begin(void); void *flan_temp_scratch_begin(void);
void flan_temp_scratch_end(void *prev); void flan_temp_scratch_end(void *prev);
@ -1495,17 +1496,13 @@ static void reply_unarmed(sink *o, snapshot *s, int32_t i) {
static pthread_mutex_t request_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t request_lock = PTHREAD_MUTEX_INITIALIZER;
/* Where [dyn]'s rendering collects. Static, because [request_lock] makes /* The word [dyn] hands the game thread. One slot, because the daemon waits
* that verb one caller at a time, and the renderer's sink is a bare function * for each rendering before asking for the next. */
* with nowhere to carry a pointer. Longer than any rendering the daemon keeps: static uint64_t dyn_word;
* it cuts every value at the result buffer's size. */ static void dyn_job(void) {
static char dyn_text[8192]; flan_dev_result_begin();
static size_t dyn_text_len; flan_dyn_emit_dev(dyn_word);
static void dyn_text_put(const uint8_t *p, int64_t n) { flan_dev_result_end();
size_t k = n < 0 ? 0 : (size_t)n;
if (k > sizeof dyn_text - dyn_text_len) k = sizeof dyn_text - dyn_text_len;
memcpy(dyn_text + dyn_text_len, p, k);
dyn_text_len += k;
} }
static void handle_line(char *line, sink *o) { static void handle_line(char *line, sink *o) {
@ -2065,26 +2062,34 @@ static void handle_line(char *line, sink *o) {
reply(o, "\n"); reply(o, "\n");
return; return;
} }
/* "dyn WORD" — the rendering of one dyn value, as hex. The walk that takes /* "dyn WORD" — render one dyn value into the result buffer, on the game
* a dyn apart is the runtime's, so the daemon hands it the word. */ * thread, and answer [ok] for having queued it; the daemon reads the value
* back as it reads a thunk's.
*
* Not rendered here. The dyn printer follows the value's pointers and can
* trap — a view of a Vec whose arena was freed — or fault on a scribbled
* word, and on this thread either one is a break loop that holds
* [request_lock] for ever, or the daemon's own death. On the game thread it
* runs as an evaluation does, inside [flan_agent_poll]'s escape, so it
* becomes a nested break the break buffer shows. */
if (strncmp(line, "dyn ", 4) == 0) { if (strncmp(line, "dyn ", 4) == 0) {
char *end = NULL; char *end = NULL;
unsigned long long w = strtoull(line + 4, &end, 0); unsigned long long w = strtoull(line + 4, &end, 0);
static const char hex[] = "0123456789abcdef"; snapshot *s = snap_top();
job j = { 0 };
if (end == line + 4) { reply(o, "err dyn wants a word\n"); return; } if (end == line + 4) { reply(o, "err dyn wants a word\n"); return; }
if (flan_dyn_emit_to == NULL) { if (flan_dyn_emit_dev == NULL) {
reply(o, "err this program has no dyn runtime linked\n"); reply(o, "err this program has no dyn runtime linked\n");
return; return;
} }
dyn_text_len = 0; if (s == NULL) { reply(o, "err no snapshot\n"); return; }
flan_dyn_emit_to(dyn_text_put, (uint64_t)w); if (!queue_room()) { reply(o, "err the install queue is full\n"); return; }
reply(o, "ok "); dyn_word = (uint64_t)w;
for (size_t k = 0; k < dyn_text_len; k++) { j.call = dyn_job;
unsigned char c = (unsigned char)dyn_text[k]; j.stopped_only = 1;
char two[2] = { hex[c >> 4], hex[c & 15] }; j.at_stop = s->gen;
emit(o, two, 2); if (!publish(j)) { reply(o, "err the install queue is full\n"); return; }
} reply(o, "ok\n");
reply(o, "\n");
return; return;
} }
if (strcmp(line, "result") == 0) { if (strcmp(line, "result") == 0) {