fix(chv-muse-r11fix): cell 只遮 /usr 下路径(复测新发现 1)
--volatile=yes 下容器根是基本空的 tmpfs(只挂进只读 /usr), nspawn 按容器根解析 --inaccessible 路径,/etc/chv 与 /home/chv/.git-credentials 在容器根里根本不存在,解析失败即 Failed to resolve 而起不来;两条 /usr 旗可解析,保留。 凭证文件按构造即不可达(容器根里无 /home、无 /etc/chv), 遮蔽不再是必须项,安全属性不变。 探针证据:celldiag2/3/4——去 inaccessible 后完整 argv 0.4s/rc=0, 单元内 $HOME/.cache 可写,单元内 ls /etc/chv 本来就不存在。
This commit is contained in:
1 parent
04287f6053
commit
581e850eb7
2 files changed
+48
-30
No files matched your search
+11
-5
@@ -11,7 +11,9 @@ systemd),单元即沙箱——单元内 codex 用 danger-full-access,不
|
|||||||
--private-users=1000:65536 —— 单元内 uid 0 → 客机侧 chv(uid 1000)。
|
--private-users=1000:65536 —— 单元内 uid 0 → 客机侧 chv(uid 1000)。
|
||||||
效果:workspace / ~/.codex(chv 所有)单元内仍可写;host-root 0600
|
效果:workspace / ~/.codex(chv 所有)单元内仍可写;host-root 0600
|
||||||
文件(如 /etc/chv/guest-env)在单元内显示为 overflow uid、不可读;
|
文件(如 /etc/chv/guest-env)在单元内显示为 overflow uid、不可读;
|
||||||
再加 --inaccessible 遮蔽作纵深防御。
|
再加 --inaccessible 遮蔽作纵深防御(只遮 /usr 下路径:--volatile=yes
|
||||||
|
下容器根是基本空的 tmpfs,/etc/chv 等在容器根里根本不存在,按构造
|
||||||
|
即不可达,列出反而让 nspawn 解析失败而起不来,见 INACCESSIBLE_BASE)。
|
||||||
|
|
||||||
网络:--network-veth。宿主侧 ve-chv-cell(10.200.0.1/24),单元侧
|
网络:--network-veth。宿主侧 ve-chv-cell(10.200.0.1/24),单元侧
|
||||||
host0(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站
|
host0(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站
|
||||||
@@ -79,11 +81,15 @@ DROP_CAPABILITIES = [
|
|||||||
SYSCALL_FILTER = ("@system-service ~io_uring_setup "
|
SYSCALL_FILTER = ("@system-service ~io_uring_setup "
|
||||||
"~io_uring_enter ~io_uring_register")
|
"~io_uring_enter ~io_uring_register")
|
||||||
|
|
||||||
# 单元内遮掉的路径:凭证与浏览器(单元内跑不起自己的浏览器,
|
# 单元内遮掉的路径:浏览器(单元内跑不起自己的浏览器,
|
||||||
# 只能走 broker 的 chv-browser)。
|
# 只能走 broker 的 chv-browser)。只列 /usr 下的路径——
|
||||||
|
# round-11 复测纠正(第二轮):--volatile=yes 下容器根是
|
||||||
|
# 基本空的 tmpfs(只挂进只读 /usr),nspawn 按容器根解析
|
||||||
|
# --inaccessible 路径,不存在的路径即启动失败;/etc/chv 与
|
||||||
|
# /home/chv/.git-credentials 在容器根里根本不存在(无 /home、
|
||||||
|
# 无 /etc/chv),凭证文件按构造就不可达,不再列出遮蔽。
|
||||||
|
# 旧的两项若列回,cell 会报 Failed to resolve 而起不来。
|
||||||
INACCESSIBLE_BASE = [
|
INACCESSIBLE_BASE = [
|
||||||
"/etc/chv",
|
|
||||||
"/home/chv/.git-credentials",
|
|
||||||
"/usr/local/share/ms-playwright",
|
"/usr/local/share/ms-playwright",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,17 @@ class CellArgvTest(unittest.TestCase):
|
|||||||
playwright_package_dir="/usr/local/lib/python3.12/dist-packages/playwright")
|
playwright_package_dir="/usr/local/lib/python3.12/dist-packages/playwright")
|
||||||
hidden = [a.split("=", 1)[1] for a in argv
|
hidden = [a.split("=", 1)[1] for a in argv
|
||||||
if a.startswith("--inaccessible=")]
|
if a.startswith("--inaccessible=")]
|
||||||
for path in ("/etc/chv", "/home/chv/.git-credentials",
|
# /usr 下的浏览器路径仍遮蔽(/usr 只读挂进单元,遮蔽有效)。
|
||||||
"/usr/local/share/ms-playwright",
|
for path in ("/usr/local/share/ms-playwright",
|
||||||
"/usr/local/lib/python3.12/dist-packages/playwright"):
|
"/usr/local/lib/python3.12/dist-packages/playwright"):
|
||||||
self.assertIn(path, hidden)
|
self.assertIn(path, hidden)
|
||||||
|
# round-11 复测纠正(第二轮):/etc/chv 与
|
||||||
|
# /home/chv/.git-credentials 不再列出——--volatile=yes 下
|
||||||
|
# 容器根是基本空的 tmpfs,这两处在容器根里根本不存在,
|
||||||
|
# 凭证文件按构造即不可达;列出反而让 nspawn 解析失败
|
||||||
|
#(Failed to resolve)而起不来。
|
||||||
|
for path in ("/etc/chv", "/home/chv/.git-credentials"):
|
||||||
|
self.assertNotIn(path, hidden)
|
||||||
|
|
||||||
def test_writable_binds_and_tmpfs(self):
|
def test_writable_binds_and_tmpfs(self):
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
@@ -348,26 +355,27 @@ class BrowserTest(unittest.TestCase):
|
|||||||
ok, _ = BROWSER.validate_url(bad)
|
ok, _ = BROWSER.validate_url(bad)
|
||||||
self.assertFalse(ok, bad)
|
self.assertFalse(ok, bad)
|
||||||
|
|
||||||
def test_truncate_a11y_small_tree_untouched(self):
|
def test_truncate_aria_small_snapshot_untouched(self):
|
||||||
tree = {"role": "WebArea", "name": "Example",
|
text = ("- generic [ref=e1]:\n"
|
||||||
"children": [{"role": "heading", "name": "Hi"}]}
|
" - heading \"Example Domain\" [level=1] [ref=e2]")
|
||||||
out, truncated = BROWSER.truncate_a11y(tree)
|
out, truncated = BROWSER.truncate_aria(text)
|
||||||
self.assertFalse(truncated)
|
self.assertFalse(truncated)
|
||||||
self.assertEqual(out, tree)
|
self.assertEqual(out, text)
|
||||||
|
|
||||||
def test_truncate_a11y_node_cap(self):
|
def test_truncate_aria_node_cap(self):
|
||||||
tree = {"role": "WebArea",
|
text = "\n".join("- link \"l%d\" [ref=e%d]" % (i, i)
|
||||||
"children": [{"role": "link", "name": "l%d" % i}
|
for i in range(300))
|
||||||
for i in range(300)]}
|
out, truncated = BROWSER.truncate_aria(text)
|
||||||
out, truncated = BROWSER.truncate_a11y(tree)
|
|
||||||
self.assertTrue(truncated)
|
self.assertTrue(truncated)
|
||||||
self.assertLessEqual(len(out["children"]), 200)
|
nodes = [ln for ln in out.splitlines()
|
||||||
|
if ln.lstrip().startswith("- ")]
|
||||||
|
self.assertLessEqual(len(nodes), 200)
|
||||||
|
|
||||||
def test_truncate_a11y_char_cap(self):
|
def test_truncate_aria_char_cap(self):
|
||||||
tree = {"role": "WebArea", "name": "x" * 70000, "children": []}
|
text = "- paragraph \"" + "x" * 70000 + "\" [ref=e1]"
|
||||||
out, truncated = BROWSER.truncate_a11y(tree)
|
out, truncated = BROWSER.truncate_aria(text)
|
||||||
self.assertTrue(truncated)
|
self.assertTrue(truncated)
|
||||||
self.assertTrue(out.get("truncated_chars"))
|
self.assertLessEqual(len(out), BROWSER.MAX_A11Y_CHARS)
|
||||||
|
|
||||||
def test_forbidden_ops_never_execute(self):
|
def test_forbidden_ops_never_execute(self):
|
||||||
agent = mock.Mock()
|
agent = mock.Mock()
|
||||||
@@ -405,7 +413,11 @@ class BrowserTest(unittest.TestCase):
|
|||||||
self.assertTrue(resp["ok"])
|
self.assertTrue(resp["ok"])
|
||||||
self.assertEqual(resp["title"], "Example Domain")
|
self.assertEqual(resp["title"], "Example Domain")
|
||||||
self.assertEqual(resp["url"], "https://example.com/")
|
self.assertEqual(resp["url"], "https://example.com/")
|
||||||
self.assertEqual(resp["a11y"]["role"], "WebArea")
|
# round-11 复测纠正(第二轮):a11y 是 ARIA 快照字符串
|
||||||
|
#(Playwright Page.aria_snapshot()),不是 dict 树。
|
||||||
|
self.assertIsInstance(resp["a11y"], str)
|
||||||
|
self.assertIn("Example Domain", resp["a11y"])
|
||||||
|
self.assertFalse(resp["truncated"])
|
||||||
snap = agent.snapshot()
|
snap = agent.snapshot()
|
||||||
self.assertTrue(snap["ok"])
|
self.assertTrue(snap["ok"])
|
||||||
shot = agent.screenshot()
|
shot = agent.screenshot()
|
||||||
@@ -488,13 +500,13 @@ class FakePage:
|
|||||||
def title(self):
|
def title(self):
|
||||||
return "Example Domain"
|
return "Example Domain"
|
||||||
|
|
||||||
@property
|
# 真实 Playwright 1.64.1 的快照入口是 Page.aria_snapshot()
|
||||||
def accessibility(self):
|
#(返回字符串);旧 fake 的 accessibility.snapshot() 是
|
||||||
return self
|
# Puppeteer 的形状,真实库里不存在,round-11 第二轮复测
|
||||||
|
# 已证实会报 'Page' object has no attribute 'accessibility'。
|
||||||
def snapshot(self):
|
def aria_snapshot(self):
|
||||||
return {"role": "WebArea", "name": "Example Domain",
|
return ("- generic [ref=e1]:\n"
|
||||||
"children": [{"role": "heading", "name": "Example"}]}
|
" - heading \"Example Domain\" [level=1] [ref=e2]")
|
||||||
|
|
||||||
def screenshot(self, path=None):
|
def screenshot(self, path=None):
|
||||||
with open(path, "wb") as fh:
|
with open(path, "wb") as fh:
|
||||||
|
|||||||
Reference in new issue
Block a user