The agent binds the daemon's socket only in the process the daemon launched, named by FLAN_AGENT_OWNER

This commit is contained in:
Joseph Ferano 2026-09-25 15:18:55 +07:00
parent e2aa0196cf
commit d5fc3aa657
5 changed files with 119 additions and 28 deletions

View File

@ -262,9 +262,9 @@ driver at all — it goes `llc` + `ld -shared` + `dlopen`, which is what makes
is a confusing shape of failure to meet without warning. is a confusing shape of failure to meet without warning.
Variables beginning `FLAN_DEV_` other than `FLAN_DEV_LEAKS`, plus Variables beginning `FLAN_DEV_` other than `FLAN_DEV_LEAKS`, plus
`FLAN_AGENT_SOCKET` and `FLAN_COMPILER_STAMP`, are internal: `flan dev` sets `FLAN_AGENT_SOCKET`, `FLAN_AGENT_OWNER` and `FLAN_COMPILER_STAMP`, are
them across its own `exec` to hand the merged binary what it needs. Setting internal: `flan dev` sets them across its own `exec` to hand the merged binary
them by hand is not supported. what it needs. Setting them by hand is not supported.
## Checking it ## Checking it

View File

@ -1637,13 +1637,6 @@ prunes a package nothing calls into in a release build. =flan dev= links the
agent's C into every program it builds whether or not the source imports it; a agent's C into every program it builds whether or not the source imports it; a
release build links it only when the program calls into it. release build links it only when the program calls into it.
** NEXT FLAN_AGENT_SOCKET in a shell's environment steals the socket
Decided 2026-09-25: narrow the gate. The daemon also exports its own pid, and the constructor binds the socket only when that pid is the program's parent (or the program itself, in a merged build).
Binding unlinks the path first, and before the constructor that unlink was reached
only by an explicit call. A sentence about the shape of the gate rather than an
observed problem: only the daemon sets the variable and it never runs release
builds. The fix, if it is ever felt, is a narrower gate.
** DONE The daemon's "has not called (agent/start ...)" note is unreachable ** DONE The daemon's "has not called (agent/start ...)" note is unreachable
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
Retired, with the matching arm of an evaluation's timeout, because it named the Retired, with the matching arm of an evaluation's timeout, because it named the

View File

