@@ -83,15 +83,29 @@ TEST(SandboxPolicy, GlobMatchingFollowsGitStyleSemantics) {
8383 EXPECT_FALSE (glob_matches_path (" /home/u/[!a]*.key" , " /home/u/a1.key" ));
8484}
8585
86+ namespace {
87+
88+ // 手工拼的策略也要守 make_sandbox_policy 的约定:条目是 canonical 形态
89+ // (WritableRoot::root 等「已 weakly_canonical」),查询路径由 resolve_access 自己规范化。
90+ // Windows 上 `/work` 这类无盘符路径按当前盘解析:当前盘恰好有 \work 目录时(本机
91+ // 2026-09-22 起就有 C:\work)查询变成 C:\work\...,未规范化的条目还是 /work,两侧对不上 ——
92+ // 旧写法让用例随机器上有没有这个目录时过时不过。
93+ std::string canon (const std::string& path) { return canonical_policy_path (path); }
94+
95+ } // namespace
96+
8697// 场景:deny 名单、可写根、只读子路径、可读根共存时的访问判定。期望:最长前缀
8798// 命中优先(写根下的 deny 子目录仍 Deny;deny 目录下配置的写根反而可写),同深度
8899// deny > write > read;全盘可读时未命中的路径 Read,受限读时 Deny;FullAccess 恒 Write。
100+ // 回归:条目曾直接写 `/work` 而不经规范化,在存在 C:\work 的 Windows 机器上
101+ // `/work/src/main.cpp` 判成 Read、`/work/secrets/key` 判成 Read(期望 Write / Deny)。
89102TEST (SandboxPolicy, ResolveAccessPrefersDeepestEntryThenDenyWriteRead) {
90103 SandboxPolicy policy;
91104 policy.mode = SandboxMode::WorkspaceWrite;
92- policy.writable_roots = {{" /work" , {" /work/.acecode/rules" }}, {" /home/u/.cache/allowed" , {}}};
93- policy.denied_paths = {" /work/secrets" , " /home/u/.cache" };
94- policy.denied_globs = {" /work/**/.env" };
105+ policy.writable_roots = {{canon (" /work" ), {canon (" /work/.acecode/rules" )}},
106+ {canon (" /home/u/.cache/allowed" ), {}}};
107+ policy.denied_paths = {canon (" /work/secrets" ), canon (" /home/u/.cache" )};
108+ policy.denied_globs = {canon (" /work" ) + " /**/.env" };
95109 EXPECT_EQ (policy.resolve_access (" /work/src/main.cpp" ), FsAccess::Write);
96110 EXPECT_EQ (policy.resolve_access (" /work/.acecode/rules/x.rules" ), FsAccess::Read);
97111 EXPECT_EQ (policy.resolve_access (" /work/secrets/key" ), FsAccess::Deny);
@@ -101,19 +115,58 @@ TEST(SandboxPolicy, ResolveAccessPrefersDeepestEntryThenDenyWriteRead) {
101115 EXPECT_EQ (policy.resolve_access (" /etc/hosts" ), FsAccess::Read) << " 全盘可读" ;
102116 EXPECT_TRUE (policy.can_read (" /etc/hosts" ));
103117 EXPECT_FALSE (policy.can_write (" /etc/hosts" ));
104- policy.readable_roots = {" /work" , " /usr" };
118+ policy.readable_roots = {canon ( " /work" ), canon ( " /usr" ) };
105119 EXPECT_EQ (policy.resolve_access (" /etc/hosts" ), FsAccess::Deny) << " 受限读未命中" ;
106120 EXPECT_EQ (policy.resolve_access (" /usr/lib/libc.so" ), FsAccess::Read);
107121 SandboxPolicy full;
108122 EXPECT_EQ (full.resolve_access (" /anything" ), FsAccess::Write);
109123 // 同深度:同一路径既是写根又在 deny 里 → Deny。
110124 SandboxPolicy tie;
111125 tie.mode = SandboxMode::WorkspaceWrite;
112- tie.writable_roots = {{" /both" , {}}};
113- tie.denied_paths = {" /both" };
126+ tie.writable_roots = {{canon ( " /both" ) , {}}};
127+ tie.denied_paths = {canon ( " /both" ) };
114128 EXPECT_EQ (tie.resolve_access (" /both/x" ), FsAccess::Deny);
115129}
116130
131+ // 场景:家目录经 junction / 符号链接以「别名」出现(本机实况:C:\Users\shao 是指向
132+ // N:\Users\shao 的 junction)。deny 名单里的 ~/.ssh 已按 canonical 展开成真实路径;
133+ // 申请(模型的 additional_permissions、会话授权)写的却是 <别名>/.ssh/newdir —— 目录还
134+ // 不存在,normalize_policy_path 只做词法归一,别名原样留着。
135+ // 期望:deny 名单仍压过授权 —— 授权被忽略,真实路径与别名路径下都不可写,is_denied
136+ // 对两种形态都成立。
137+ // 回归:deny 判定(denied_by)拿未规范化的授权路径和 canonical 的 deny 条目逐字比较,
138+ // 别名形态对不上,授权进了可写根;又因为「更深的写根压过浅的 deny」,~/.ssh 下的新
139+ // 目录变成可写。
140+ TEST (SandboxPolicy, DenyListStillWinsWhenGrantUsesDirectoryAlias) {
141+ test::TempTree tree;
142+ const auto real_home = tree.dir (" real-home" );
143+ tree.dir (" real-home/.ssh" );
144+ const auto workspace = tree.dir (" work" );
145+ const auto alias_home = tree.root / " alias-home" ;
146+ if (!test::make_directory_alias (real_home, alias_home)) {
147+ GTEST_SKIP () << " cannot create a directory alias here" ;
148+ }
149+ const std::string alias_grant = normalize_policy_path (path_to_utf8 (alias_home / " .ssh" / " newdir" ));
150+ ASSERT_NE (alias_grant.find (" alias-home" ), std::string::npos) << " 申请路径保持别名形态" ;
151+ const std::string real_newdir = path_to_utf8 (fs::weakly_canonical (real_home / " .ssh" / " newdir" ));
152+
153+ SandboxPolicyOptions options;
154+ options.include_tmpdir = false ;
155+ options.home_override = path_to_utf8 (alias_home);
156+ options.denied_entries = {" ~/.ssh" };
157+ options.grants .write = {alias_grant};
158+ const auto policy = make_sandbox_policy (SandboxMode::WorkspaceWrite, path_to_utf8 (workspace), options);
159+
160+ for (const auto & root : policy.writable_roots ) {
161+ EXPECT_EQ (root.root .find (" .ssh" ), std::string::npos) << " deny 名单下的授权进了可写根: " << root.root ;
162+ }
163+ EXPECT_EQ (policy.resolve_access (real_newdir + " /id_rsa" ), FsAccess::Deny);
164+ EXPECT_EQ (policy.resolve_access (alias_grant + " /id_rsa" ), FsAccess::Deny);
165+ EXPECT_TRUE (policy.is_denied (alias_grant));
166+ EXPECT_TRUE (policy.is_denied (real_newdir));
167+ EXPECT_FALSE (policy.is_denied (path_to_utf8 (workspace / " src" )));
168+ }
169+
117170// 场景:make_sandbox_policy 把配置条目、默认 deny 名单、会话授权揉进策略。期望:
118171// deny 展开成绝对路径 / glob;落在可写根下的 deny 路径同时列为只读子路径(三平台
119172// 写拒绝一致);授权的写路径进可写根,但在 deny 名单上的授权被忽略;配置了 read
0 commit comments