From 9e73179bed306efe439fbc23689d00b6baa1dc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E5=8F=8A?= <522caiji@gmail.com> Date: Sat, 3 Oct 2026 00:37:30 +0800 Subject: [PATCH] fix(qoder): run CN check-in fingerprint outside noexec homes Account runtime directories can be mounted noexec, so copy runtime-info to an executable directory before reading its machine fingerprint. --- .../qoder-cn-runtime-info-noexec.md | 7 ++++++ worker/src/checkin.mjs | 25 ++++++++++++++++--- worker/test/checkin.test.mjs | 23 +++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 changelog/unreleased/qoder-cn-runtime-info-noexec.md diff --git a/changelog/unreleased/qoder-cn-runtime-info-noexec.md b/changelog/unreleased/qoder-cn-runtime-info-noexec.md new file mode 100644 index 0000000..a009730 --- /dev/null +++ b/changelog/unreleased/qoder-cn-runtime-info-noexec.md @@ -0,0 +1,7 @@ +### English + +- Run the Qoder CN check-in fingerprint from an executable directory when the account runtime directory cannot run programs. + +### 中文 + +- 账号运行目录不能执行程序时,Qoder 国内版签到会改到可执行目录读取机器指纹。 diff --git a/worker/src/checkin.mjs b/worker/src/checkin.mjs index 4aa9f9e..b50002d 100644 --- a/worker/src/checkin.mjs +++ b/worker/src/checkin.mjs @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { readdirSync } from "node:fs"; +import { copyFileSync, mkdirSync, readdirSync } from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -104,7 +104,7 @@ function machineHostname() { return printableHeader.test(shortened) ? shortened : ""; } -function runtimeInfoPath(home = process.env.QODER_HOME || process.env.HOME || "") { +function installedRuntimeInfo(home = process.env.QODER_HOME || process.env.HOME || "") { if (!home) return ""; const directory = path.join(home, ".bin"); let names = []; @@ -118,6 +118,23 @@ function runtimeInfoPath(home = process.env.QODER_HOME || process.env.HOME || "" return match ? path.join(directory, match) : ""; } +function executableRuntimeInfo(source, prepare = prepareExecutableRuntimeInfo) { + if (!source) return ""; + try { + return prepare(source) || source; + } catch { + return source; + } +} + +function prepareExecutableRuntimeInfo(source) { + const directory = process.env.QODER_RUNTIME_INFO_DIR || os.tmpdir(); + const executable = path.join(directory, path.basename(source)); + mkdirSync(directory, { recursive: true }); + copyFileSync(source, executable); + return executable; +} + function accountId(user) { for (const value of [user?.uid, user?.user_id, user?.userId, user?.id]) { if (typeof value === "string" && value.trim()) return value.trim(); @@ -196,7 +213,9 @@ async function machineHeaders(auth, user, options) { headers["Cosy-MachineId"] = id; headers["Cosy-MachineToken"] = id; } - const executable = (options.runtimeInfoPath ?? runtimeInfoPath)(); + const executable = options.runtimeInfoPath + ? executableRuntimeInfo(options.runtimeInfoPath(), options.prepareRuntimeInfo) + : executableRuntimeInfo(installedRuntimeInfo()); const account = accountId(user); if (executable && account) { const identity = await (options.readRiskIdentity ?? readRiskIdentity)(executable, account); diff --git a/worker/test/checkin.test.mjs b/worker/test/checkin.test.mjs index 684de27..d9a3e06 100644 --- a/worker/test/checkin.test.mjs +++ b/worker/test/checkin.test.mjs @@ -102,6 +102,29 @@ test("CN check-in sends the machine identity headers used by the desktop client" } }); +test("CN check-in uses an executable copy when the account runtime directory cannot execute files", async () => { + const previous = process.env.QODER_RUNTIME_INFO_DIR; + process.env.QODER_RUNTIME_INFO_DIR = "/opt/qoder-runtime"; + const reads = []; + try { + const { checkin } = fixture([json(listed(credit({ status: "CLAIMED" })))], { + machineId: "machine-test-id", + accountId: "account-1", + runtimeInfoPath: () => "/run/account/.bin/runtime-info-linux-x64-probe", + prepareRuntimeInfo: (source) => `/opt/qoder-runtime/${source.split("/").at(-1)}`, + readRiskIdentity: async (executable) => { + reads.push(executable); + return { machineToken: "risk-token", machineType: "risk-type", machineCode: "risk-code" }; + }, + }); + assert.equal((await checkin()).status, "already"); + assert.deepEqual(reads, ["/opt/qoder-runtime/runtime-info-linux-x64-probe"]); + } finally { + if (previous === undefined) delete process.env.QODER_RUNTIME_INFO_DIR; + else process.env.QODER_RUNTIME_INFO_DIR = previous; + } +}); + test("CN check-in keeps the machine id when the risk identity bridge is unavailable", async () => { const { checkin, calls } = fixture([json(listed(credit({ status: "CLAIMED" })))], { machineId: "machine-test-id",