@ -5251,6 +5251,10 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
environment. Guessing instead would fail silently — everything compiles, environment. Guessing instead would fail silently — everything compiles,
the module is built, and nothing ever receives it. *) the module is built, and nothing ever receives it. *)
Unix.putenv "FLAN_AGENT_SOCKET" agent; Unix.putenv "FLAN_AGENT_SOCKET" agent;
(* The child binds that path only if this pid is its parent, so a process
that merely inherited the variable leaves the socket alone
(flan_agent.c, [daemon_socket]). *)
Unix.putenv "FLAN_AGENT_OWNER" (string_of_int (Unix.getpid ()));
(* And who its parent is, which is the child's licence to end itself. (* And who its parent is, which is the child's licence to end itself.
vendor/agent/flan_agent.c carries the argument at length; the half that vendor/agent/flan_agent.c carries the argument at length; the half that
belongs here is that this daemon is the only thing that ever kills its belongs here is that this daemon is the only thing that ever kills its
@ -6264,6 +6268,9 @@ let start_merged ?(debug = false) ?(x86 = true) ~file ~sock () =
that the game thread's [getenv] cannot race the compiler thread's that the game thread's [getenv] cannot race the compiler thread's
[putenv]: there is no ordering left to get wrong. *) [putenv]: there is no ordering left to get wrong. *)
Unix.putenv "FLAN_AGENT_SOCKET" agent; Unix.putenv "FLAN_AGENT_SOCKET" agent;
(* This pid, because the exec below keeps it: the program is the owner
flan_agent.c's [daemon_socket] looks for. *)
Unix.putenv "FLAN_AGENT_OWNER" (string_of_int (Unix.getpid ()));
Unix.putenv "FLAN_DEV_SOURCE" file; Unix.putenv "FLAN_DEV_SOURCE" file;
Unix.putenv "FLAN_DEV_SOCK" sock; Unix.putenv "FLAN_DEV_SOCK" sock;
Unix.putenv "FLAN_DEV_DIR" dir; Unix.putenv "FLAN_DEV_DIR" dir;

View File

@ -61,6 +61,13 @@ let send path line =
Unix.close s; Unix.close s;
Buffer.contents b Buffer.contents b
(* The pair the daemon sets: the path, and the pid it is meant for. This test
binary is the parent of every program it starts, which is the
--two-process shape. *)
let daemon_env path =
[| "FLAN_AGENT_SOCKET=" ^ path;
"FLAN_AGENT_OWNER=" ^ string_of_int (Unix.getpid ()) |]
let () = let () =
match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with
| 0 -> | 0 ->
@ -298,7 +305,7 @@ let () =
let bad = tmp "bad.out" in let bad = tmp "bad.out" in
let bfd' = ofd bad in let bfd' = ofd bad in
let benv = let benv =
Array.append aenv [| "FLAN_AGENT_SOCKET=/nonexistent-dir/agent.sock" |] Array.append aenv (daemon_env "/nonexistent-dir/agent.sock")
in in
let bpid' = Unix.create_process_env aexe [| aexe |] benv Unix.stdin bfd' bfd' in let bpid' = Unix.create_process_env aexe [| aexe |] benv Unix.stdin bfd' bfd' in
Unix.close bfd'; Unix.close bfd';
@ -325,8 +332,69 @@ let () =
"cannot listen\n" "cannot listen\n"
end; end;
(* ── The variable inherited by a process the daemon did not start ── *)
(* A shell opened from inside a [flan dev] program carries its
FLAN_AGENT_SOCKET, and so does anything run from that shell. Binding
unlinks the path first, so honouring it there would take the session's
socket from its program. FLAN_AGENT_OWNER names the process the daemon
launched; pid 1 is neither this program nor its parent, so the variable
is not this program's, and it picks and announces a path of its own as
if nothing were set. The file standing in for the session's socket has
to still be the same file afterwards. *)
let stolen = tmp "stolen.sock" and serr = tmp "stolen.err" in
Out_channel.with_open_bin stolen (fun oc ->
output_string oc "the session's");
let senv =
Array.append aenv
[| "FLAN_AGENT_SOCKET=" ^ stolen; "FLAN_AGENT_OWNER=1" |]
in
let s1 = ofd (tmp "stolen.out") and s2 = ofd serr in
let spid = Unix.create_process_env aexe [| aexe |] senv Unix.stdin s1 s2 in
Unix.close s1;
Unix.close s2;
let prefix = "flan agent: listening on " in
let sannounced () =
let text = In_channel.with_open_bin serr In_channel.input_all in
List.find_map
(fun l ->
if String.length l > String.length prefix
&& String.sub l 0 (String.length prefix) = prefix
then Some (String.sub l (String.length prefix)
(String.length l - String.length prefix))
else None)
(String.split_on_char '\n' text)
in
(match
if await (fun () -> sannounced () <> None) then sannounced () else None
with
| None ->
fail "a program with someone else's FLAN_AGENT_SOCKET announced no \
socket of its own";
(try Unix.kill spid Sys.sigkill with Unix.Unix_error _ -> ())
| Some p ->
if p = stolen then fail "the inherited path was bound: %S" p;
if not (await (fun () -> Sys.file_exists p)) then
fail "nothing was bound at the announced %S" p
else ignore (send p aso);
let reaped =
await ~ms:5000 (fun () ->
match Unix.waitpid [ Unix.WNOHANG ] spid with
| 0, _ -> false
| _ -> true)
in
if not reaped then begin
(try Unix.kill spid Sys.sigkill with Unix.Unix_error _ -> ());
fail "the program with an inherited variable never finished"
end);
(match In_channel.with_open_bin stolen In_channel.input_all with
| "the session's" -> ()
| _ -> fail "the inherited FLAN_AGENT_SOCKET's file was replaced"
| exception Sys_error _ ->
fail "the inherited FLAN_AGENT_SOCKET's file was removed");
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
[ aexe; aso; aout; aerr; bad ]; [ aexe; aso; aout; aerr; bad; stolen; serr; tmp "stolen.out" ];
(* ── No (agent/start) at all ────────────────────────────────────── *) (* ── No (agent/start) at all ────────────────────────────────────── *)
@ -359,7 +427,7 @@ let () =
let nc = Session.eval nt "(defn tick [] i64 1000)" in let nc = Session.eval nt "(defn tick [] i64 1000)" in
ignore (Build.shared ~opts:dev ~ir:nc.Session.ir ~out:nso ()); ignore (Build.shared ~opts:dev ~ir:nc.Session.ir ~out:nso ());
let nenv = let nenv =
Array.append (Unix.environment ()) [| "FLAN_AGENT_SOCKET=" ^ nsock |] Array.append (Unix.environment ()) (daemon_env nsock)
in in
let nfd = let nfd =
Unix.openfile nout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 Unix.openfile nout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
@ -412,7 +480,7 @@ let () =
bt.Session.host ~out:bexe); bt.Session.host ~out:bexe);
let bfd = Unix.openfile bout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in let bfd = Unix.openfile bout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let env = let env =
Array.append (Unix.environment ()) [| "FLAN_AGENT_SOCKET=" ^ bsock |] Array.append (Unix.environment ()) (daemon_env bsock)
in in
let bpid = let bpid =
Unix.create_process_env bexe [| bexe |] env Unix.stdin bfd bfd Unix.create_process_env bexe [| bexe |] env Unix.stdin bfd bfd
@ -753,7 +821,7 @@ let () =
lt.Session.host ~out:lexe); lt.Session.host ~out:lexe);
let lfd = Unix.openfile lout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in let lfd = Unix.openfile lout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let lenv = let lenv =
Array.append (Unix.environment ()) [| "FLAN_AGENT_SOCKET=" ^ lsock |] Array.append (Unix.environment ()) (daemon_env lsock)
in in
let lpid = Unix.create_process_env lexe [| lexe |] lenv Unix.stdin lfd lfd in let lpid = Unix.create_process_env lexe [| lexe |] lenv Unix.stdin lfd lfd in
Unix.close lfd; Unix.close lfd;

