diff --git a/plugins/zcode-advisor/tools/companion/launchd.cjs b/plugins/zcode-advisor/tools/companion/launchd.cjs index e832741..0be261d 100644 --- a/plugins/zcode-advisor/tools/companion/launchd.cjs +++ b/plugins/zcode-advisor/tools/companion/launchd.cjs @@ -111,10 +111,75 @@ function uid() { try { return typeof process.getuid === 'function' ? process.getuid() : null; } catch (_) { return null; } } +// launchctl 固定在 /bin/launchctl:这是 macOS 系统自带、不随版本漂移的绝对路径, +// 刻意不走 PATH 查找——PATH 上的同名文件可能被用户环境劫持,而这里的调用会 +// bootout 用户真实的自启作业。 function launchctl(args) { return execFileSync('/bin/launchctl', args, { stdio: 'pipe', timeout: 20000, encoding: 'utf8' }); } +// 用户真实自启作业的 plist 路径——**绕过一切覆盖**(dir 参数 / ZCODE_ADVISOR_LAUNCHD_DIR)。 +// 完整安全不变量见 canTouchLaunchd 上方注释;这里只负责算出路径。 +function realPlistPath() { + return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`); +} + +// 路径规范化:优先 realpath 父目录(plist 可能还没落盘,但父目录一定存在), +// 再退回 realpath 自身,最后退回 path.resolve。 +// +// 只做 path.resolve 会把「指向真实目录的符号链接」判成另一个位置:macOS 上 +// /var → /private/var、/tmp → /private/tmp 都是这种链接。后果是真实卸载被误判成 +// 「不是真实作业」而静默跳过——卸载功能本身失效。realpath 之后两个方向都不会误判: +// 指向真实目录的链接被认成真实(该 bootout),沙箱仍被认成沙箱(不碰 launchd 域)。 +// +// realpathSync 可能因目录在两次调用之间被删而抛(TOCTOU)。这里不回退重试而是直接 +// 落到 path.resolve:**兜底方向是安全的**——沙箱目录消失时判定结果仍是「不是真实作业」, +// 于是不碰 launchd 域(宁可不操作,也不能误打用户真实作业)。反之若真实目录消失, +// 两条路径都退化成 path.resolve,仍然相等,卸载照常生效。 +function canonicalPath(p) { + const dir = path.dirname(p); + const base = path.basename(p); + try { return path.join(fs.realpathSync(dir), base); } catch (_) { /* 父目录不存在 */ } + try { return fs.realpathSync(p); } catch (_) { /* 文件也不存在 */ } + return path.resolve(p); +} + +// 目标 plist 是否就是用户真实那份(只有这份才允许操作 launchd 域)。 +// +// 注意:os.homedir() 在个别环境下会抛(用户记录查不到)。这里**不静默吞掉**: +// 吞掉会让 install 照旧把 plist 落盘、却永远不 bootstrap——正好是本 bug 的另一种形态 +// (文件在、作业不在、用户以为装好了)。出错时明确写 stderr 再返回 false。 +function managesRealAgent(p) { + let real; + try { + real = realPlistPath(); + } catch (err) { + process.stderr.write(`[launchd] 无法定位用户 LaunchAgents 目录,跳过 launchd 域操作:${(err && err.message) || err}\n`); + return false; + } + return canonicalPath(p) === canonicalPath(real); +} + +// launchd 域操作仅在 macOS、拿得到 uid、且**目标就是用户真实那份 plist** 时才允许。 +// +// 关键不变量:LABEL 是固定常量,重定向只能改 plist 的**路径**,改不掉 launchd +// 域里的作业名。所以一旦目标 plist 不是这一份,任何 +// `launchctl print/bootout gui//