mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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:
parent
b06c1f2cf1
commit
9a3ca68bda
4 changed files with 55 additions and 10 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue