fix(chord): remote references refuse a foreign ctx

A call through a remote reference checked the ctx against its own host,
so a ctx from another host, or an object not made by Chord, passed and
its root was carried over the link. It now runs the same checks as a
local reference and rejects with invalid/foreign_ctx.

closes #10959
This commit is contained in:
Mario Zechner 2026-10-15 06:25:18 +00:00
parent b06c1f2cf1
commit 9a3ca68bda
4 changed files with 55 additions and 10 deletions

View file

@ -53,6 +53,7 @@
- IPC links and the host manager's control channel wrote to a peer process that had exited without a callback, so the `EPIPE` Node reports for a write in the gap between the exit and `disconnect` (an `ack` sent just after a worker shut down) became an unhandled `error` event that crashed the process. The link now ends on a failed write (reason `ipc send failed: EPIPE`), and control messages to a gone peer are dropped
- A relayed call ran under the origin's cancel signal only, so a forced retirement of the relaying host could not cancel it before closing the old code's resources; it now follows its root's signal
- A resource kept across a reload closed with the `close` of the factory run that first opened it, and kept that run's code reachable; it now adopts the newest declaration at commit
- A call through a remote reference accepted a ctx from another host, or an object not made by Chord, and carried its root over the link; it now rejects it with `invalid/foreign_ctx` like a local reference ([#10959](https://github.com/earendil-works/pi/issues/10959))
## [1.2.0] - 2026-10-12

View file

@ -29,6 +29,7 @@ import type { EventOf, ListenerDecl, Resource, ResourceDeclaration, Subscribable
import { asChordError, ChordError, invalid, runAll } from "./errors.ts";
import { enqueue } from "./publish.ts";
import type { Registration, Registry } from "./registry.ts";
import type { RemoteHost } from "./remote.ts";
import { assertValid, normalize } from "./schema.ts";
import type { Ctx, Exposed, GetOptions, Readable, RefStatus, Shape, StateEvent } from "./types.ts";
@ -418,8 +419,8 @@ export class CtxImpl implements Ctx {
return this.#closed;
}
/** Throws unless this ctx may start work on `host` now (not expired, not aborted, not from another host). */
check(host: HostCore): void {
/** Throws unless this ctx may start work on `host` (the called reference's host) now: not expired, aborted or foreign. */
check(host: HostCore | RemoteHost): void {
if (this.host !== host) throw invalid("foreign_ctx", "ctx belongs to another host");
if (this.#closed) throw new ChordError("aborted", "ctx expired: its call already settled", { reason: "expired" });
if (this.signal.aborted) {

View file

@ -2,7 +2,7 @@ import { Value } from "typebox/value";
import { HYDRATION, type Hydration } from "./cell.ts";
import { type AnyContract, takesNoArgs } from "./contract.ts";
import { applyImmutable, type Decoder, decoder } from "./delta/index.ts";
import { ChordError } from "./errors.ts";
import { ChordError, invalid } from "./errors.ts";
import {
CtxImpl,
notifyStatus,
@ -631,14 +631,17 @@ export class RemoteRef implements RefCore {
const host = this.binding.host;
if (host.inFactory > 0)
throw new ChordError("not_ready", `${this.address}.${name} called inside a factory`);
if (ctx instanceof CtxImpl) {
try {
ctx.check(ctx.host);
} catch (error) {
return Promise.reject(error);
}
// The same checks as a local reference's caller (RefImpl.caller and ObjectImpl.call): a ctx from another host
// or one not made by Chord would carry a root this host does not own over its link.
if (ctx !== undefined && !(ctx instanceof CtxImpl)) {
return Promise.reject(invalid("foreign_ctx", "ctx must come from a Chord host"));
}
if (ctx === undefined || (ctx instanceof CtxImpl && ctx.holder)) {
try {
ctx?.check(host);
} catch (error) {
return Promise.reject(error);
}
if (ctx === undefined || ctx.holder) {
return host.root((root) => this.#call(name, args, root), { signal: ctx?.signal });
}
return this.#call(name, args, ctx);

View file

@ -11,11 +11,13 @@ import {
hostNamed,
isChordError,
local,
memoryLinkPair,
method,
type Ref,
remote,
} from "../src/index.ts";
import { fits } from "../src/schema.ts";
import { CountingLink } from "./fixtures/counting.ts";
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
const ticks = async (n = 10) => {
@ -643,4 +645,42 @@ describe("review: ctx races", () => {
});
assert.ok(foreign);
});
// #10959
it("a remote reference refuses a foreign ctx and a ctx not made by Chord, without calling", async () => {
const RemoteTarget = remote("t.remote-target", {
version: 1,
input: Type.Object({}),
host: hostNamed("server"),
variants: { main: { state: {}, methods: { hit: method() } } },
});
let hits = 0;
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
server.serve(RemoteTarget, () => ({
variant: "main",
hit: () => {
hits++;
},
}));
const [down, up] = memoryLinkPair();
server.accept(down, { kind: "host", name: "worker", principal: { worker: true } });
const link = new CountingLink(up);
const worker = createHost({ self: { kind: "host" }, name: "worker", onError: () => {} });
worker.connect(link, { principal: { kind: "server" } });
const other = createHost({ self: { kind: "host" }, name: "other", onError: () => {} });
const target = worker.hold(RemoteTarget, {});
await target.ready;
const foreign = await other.root((ctx) => target.hit(ctx).catch((error: unknown) => error));
assert.ok(isChordError(foreign, "invalid") && foreign.reason === "foreign_ctx", String(foreign));
const forged = { rootId: "forged", codeVersion: 1, signal: new AbortController().signal } as unknown as Ctx;
await assert.rejects(
target.hit(forged),
(error) => isChordError(error, "invalid") && error.reason === "foreign_ctx",
);
await ticks();
assert.equal(hits, 0);
assert.equal(link.count("sent", "call"), 0);
await worker.root((ctx) => target.hit(ctx));
assert.equal(hits, 1);
});
});