AGC ACL 提权修复按目标做 single-flight,避免并发重复弹 UAC(#498) #502
Reference in New Issue
Block a user
Delete Branch "fix/acl-elevation-single-flight"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
关联 issue:#498
问题
AGC 启动页一次挂载会出现多个叠在一起的 UAC 提权弹窗;用户点「否」之后还会被再问一次。
根因
windows_acl_repair_target(apps/ai-game-creator-shell/src-tauri/src/config.rs)对 Managed 作用域不返回叶路径,而是返回第一个读取被拒的祖先,同一祖先下的多个项目因此解析到同一个 repair target。attempted_targets,函数返回即消失——跨调用、跨线程没有记忆。powershell -Verb RunAs;Start-Process -Wait没有超时,被忽略的弹窗会长期占住线程。powershellexit 1223)与「ACL 修复失败」在错误文案上无法区分,调用方只能去匹配中文。方案
acl_repair_gate:key =(规范化 repair target, scope)。并发调用只允许一次真实提权,其余等待并复用同一结果(成功/失败都复用,不是简单跳过)。windows_acl_repair_gate_key统一\\?\/\\?\UNC\扩展长度前缀:最近项目列表里同一项目实测会同时出现两种写法,不归一化会让同一个目录各弹一次 UAC。AGC_ACL_ELEVATION_DENIED;前端据此判定「不可自动重试」,不再依赖中文文案。src/features/app-shell/aclElevation.ts,在打开/新建项目、打开或选择目录、重命名刷新等入口统一调用clear_game_creator_acl_elevation_denials。改动前后对照
powershell -Verb RunAs,弹窗叠在一起-Wait无限期占用,其它调用各自再弹一次AGC_ACL_ELEVATION_DENIED(中文文案保留兼容)不改的东西:ACL 判据与失败关闭语义、scope 校验、授权 nonce 机制、提权脚本本身、公开 API/DTO/持久化格式。
变更文件(相对 master 13 个)
验证
cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- tests::acl_repair_gate --test-threads=1→ 10 passed:Reused;不同 target 不合并;clear_denials后显式重试可再执行;WaitTimedOut,放行后 leader 正常收尾;Reused(Failed);\\?\C:\...与C:\...写法归一到同一个 key(UNC 同理),scope 不同仍不合并。tests::configuration45 passed(未回归)。recentProjectsHook5(含「AGC_ACL_ELEVATION_DENIED这类失败只检查一次」的跨模块契约用例)+recentProjectsModel1 +appSurface220(9 skipped)+themedModal/WindowChrome+admin-web20 全通过。cargo fmt --check、npm run check:encoding、git diff --check、npm run agc:typecheck、eslint、prettier全绿。29c5409ac。未覆盖 / 未做
#[cfg(windows)],单测入口有意不触发 UAC,Linux CI 只能覆盖闸门逻辑。issue #498 里附了无需人应答的复现计数方法(对共同祖先的父目录icacls /deny,数powershell … RunAs进程与consent.exe峰值)。Start-Process -Wait目前仍无超时。中断一个正在等用户点击的 UAC 流程比等待更糟,single-flight 已把并发弹窗收成一个,follower 由 60s 窗口兜底;需要更激进策略时再单独讨论。评审结论与真机验收(issue #498)
机制
per-target single-flight(
acl_repair_gate,key =(规范化 repair target, scope))+ 结果冷却(成功 30s / 失败 15s / 用户取消 120s)+ follower 60s 有界等待 + leader 异常 RAII 兜底。闸门在锁外执行、完成时持锁写入后notify_all,没有丢唤醒;executor 可注入,单测不需要真实 UAC。评审发现并已修(
3480a2f33、1931852e9、2e45608f6)repair_path.to_string_lossy().to_lowercase()。最近项目列表里同一项目实测同时存在\\?\C:\...与C:\...两种形态,同一个物理目录会算出两个 key → single-flight 退化成「每种写法弹一次」。改为经windows_acl_repair_gate_key先normalize_windows_policy_path(去\\?\/\\?\UNC\)+ 小写;不canonicalize(待修复目标恰恰是「读不动的目录」)。逆向确认:改回旧语义后新用例打印出两个不同 key(c:\users\...\projectsvs\\?\c:\users\...\projects)。openProject(行内打开、文件选择器选择目录、运行中项目入口都走它)没有清零,用户点了打开会撞上 120s 冷却:直接失败且不弹 UAC。统一走features/app-shell/aclElevation.ts::clearAclElevationDenials()并在openProject入口先清零,补了「先 clear 再 inspect」的断言(逆向确认:去掉调用即红)。Start-Process -Wait无超时)时该 key 一直running,之后同目标调用一律 60s 超时失败,而clear_denials不清理 running —— 只能重启客户端。现在加leader_deadline(默认 5 分钟,系统对无人应答的 UAC 约 2 分钟超时,只兜真挂死),超时后新调用接管;每个 leader 带令牌,被接管后旧 leader 迟到的结果直接丢弃,不会覆盖接管者写下的结果。真机验收(Windows 11,dev 客户端,8 个同祖先项目,共同 repair target =
...\acl-e2e\ancestor)观测方式:每 200ms 采样,统计命令行同时含
-Verb RunAs与--repair-private-acl的powershell.exe(Start-Process -Wait会让它一直存活到应答)distinct PID 数与同刻并发峰值,并记录consent.exe峰值。inspect_local_project_directory(同祖先)dongy:(OI)(CI)(F),整轮 3.37s对照:修复前每个调用各自
Start-Process -Verb RunAs,8 个项目 = 8 个并发 UAC(issue #498 截图)。UI 前后对照:修复前同一列表渲染成「暂无最近项目」(8 行全部检查失败、
canOpen=false被隐藏);单次提权修复后启动页正常显示最近项目卡片且「进度 本地项目」。截图留在本机%TEMP%\agc-acl-e2e\ui-restored.png:Gitea 的 issue asset 上传接口两次都返回 500(像是服务端附件存储没开),需要内联的话说一声我换方式补。单测:
tests::acl_repair_gate10 passed(并发只执行一次、冷却复用、拒绝冷却、清除后可重试、follower 超时、leader panic 唤醒等待者、冷却基准、路径写法归一、leader 卡死接管与迟到结果丢弃);上表四条新用例都做过逆向确认(改回修复前语义即红)。残余边界
leader_deadline(5 分钟)兜底:超过后新调用接管;被接管期间旧 leader 仍可能在Start-Process -Wait上挂着,等它自行退出(系统对无人应答的 UAC 约 2 分钟超时)即释放。denied_elevation_is_reused_for_the_denial_cooldown覆盖;真机只验证了挂载后单次弹窗 + 之后无重复请求)。顺带查明的产品级事实(与本 PR 无关)
「显式拒绝当前用户 SID」形态的 ACL 损坏,提权修复也修不了:UAC 提权不改变用户 SID,DENY ACE 对提权令牌同样生效,子进程连目标都 stat 不到而失败关闭(实测子进程 stderr:
AGC ACL 提权修复失败:读取待修复私有对象失败:...: 拒绝访问。 (os error 5))。这类损坏只能人工/管理员处理;issue #498 现场更可能是「授权缺失 / 继承被改坏」这一类可修复形状——夹具换成该形状后,一次提权即修好。另外icacls /grant:r不删除 DENY ACE(需/remove:d),复现时容易踩。