View File

@ -2459,17 +2459,40 @@ failed:
return -1; return -1;
} }
/* The daemon's socket for this process, or NULL when there is none.
*
* FLAN_AGENT_SOCKET alone is not enough, because an environment is inherited:
* a shell started from inside a [flan dev] program, or anything that program
* starts, carries it too, and binding unlinks the path first, so such a
* process would take the session's socket from the program it belongs to. So
* the daemon also names the process it launched, in FLAN_AGENT_OWNER, and the
* path is honoured only there: the owner is this process in a merged build,
* where the launcher execs into the program, and this process's parent under
* --two-process, where the daemon started it. */
static const char *daemon_socket(void) {
const char *env = getenv("FLAN_AGENT_SOCKET");
const char *own = getenv("FLAN_AGENT_OWNER");
char *end;
long pid;
if (env == NULL || env[0] == '\0' || own == NULL || own[0] == '\0')
return NULL;
pid = strtol(own, &end, 10);
if (end == own || *end != '\0' || pid <= 0) return NULL;
if (pid != (long)getpid() && pid != (long)getppid()) return NULL;
return env;
}
/* [path] is a Flan string: ptr and len, not NUL-terminated. /* [path] is a Flan string: ptr and len, not NUL-terminated.
* *
* FLAN_AGENT_SOCKET overrides it. A program's source has to name some path, * The daemon's socket overrides it (see [daemon_socket]). A program's source
* and the daemon that launches the program is the one that knows where it * has to name some path, and the daemon that launches the program is the one that knows where it
* wants to talk to it — without the override the daemon would have to guess, * wants to talk to it — without the override the daemon would have to guess,
* and guessing wrong fails silently: everything compiles, the module is built, * and guessing wrong fails silently: everything compiles, the module is built,
* and nothing ever receives it. */ * and nothing ever receives it. */
int32_t flan_agent_start(const uint8_t *path, int64_t len) { int32_t flan_agent_start(const uint8_t *path, int64_t len) {
char buf[sizeof(((struct sockaddr_un *)0)->sun_path)]; char buf[sizeof(((struct sockaddr_un *)0)->sun_path)];
const char *env = getenv("FLAN_AGENT_SOCKET"); const char *env = daemon_socket();
if (env != NULL && env[0] != '\0') return start_on(env) < 0 ? -1 : 0; if (env != NULL) return start_on(env) < 0 ? -1 : 0;
if (len <= 0 || (size_t)len >= sizeof buf) return -1; if (len <= 0 || (size_t)len >= sizeof buf) return -1;
memcpy(buf, path, (size_t)len); memcpy(buf, path, (size_t)len);
buf[len] = '\0'; buf[len] = '\0';
@ -2493,8 +2516,8 @@ int32_t flan_agent_start_auto(void) {
char path[sizeof(((struct sockaddr_un *)0)->sun_path)]; char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
struct timespec ts; struct timespec ts;
int32_t r; int32_t r;
const char *env = getenv("FLAN_AGENT_SOCKET"); const char *env = daemon_socket();
if (env != NULL && env[0] != '\0') return start_on(env) < 0 ? -1 : 0; if (env != NULL) return start_on(env) < 0 ? -1 : 0;
if (clock_gettime(CLOCK_REALTIME, &ts) != 0) ts.tv_nsec = 0; if (clock_gettime(CLOCK_REALTIME, &ts) != 0) ts.tv_nsec = 0;
snprintf(path, sizeof path, "/tmp/flan-agent-%ld-%08lx.sock", snprintf(path, sizeof path, "/tmp/flan-agent-%ld-%08lx.sock",
(long)getpid(), (unsigned long)(ts.tv_nsec & 0xffffffffL)); (long)getpid(), (unsigned long)(ts.tv_nsec & 0xffffffffL));
@ -2511,11 +2534,11 @@ int32_t flan_agent_start_auto(void) {
/* And the call itself, gone. A program under [flan dev] that imports this /* And the call itself, gone. A program under [flan dev] that imports this
* package gets the listener before main, without asking. * package gets the listener before main, without asking.
* *
* FLAN_AGENT_SOCKET is the whole condition, and it is the right one: the * [daemon_socket] is the condition: the daemon sets both of its variables in
* daemon sets it in both shapes — before the fork in --two-process, before the * both shapes — before the fork in --two-process, before the exec in the
* exec in the merged build — and nothing else on a machine sets it. So an * merged build — so an ordinary run of an ordinary program falls straight
* ordinary run of an ordinary program falls straight through here and this * through here, and so does a process that only inherited them. (Not
* costs it one getenv. (Not FLAN_DEV_PARENT, which is deliberately unset in * FLAN_DEV_PARENT, which is deliberately unset in
* the merged build; gating on it would quietly skip half the daemon.) * the merged build; gating on it would quietly skip half the daemon.)
* *
* WHAT THIS DOES NOT REACH, because it is a fact about linking rather than a * WHAT THIS DOES NOT REACH, because it is a fact about linking rather than a
@ -2540,8 +2563,8 @@ int32_t flan_agent_start_auto(void) {
* cannot, in either shape — it has an editor to hear from first, and a module * cannot, in either shape — it has an editor to hear from first, and a module
* to compile after that. */ * to compile after that. */
__attribute__((constructor)) static void auto_start(void) { __attribute__((constructor)) static void auto_start(void) {
const char *env = getenv("FLAN_AGENT_SOCKET"); const char *env = daemon_socket();
if (env == NULL || env[0] == '\0') return; if (env == NULL) return;
/* The answer is dropped because there is nobody to give it to: this is ELF /* The answer is dropped because there is nobody to give it to: this is ELF
* init, before main, before the program has decided anything. What matters * init, before main, before the program has decided anything. What matters
* is that a failure here is not final — [start_on] gives [started] back, so * is that a failure here is not final — [start_on] gives [started] back, so