fix(ci): M91 r3 —— unsafe-scope 门禁换成词法器(括号字面量/expect/块注释三处假绿根治)
必修 1:不配对括号字面量把 r2 的「全文净深度合并」带偏,文件尾部永久不可见
- r2 用「方括号净深度」决定续行合并,但 ['[', '(']、"[2"、starts_with("[2") 这类字面量
会让深度永久偏离 0(实测 601 文件里 10 个扫到 EOF 仍非 0),于是该文件后面的一切都被跳过:
在 gate.rs 尾部追加作用域 allow,r2 门禁 exit 0(假绿)
- 换成逐字符词法器:普通 / 行注释 / 块注释(可嵌套)/ 字符串 / raw 字符串 / 字符字面量,
只在普通态认 `#` 或 `#!` 紧跟 `[` 的属性起点,只在这一个属性内部累计到 `]` 配平;
数组字面量跨行不再参与合并
必修 2:`#![expect(unsafe_code)]` 既绕过门禁也绕过 workspace 的 deny
- 判据从 allow 放宽到 (allow|expect):expect 同样放行 unsafe(rustc -D unsafe_code 实测通过)
必修 3:块注释里的属性文本被当成第二个落点(与「注释刻意不算」的承诺相反)
- 词法器带块注释状态机(含嵌套),行注释/块注释/字符串里的属性文本都不算
顺带
- 已知边界对齐事实:宏体里直接写出的属性**会**被扫到;真正漏的是「宏在别处定义、这里只调用」;
补两条:注释/字符串里的属性文本刻意不算、manifest 侧 [lints.rust] unsafe_code 不在扫描面
- 候选文件用 git grep -l unsafe_code 预筛(标识符不会被空白拆开,不会漏),整个 job 约 1~2 秒
This commit is contained in:
1 parent
fa2c6560fd
commit
03ce813c32
1 file changed
+114
-40
+114
-40
@@ -153,56 +153,130 @@ jobs:
|
||||
# D36 定案:整窗 α 是全仓**唯一**需要 `unsafe` 的能力(Win32 的
|
||||
# `SetLayeredWindowAttributes` 没有安全替代),落点收在一个文件里。
|
||||
# 判据必须是**集合相等**而不是「黑名单命中」:
|
||||
# - 新增第二个带 allow(unsafe_code) 的文件 → 红(黑名单写法在这里会假绿);
|
||||
# - 落点文件把 allow 删掉 / 改名 / 搬走 → 红(否则门禁守着一条已经不存在的路径);
|
||||
# - 落点文件里多开别的 unsafe 能力 → 门禁看不见(这是本守门的已知边界,
|
||||
# workspace 的 `unsafe_code = "deny"` 也只是按文件放行,逐行审计靠 code review);
|
||||
# - 多行属性 / 同行合并属性:**已由跨行扫描覆盖**(r2 加固前是行级正则,那两种写法能过闸,
|
||||
# 当时唯一的缓解是 ci.yml 里独立的 fmt job——rustfmt 会把它们归成一行。现在不再依赖 fmt)。
|
||||
# - 新增第二个显式放行 unsafe 的文件 → 红(黑名单写法在这里会假绿);
|
||||
# - 落点文件把放行删掉 / 改名 / 搬走 → 红(否则门禁守着一条已经不存在的路径);
|
||||
# - 多行属性、同行合并属性、多行 `cfg_attr`、`expect(unsafe_code)` 都已覆盖
|
||||
# (r2 前的行级正则对前两种会假绿,当时只能靠 ci.yml 的 fmt job 兜底;r3 起由词法器直接抓)。
|
||||
unsafe-scope:
|
||||
name: unsafe 落点守门(集合必须恰好一个文件)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 全仓 allow(unsafe_code) 的文件集合 == 唯一落点
|
||||
- name: 全仓显式放行 unsafe 的文件集合 == 唯一落点
|
||||
run: |
|
||||
set -uo pipefail
|
||||
EXPECTED='crates/app-iced/src/platform/win32/transparency.rs'
|
||||
# 扫描口径(M91 r2 加固):**跨行**扫描属性,不再用行级正则。
|
||||
# 旧版是单行正则,实测有两种绕过能过闸:多行属性 `#![allow(\n unsafe_code\n)]`、
|
||||
# 以及同行两个属性 `#[inline] #[allow(unsafe_code)]`——当时只能靠 ci.yml 的独立
|
||||
# fmt job 兜底(rustfmt 会把这两种写法归成一行,所以 fmt 红会间接拦住它们)。
|
||||
# 现在不依赖 fmt 了:注释先去掉,再把「方括号未闭合」的续行并成逻辑行,
|
||||
# 最后在逻辑行里逐个属性组 `#[...]` / `#![...]` 判断是否含 `allow(...unsafe_code...)`。
|
||||
# 扫描口径(M91 r3):一个小词法器,而不是正则。
|
||||
#
|
||||
# 仍然抓不住的(已知边界,别把它当全覆盖):
|
||||
# - 落点文件**内部**多开别的 unsafe 能力(本门禁只看「有没有 allow」,不看 unsafe 块有几个);
|
||||
# - 宏/`include!` 生成出来的 allow(扫描的是源码文本,不是展开后的 HIR);
|
||||
# - 字符串字面量里写着属性文本的行(会被当代码扫;本仓库不存在这种行);
|
||||
# - 注释里提到的属性**刻意不算**(`crates/app/src/main.rs` 的说明文字就提到过它)。
|
||||
# 为什么必须用词法器:r2 的写法是「按全文方括号净深度合并续行」,但 Rust 里
|
||||
# `['[', '(']`、`"[2"`、`starts_with("[2")` 这类**不配对括号字面量**会让深度永久跑偏,
|
||||
# 于是该文件后面的一切(包括一个真的 `#[allow(unsafe_code)]`)都再也不会被扫到——
|
||||
# 实测本仓 601 个文件里有 10 个扫到 EOF 仍非 0,`gate.rs` 就是其中之一(r3 复审实证:
|
||||
# 在它尾部加作用域 allow,r2 门禁 exit 0 假绿)。
|
||||
# 现在的做法:逐字符状态机(普通 / 行注释 / 块注释(可嵌套)/ 字符串 / raw 字符串 /
|
||||
# 字符字面量),只在「普通态」里认 `#` 或 `#!` 紧跟 `[` 的属性起点,然后只在这一个
|
||||
# 属性内部累计到它的 `]` 配平——数组字面量跨行不再参与合并,也就不会带偏。
|
||||
#
|
||||
# 放行的两种拼法都算:`allow(...)` 与 `expect(...)`。`expect(unsafe_code)` 同样会
|
||||
# 放行 unsafe 代码(rustc 实测:`#![expect(unsafe_code)]` + `pub unsafe fn f() {}` 在
|
||||
# `-D unsafe_code` 下编译通过),所以它必须被同一条判据抓住,不能只认 allow。
|
||||
#
|
||||
# 仍然抓不住的(已知边界,别当全覆盖):
|
||||
# - 落点文件**内部**多开别的 unsafe 能力(本门禁只看「有没有放行属性」,不看 unsafe 块个数);
|
||||
# - **在别处定义、这里只调用**的宏展开出来的 allow:宏体里直接写出的属性在源码文本里,
|
||||
# 会被扫到;但「宏定义在别的文件、调用点展开出 allow」时,本文件源码里没有那个属性;
|
||||
# - 注释(行注释/块注释)与字符串(普通/raw)里的属性文本**刻意不算**:它们不构成放行;
|
||||
# 代价是「真把 allow 写进字符串」这种写法不会被当作落点(它也确实不放行任何代码);
|
||||
# - manifest 侧 `[lints.rust] unsafe_code = "allow"`:本门禁只扫 `*.rs`,看不见 Cargo.toml;
|
||||
scan() {
|
||||
awk '
|
||||
BEGIN { buf = ""; depth = 0; found = 0 }
|
||||
{
|
||||
line = $0
|
||||
if (line ~ /^[[:space:]]*\/\//) next
|
||||
sub(/\/\/.*$/, "", line)
|
||||
buf = (buf == "" ? line : buf " " line)
|
||||
opens = gsub(/\[/, "[", line)
|
||||
closes = gsub(/\]/, "]", line)
|
||||
depth += opens - closes
|
||||
if (depth > 0) next
|
||||
s = buf; buf = ""; depth = 0
|
||||
while (match(s, /#!?\[[^]]*\]/)) {
|
||||
attr = substr(s, RSTART, RLENGTH)
|
||||
if (attr ~ /allow\([^]]*unsafe_code/) found = 1
|
||||
s = substr(s, RSTART + RLENGTH)
|
||||
awk -v path="$1" '
|
||||
function lex( s, line, n, i, c, nx, state, depth, hashes, j, ok, k, ch, ch2, adepth, attr, q) {
|
||||
q = "\047"
|
||||
s = ""
|
||||
while ((getline line < path) > 0) s = s line "\n"
|
||||
close(path)
|
||||
n = length(s)
|
||||
state = 0; depth = 0; hashes = 0; found = 0
|
||||
for (i = 1; i <= n; i++) {
|
||||
c = substr(s, i, 1)
|
||||
if (state == 1) { if (c == "\n") state = 0; continue }
|
||||
if (state == 2) {
|
||||
if (c == "/" && substr(s, i + 1, 1) == "*") { depth++; i++; continue }
|
||||
if (c == "*" && substr(s, i + 1, 1) == "/") { depth--; i++; if (depth <= 0) state = 0; continue }
|
||||
continue
|
||||
}
|
||||
if (state == 3) {
|
||||
if (c == "\\") { i++; continue }
|
||||
if (c == "\"" || c == "\n") state = 0
|
||||
continue
|
||||
}
|
||||
if (state == 4) {
|
||||
if (c == "\"") {
|
||||
ok = 1
|
||||
for (j = 1; j <= hashes; j++) if (substr(s, i + j, 1) != "#") ok = 0
|
||||
if (ok) { state = 0; i += hashes }
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (state == 5) {
|
||||
if (c == "\\") { i++; continue }
|
||||
if (c == q || c == "\n") state = 0
|
||||
continue
|
||||
}
|
||||
if (c == "/" && substr(s, i + 1, 1) == "/") { state = 1; i++; continue }
|
||||
if (c == "/" && substr(s, i + 1, 1) == "*") { state = 2; depth = 1; i++; continue }
|
||||
if (c == "\"") { state = 3; continue }
|
||||
if (c == q) {
|
||||
nx = substr(s, i + 1, 1)
|
||||
if (nx == "\\" || substr(s, i + 2, 1) == q) state = 5
|
||||
continue
|
||||
}
|
||||
if (c == "r" || c == "b") {
|
||||
j = i + 1
|
||||
if (c == "b") {
|
||||
if (substr(s, j, 1) == "\"") { state = 3; i = j; continue }
|
||||
if (substr(s, j, 1) == q) { state = 5; i = j; continue }
|
||||
if (substr(s, j, 1) != "r") continue
|
||||
j++
|
||||
}
|
||||
hashes = 0
|
||||
while (substr(s, j, 1) == "#") { hashes++; j++ }
|
||||
if (substr(s, j, 1) == "\"") { state = 4; i = j; continue }
|
||||
continue
|
||||
}
|
||||
if (c == "#") {
|
||||
j = i + 1
|
||||
if (substr(s, j, 1) == "!") j++
|
||||
if (substr(s, j, 1) != "[") continue
|
||||
attr = ""; adepth = 0; k = j
|
||||
while (k <= n) {
|
||||
ch = substr(s, k, 1)
|
||||
if (ch == "\"") {
|
||||
k++
|
||||
while (k <= n) {
|
||||
ch2 = substr(s, k, 1)
|
||||
if (ch2 == "\\") { k += 2; continue }
|
||||
if (ch2 == "\"") break
|
||||
k++
|
||||
}
|
||||
} else if (ch == "[") { adepth++ }
|
||||
else if (ch == "]") { adepth--; if (adepth == 0) break }
|
||||
attr = attr ch
|
||||
k++
|
||||
}
|
||||
if (attr ~ /(allow|expect)\([^]]*unsafe_code/) found = 1
|
||||
i = k
|
||||
continue
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
END { exit(found ? 0 : 1) }
|
||||
' "$1"
|
||||
BEGIN { exit(lex() ? 0 : 1) }
|
||||
'
|
||||
}
|
||||
actual=$(git ls-files '*.rs' | while IFS= read -r f; do
|
||||
# 只对「文本里出现过 unsafe_code」的文件跑词法器(标识符不可能被空白拆开,
|
||||
# 所以这个预筛不会漏)。601 个文件 → 个位数个候选,整个 job 约 2 秒。
|
||||
actual=$(git grep -l 'unsafe_code' -- '*.rs' 2>/dev/null | LC_ALL=C sort | while IFS= read -r f; do
|
||||
if scan "$f"; then printf '%s\n' "$f"; fi
|
||||
done | LC_ALL=C sort)
|
||||
echo "期望(恰好这一个):$EXPECTED"
|
||||
@@ -215,12 +289,12 @@ jobs:
|
||||
printf '%s\n' "$extra" | sed 's/^/ - /'
|
||||
fi
|
||||
if ! printf '%s\n' "$actual" | grep -qxF "$EXPECTED"; then
|
||||
echo "::error file=$EXPECTED::这个文件里没有 allow(unsafe_code) —— 落点被搬走/改名/删了 allow?守门路径已失效。"
|
||||
echo "::error file=$EXPECTED::这个文件里没有显式放行 unsafe(allow / expect(unsafe_code))—— 落点被搬走/改名/删了放行?守门路径已失效。"
|
||||
fi
|
||||
echo "::error::整窗 α 的 FFI 只允许住在 $EXPECTED(D36)。需要新的 unsafe 能力请先立 mission:改这条守门 + 更新 D36 的定案,不要顺手 allow。"
|
||||
echo "::error::整窗 α 的 FFI 只允许住在 $EXPECTED(D36)。需要新的 unsafe 能力请先立 mission:改这条守门 + 更新 D36 的定案,不要顺手放行。"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: 全仓 allow(unsafe_code) 恰好只有 $EXPECTED"
|
||||
echo "OK: 全仓显式放行 unsafe 的文件恰好只有 $EXPECTED"
|
||||
|
||||
file-size:
|
||||
name: 文件行数 ≤400 守门
|
||||
|
||||
Reference in new issue
Block a user