#!/usr/bin/env bash # 源文件的权限位**政策**:不许比 0644 更严。 # # # 为什么要这条(一个实测出来的实例) # # 这台机器上写文件的工具**不理会 umask**(umask 是 022,但它建的仍是 0600)。 # 后果不是"本地难看",而是它会**进生产**:`deploy/redeploy-plugin.sh` 用 # `cp -a "$SRC/." "$STAGING/"` 打快照,`-a` 保留权限位 —— # 于是快照里 `src/paths.mjs`/`src/turn-cwd.mjs` 是 **0600**,而仓库是 0644。 # # 两头都不报警: # · `cmp` 只比**内容** ⇒ 报"逐字节相同"(实测就是这样); # · `check-deploy-drift.mjs` 只比**内容**、且只在脚本上判**可执行位**(`mode & 0o111`)。 # ⇒ "线上与仓库不一致"里,**只有权限位这一类**完全没有判据看着。 # # # 与漂移侧那条的分工(pi 评审 2026-09-15 §三 明确要求分开) # # · **本脚本 = 政策**:源文件不得比 0644 更严。它**不**看快照, # 所以"两边都是 0600"这种情况它能抓到,而漂移侧永远抓不到(两边一致 ⇒ 恒绿)。 # · **漂移侧 = 一致性**:部署副本的权限位 = 仓库那一份。 # 它抓不到"两边都错"。 # 两条别合成一条:合成的结果是"看起来覆盖了、其实只覆盖一半"。 # # # 为什么"更严"也算错 # # 这些文件是**源码**,要被构建/测试/部署以不同身份读(`install.sh` 会切身份做可写性判定, # 判据里还有以 nobody 跑的分支)。0600 在"跑的人恰好是属主"时不炸,一旦换身份就是 # EACCES —— 而那串报错看起来是**代码问题**。这条链一路上在消的就是这一类。 set -uo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO" # 允许的最严权限:0644。比它更严(组/其他读位缺失)即判红。 # 例外:**可执行**脚本的 0755 不是"更严",它多了执行位、读位齐全,属正常。 # 允许的最严权限:0644(非可执行)/ 0755(可执行)。 # # ★ 可执行与否按 **git 记录的那个位**判(`git ls-files -s` 的 100755),不按当前文件系统权限 # —— 否则"执行位被谁弄丢了"会被这条判据**追认**成正常。 # # ★ 这条我第一版写错了,代价是**真的弄坏了一个脚本**:`client/electron/scripts/release-linux.sh` # 原本是 **711**(group/other 读位缺失 ⇒ 本判据判红,判得对),但我给的药方是无脑 # `chmod 644` —— 那是**去掉执行位**,等于把"读位缺失"换成"不能执行",比原来更坏, # 而判据随后报"通过"。⇒ 一条判据说"这个文件被改错了"时,**药方必须按文件类型分岔**; # 只有一个药方就会把另一类文件改坏,且同样显示"已修好"。 fail=0 worst="" count=0 while IFS= read -r line; do gmode="${line%% *}" f="${line##* }" [ -n "$f" ] || continue [ -f "$f" ] || continue mode="$(stat -c '%a' "$f" 2>/dev/null)" || continue m="${mode: -3}" o="${m: -1}"; g="${m:1:1}"; u="${m:0:1}" bad=0 # group 与 other 必须有读位 [ "$(( o & 4 ))" -eq 0 ] && bad=1 [ "$(( g & 4 ))" -eq 0 ] && bad=1 # git 记为可执行的,三处执行位都要在 if [ "$gmode" = "100755" ]; then [ "$(( u & 1 ))" -eq 0 ] && bad=1 [ "$(( g & 1 ))" -eq 0 ] && bad=1 [ "$(( o & 1 ))" -eq 0 ] && bad=1 fi [ "$bad" -eq 0 ] && continue count=$((count + 1)) [ -z "$worst" ] && worst="$f" if [ "$gmode" = "100755" ]; then printf ' [FAIL] 权限不对:%s 是 %s(git 记为可执行 ⇒ 应为 0755,注意别去掉执行位)\n' "$f" "$mode" >&2 else printf ' [FAIL] 权限过严:%s 是 %s(政策要求不严于 0644)\n' "$f" "$mode" >&2 fi done < <(git ls-files -s | awk '{print $1" "$4}') # ══════════════════════════════════════════════════════════════════════════════ # ★★ 目录的**可访问性**(2026-09-18,pi 报的洞) # # 上面那段只遍历 `git ls-files` ⇒ **只看文件**。而"目录缺 `x`"比"文件 0644 更严"**更严重**: # 目录没有 `x` 就**不能访问其中任何文件**(连 stat 都不行),而 root 绕过这一层, # 所以维护者**永远不会在自己身上看到症状**。实测(pi 报、我复现): # # $ stat -c '%A %n' client/electron/test/mutants/jobs # drw-r--r-- client/electron/test/mutants/jobs ← 缺 x # $ runuser -u nobody -- python3 …/summary.py # PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json' # # ⇒ 那份"口径的唯一权威"在这份工作树里**只有 root 跑得起来**,而它落在**任何判据的射程之外** # (本政策只看文件;权限位判据当初就是为"该不该算漂移"立的,覆盖的形状是"文件")。 # # ⚠️ 范围与取向(**保守,只判"不可访问"**): # · git **不存目录权限** ⇒ 这是**本地状态**,不跟着提交走、新克隆不受影响。 # 所以这里判的是"**这份工作树此刻能不能被别人用**",不是"仓库内容对不对"。 # · 只判**有 `git ls-files` 条目的目录**(即"仓库内容所在的目录"), # 不去管 node_modules / dist 这类产物的目录 —— 那会把这条判据淹掉。 # · 只判 `u+x`(**属主**可进入):这是"别把自己锁在外面"的最低要求, # 不去管 group/other —— 那取决于本机 umask 与身份,判严了会到处误红。 # ══════════════════════════════════════════════════════════════════════════════ dir_bad=0 dir_worst="" while IFS= read -r d; do [ -n "$d" ] && [ -d "$d" ] || continue # ⚠️⚠️ **不许用 `[ -x "$d" ]`** —— 这是我第一版写的,而它**恒为真**(我实测抓到的): # root **无视权限位**(这正是本政策存在的理由之一),所以 `test -x` 对 uid=0 # **永远返回 0**,哪怕目录是 `drw-r--r--`。⇒ 变异验证时"撤掉 x"根本不触发, # 而**输出看起来完全正常**(这正是被测的那个病本身长在判据上)。 # ⇒ 必须**直接读权限位**(与上面文件那段的 `o/g/u` 位运算同一个做法)。 dperm="$(stat -c '%a' "$d" 2>/dev/null)" || continue dm="${dperm: -3}" [ "$(( ${dm:0:1} & 1 ))" -eq 1 ] && continue # 属主有 x ⇒ 可进入,放行 dir_bad=$((dir_bad + 1)) [ -z "$dir_worst" ] && dir_worst="$d" printf ' [FAIL] 目录不可进入:%s(%s,缺属主 x 位)\n' "$d" "$(stat -c '%A' "$d" 2>/dev/null)" >&2 done < <(git ls-files | awk -F/ 'NF>1 { for (i=1;i&2 printf ' 药方:chmod +x <目录>(**只加执行位,别动读位**)—— 目录的 `x` 是"能进入/能 stat 里面"。\n' >&2 printf ' 为什么单列一条:目录缺 `x` 时,**里面每个文件都读不到**,而 root 绕过它 ⇒\n' >&2 printf ' 维护者在自己身上永远看不到症状,只有换个身份(或别人来跑)才炸。\n' >&2 printf ' git 不存目录权限 ⇒ 这是**本地状态**,不跟提交走(新克隆不受影响)。\n' >&2 printf ' 这是**政策问题**(工作树应当能被其它身份使用),不是代码问题。\n' >&2 exit 1 fi if [ "$count" -gt 0 ]; then printf '\n 共 %d 个源文件权限不对(首个:%s)。\n' "$count" "$worst" >&2 printf ' 药方(**按类型分岔**):非可执行文件补齐到 644;git 记为 100755 的补齐到 755。\n' >&2 printf ' 原因:写文件的工具不理会 umask,而 `cp -a` 会把这个权限位带进生产快照,\n' >&2 printf ' 且 `cmp` / ① 内容判据都看不见它。\n' >&2 printf ' 这是**政策问题**(源文件应当可被构建/测试/部署以不同身份读),不是代码问题。\n' >&2 exit 1 fi printf ' 源文件权限政策:通过(已跟踪文件都够读;git 记为可执行的都保留了执行位)\n' exit 0