fix(lifecycle): validate owned Hopper process identity
This commit is contained in:
1 parent
f8e8ea35ff
commit
898d6b0192
3 files changed
+86
-10
No files matched your search
@@ -8,6 +8,10 @@ export interface OwnedProcessGroup {
|
||||
readonly runId: string;
|
||||
readonly leaderPid: number;
|
||||
readonly processGroupId: number;
|
||||
/** Expected launcher identity, checked only while the leader exists. */
|
||||
readonly expectedCommand?: string;
|
||||
/** Expected launcher parent, checked only while the leader exists. */
|
||||
readonly expectedParentPid?: number;
|
||||
}
|
||||
|
||||
interface ProcessGroupMember {
|
||||
@@ -86,6 +90,25 @@ export const cleanupOwnedProcessGroup = async (
|
||||
return { cleaned: false, reason: "process group could not be inspected" };
|
||||
}
|
||||
if (members.length === 0) return { cleaned: true, signaled: false };
|
||||
const leader = members.find((member) => member.pid === ownership.leaderPid);
|
||||
if (leader !== undefined) {
|
||||
if (
|
||||
ownership.expectedParentPid !== undefined &&
|
||||
leader.parentPid !== ownership.expectedParentPid
|
||||
)
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: "owned launcher parent identity did not match",
|
||||
};
|
||||
if (
|
||||
ownership.expectedCommand !== undefined &&
|
||||
!commandMatches(leader.command, ownership.expectedCommand)
|
||||
)
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: "owned launcher command identity did not match",
|
||||
};
|
||||
}
|
||||
for (const member of members) {
|
||||
let environment: Readonly<Record<string, string>>;
|
||||
try {
|
||||
@@ -113,3 +136,12 @@ export const cleanupOwnedProcessGroup = async (
|
||||
}
|
||||
return { cleaned: true, signaled: true };
|
||||
};
|
||||
|
||||
const commandMatches = (actual: string, expected: string): boolean => {
|
||||
const normalizedActual = actual.trim();
|
||||
const normalizedExpected = expected.trim();
|
||||
return (
|
||||
normalizedActual === normalizedExpected ||
|
||||
normalizedActual.startsWith(`${normalizedExpected} `)
|
||||
);
|
||||
};
|
||||
@@ -141,22 +141,18 @@ export class HopperApplicationLauncher implements BridgeLauncher {
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
await cleanupOwnedProcessGroup({
|
||||
runId: session.runId,
|
||||
leaderPid: pid,
|
||||
processGroupId: pid,
|
||||
});
|
||||
await cleanupOwnedProcessGroup(
|
||||
ownedProcessGroup(session, pid, this.options.launcherPath),
|
||||
);
|
||||
return err(new HopperStartError({ cause }));
|
||||
}
|
||||
return ok({
|
||||
process: child,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup: () =>
|
||||
cleanupOwnedProcessGroup({
|
||||
runId: session.runId,
|
||||
leaderPid: pid,
|
||||
processGroupId: pid,
|
||||
}),
|
||||
cleanupOwnedProcessGroup(
|
||||
ownedProcessGroup(session, pid, this.options.launcherPath),
|
||||
),
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
return err(new HopperStartError({ cause }));
|
||||
@@ -164,6 +160,18 @@ export class HopperApplicationLauncher implements BridgeLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
const ownedProcessGroup = (
|
||||
session: BridgeSession,
|
||||
pid: number,
|
||||
launcherPath: string,
|
||||
) => ({
|
||||
runId: session.runId,
|
||||
leaderPid: pid,
|
||||
processGroupId: pid,
|
||||
expectedCommand: launcherPath,
|
||||
expectedParentPid: process.pid,
|
||||
});
|
||||
|
||||
const prepareHopperApplication = async (
|
||||
launcherPath: string,
|
||||
signal?: AbortSignal,
|
||||
|
||||
@@ -67,4 +67,40 @@ describe("owned process-group cleanup", () => {
|
||||
signaled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when the launcher command identity changes", async () => {
|
||||
const { adapter, signalGroup } = host({
|
||||
100: { REA_PROCESS_RUN_ID: "run-token" },
|
||||
});
|
||||
expect(
|
||||
await cleanupOwnedProcessGroup(
|
||||
{
|
||||
...ownership,
|
||||
expectedCommand: "/owned/hopper",
|
||||
expectedParentPid: 1,
|
||||
},
|
||||
adapter,
|
||||
),
|
||||
).toEqual({
|
||||
cleaned: false,
|
||||
reason: "owned launcher command identity did not match",
|
||||
});
|
||||
expect(signalGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails closed when the launcher parent identity changes", async () => {
|
||||
const { adapter, signalGroup } = host({
|
||||
100: { REA_PROCESS_RUN_ID: "run-token" },
|
||||
});
|
||||
expect(
|
||||
await cleanupOwnedProcessGroup(
|
||||
{ ...ownership, expectedCommand: "fixture", expectedParentPid: 999 },
|
||||
adapter,
|
||||
),
|
||||
).toEqual({
|
||||
cleaned: false,
|
||||
reason: "owned launcher parent identity did not match",
|
||||
});
|
||||
expect(signalGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user