Skip to content

fix(cosh-ng): [rpm] idempotent shells registration and fail-closed erase - #2599

Open
SunnyQjm wants to merge 2 commits into
mainfrom
fix/cosh-2543-rpm-shells-scriptlet
Open

fix(cosh-ng): [rpm] idempotent shells registration and fail-closed erase#2599
SunnyQjm wants to merge 2 commits into
mainfrom
fix/cosh-2543-rpm-shells-scriptlet

Conversation

@SunnyQjm

@SunnyQjm SunnyQjm commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the two RPM scriptlet gaps tracked in #2543 (ALinux4 default
login-shell readiness audit, PKG-007 / PKG-010):

  • PKG-007: the %post lua scriptlet appended the /etc/shells
    registration with a bare write. When the file lacked a trailing
    newline the entry glued onto the previous line
    (/bin/bash/usr/bin/cosh), and the membership probe never matched
    again, appending another entry on every reinstall.
  • PKG-010: the spec shipped no %preun, so rpm -e cosh-ng
    succeeded (rc=0) while passwd entries still referenced
    /usr/bin/cosh as their login shell, leaving users with a dangling
    shell and no warning.

Changes

  • cosh-ng.spec.in %post: normalize the read content before the
    membership probe (plain find of the exact newline-delimited entry)
    and prepend a separator newline when the existing content lacks a
    trailing one. Registration stays fail-open when /etc/shells
    cannot be opened (unchanged semantics).
  • cosh-ng.spec.in: add a shared %{cosh_replacement_ready}
    predicate and a %preun guard that blocks plain erase ($1 = 0)
    while users still reference /usr/bin/cosh, listing them on stderr.
    Enumeration is fail-closed (failing/empty getent, failing filter
    all abort). The predicate keeps the documented cosh-switch
    (yum swap) path working by allowing removal once /usr/bin/cosh is
    owned by copilot-shell (in a single swap transaction rpm installs
    the new provider before running the old package's %preun, so the
    predicate observes the new owner). A plain erase while users still
    reference the shell stays blocked by design. Upgrades ($1 >= 1)
    are never blocked.
  • tests/test-package-rpm.sh (new packaging harness, mirroring
    tests/test-package-raw.sh): structural lifecycle-section anchors,
    %post install/reinstall matrix through the real RPM Lua
    interpreter (missing file, empty, trailing/missing trailing newline,
    existing/duplicate registrations, substring non-registration),
    %post fail-open behavior, and the %preun guard matrix via
    fixture-backed bash runs (referencing user, no users, broken/empty
    enumeration, broken filter, replacement readiness, swap, upgrade).
  • scripts/run-test-gates.sh: new run_rpm_packaging gate
    (shellcheck + execution) wired into the fast and all gates,
    following the existing run_raw_packaging pattern.

Tests

  • tests/test-package-rpm.sh, run by run_rpm_packaging in
    scripts/run-test-gates.sh fast|all. The RPM-Lua-backed %post
    matrix skips (with an explicit SKIP notice) when the rpm lua
    interpreter is unavailable (macOS dev hosts); the %preun matrix
    and structural anchors always run.

Review follow-up: the scriptlet coverage initially landed in
tests/raw_cli/passthrough.rs; per review it moved to this dedicated
packaging harness, and passthrough.rs is untouched now.

Verification

Focused scope (run, green):

  • FAIL→PASS contrast on the unmodified vs fixed %post scriptlet via
    the real rpm embedded Lua interpreter in an alinux3 container
    (arm64): before — glued /bin/bash/usr/bin/cosh plus duplicate
    growth on reinstall; after — single clean entry, byte-idempotent.
  • %preun acceptance in the same container with a real
    useradd -s /usr/bin/cosh user: erase blocked (rc=1, user listed),
    upgrade allowed, clean erase allowed.
  • bash tests/test-package-rpm.sh green in the alinux3 container
    (full matrix incl. rpm lua) and on the macOS host (skip path for
    the %post matrix); shellcheck tests/test-package-rpm.sh clean.
  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, crates/cosh-shell/scripts/check-layout.sh,
    scripts/check-test-inventory.sh — all green after rebase onto
    latest main; additional cargo clippy -p cosh-shell --all-targets
    on rust 1.97.1 clean.

Excluded scope (not run):

  • Full RPM build + rpm -Uvh/rpm -e machine cycle. Scriptlet-level
    acceptance was chosen for this PR; the issue's archived double-arch
    full-package runs are the machine-level reference for the failing
    baseline.
  • cosh-ng cargo test suites (this PR changes no Rust code; covered
    by CI).

Evidence

Hosted on a fork orphan branch (pr-2599-assets), pinned by commit SHA:

Closes #2543

@github-actions github-actions Bot added the component:cosh-ng src/cosh-ng label Aug 17, 2026
@SunnyQjm
SunnyQjm marked this pull request as ready for review August 17, 2026 04:45
@SunnyQjm
SunnyQjm requested a review from KaiLongZhou as a code owner August 17, 2026 04:45
@SunnyQjm

SunnyQjm commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

PR number: #2599
head_sha: 0f968bc
reviewed_at: 2026-08-17T04:59:16Z

Findings

  • [P2] src/cosh-ng/crates/cosh-shell/tests/raw_cli/passthrough.rs:515 — 新增 4 个 packaging scriptlet 测试挂在 raw_cli target,但不驱动 raw CLI(只读 spec 文件、用 rpm --eval 和 bash fixture 回放 scriptlet),与既有 codex P1 评论一致,更适合独立 packaged/spec 测试 target。文件按 diff +343 行估算约从 512 增至 855 行(实测以 CI gate 为准);测试文件阈值以测试 SDD 为准,暂不阻断,但需作者确认归属或迁移计划。
  • [P2] src/cosh-ng/cosh-ng.spec.in:4%preun(约 :102-120)— cosh_replacement_ready 在 scriptlet 内递归调用 rpm -q(事务期 rpmdb 访问历来不被推荐),且宏转义(%%{NAME}\\n)只有真实 spec 构建/事务中才展开;Rust 测试用 bash fixture 完全绕开 rpm 宏展开,PR 也声明排除完整 rpm -e/swap 机器周期。转义或事务期行为不符预期时 fail-closed:普通擦除仍受保护,但文档承诺的 cosh-switch(yum swap)路径可能被静默阻断。建议合入前补一次真实 swap/erase 机器级验证。
  • [P3] passthrough.rspackaged_post_script/packaged_preun_script(约 :564、:578)— 用字符串切片提取 scriptlet,隐式依赖 spec 段落顺序(%preun 须在 %post -p <lua> 之前、%post 后须紧跟 %postun);段落重排会以 panic 而非可读断言失败,结构性锚点测试只钉存在性未钉顺序。

未发现 blocking package/module/public API 组织问题:未新增 crate、root src/*.rs implementation 文件、lib.rs pub mod/pub use、production pubcosh_shell:: self-crate path;未触碰 forbidden dependency direction;未向超 1000 行 production 文件追加功能。spec 修复与 cosh-shell 测试同 PR 属同一能力的必要配套,PR body 已说明关系与验证边界。

Open Questions

  • %post lua 幂等逻辑经矩阵推演正确,但真实 %post -p <lua> 事务上下文与 rpm --eval 是否完全等价,建议以 Linux release gate 实测确认。
  • scriptlet 测试在 /usr/bin/rpm 缺失时静默 skip,实际覆盖依赖 Linux CI 装有 rpm,请确认 gate 环境满足。
  • fail-closed 口径(空 getent 输出即阻断擦除)为 PR 明示的设计取舍,请确认已获 owner 认可。

Validation

  • 未运行(本轮不执行工具):check-layout.shinventory-public-api.sh、cargo 测试、容器内 RPM 验收。
  • 作者已报告:alinux3 容器 FAIL→PASS 对比、cargo test -p cosh-shell --test raw_cli packaged_ 4 passed、fmt/clippy/check-layout/test-inventory 全绿;CI Test cosh-ngTest cosh-ng fast checks 已 SUCCESS。

@qoderai qoderai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次审查范围内未发现需要修改的问题。


🤖 Generated by QoderView workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f968bcaff

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cosh-ng/crates/cosh-shell/tests/raw_cli/passthrough.rs Outdated
@KaiLongZhou

Copy link
Copy Markdown
Collaborator

Code Review: PR #2599 — fix(cosh-ng): [rpm] idempotent shells registration and fail-closed erase

总体结论:未发现阻塞问题,可以合并

PR 正确修复了 PKG-007 和 PKG-010 两个问题,方案设计合理,测试覆盖充分。以下是详细分析。


PKG-007 修复分析(%post Lua scriptlet)

修复方案正确,覆盖所有边界情况:

场景 旧行为 新行为
/etc/shells 无尾部换行 条目粘连(/bin/bash/usr/bin/cosh 自动补分隔换行 ✅
文件存在但为空 nil concatenation crash or '' 兜底 ✅
子串匹配(/usr/bin/cosh-backup 误判为已注册 true 参数 plain find + 尾部 \n 精确匹配 ✅
重复安装 重复追加 字节幂等 ✅

关键改进点:

  • f:seek('set') 解决 a+ 模式下读位置在文件末尾的问题
  • f:read('*all') or '' 修复了旧代码在空文件时 nil..'\n' 的 crash(旧 bug bonus fix)
  • normalized:find(nl..cosh..nl, 1, true) 使用 plain text 精确匹配,避免了 Lua pattern 的元字符问题

PKG-010 修复分析(%preun guard)

Fail-closed 设计正确:

  • getent 失败 → exit 1 ✅
  • getent 返回空 → exit 1 ✅
  • awk 过滤失败 → exit 1 ✅
  • 用户仍引用 cosh 且 replacement 未就绪 → exit 1 并列出用户 ✅
  • 升级($1 >= 1)→ 不阻塞 ✅
  • copilot-shell 已接管 /usr/bin/cosh → 允许卸载 ✅

%define cosh_replacement_ready 宏设计合理,将 [ -x ... ] 检查和 rpm -q 所有权验证组合为共享谓词。

测试分析

测试质量很高:

  1. packaged_spec_declares_login_shell_lifecycle_sections — 结构性回归保护,防止关键 section 被误删
  2. packaged_post_registers_shell_once_across_shells_variants — 7 个变体矩阵,通过真实 RPM Lua 解释器执行,覆盖文件缺失/空/有尾部换行/无尾部换行/已注册/重复注册/子串等场景
  3. packaged_post_skips_registration_when_shells_cannot_be_opened — 验证 fail-open 语义
  4. packaged_uninstall_blocks_while_passwd_references_cosh — 10 个场景矩阵,使用 bash fixture 模拟 getent/rpm/awk 的各种正常和异常行为

测试正确放在 raw_cli target 下,与 AGENTS.md 的分层规范一致。Linux-only 测试有 #[cfg(target_os = "linux")] 守卫,macOS 开发环境会正确跳过。


P2 观察(非阻塞,供参考)

P2: %postun Lua 脚本存在类似的尾部换行问题(已有问题,非本 PR 引入)

  • 文件与行号src/cosh-ng/cosh-ng.spec.in %postun section
  • 实际问题%postun 的 Lua 代码使用 io.lines() 读取 /etc/shells 并重写。如果文件无尾部换行,最后一行会被 io.lines() 跳过(Lua 行为),导致卸载后丢失该条目。同时 pairs(t) 不保证顺序。
  • 建议:这不是本 PR 引入的问题,但建议后续单独 issue 跟踪并修复,保持 %post%postun 的一致性。

P2: %preun awk pipeline 仅检测 awk 失败,未检测 printf 管道上游失败

  • 文件与行号src/cosh-ng/cosh-ng.spec.in %preun if ! users=$(printf '%s\n' "$passwd_entries" | awk ...)
  • 实际问题:管道退出码取最后一个命令(awk)。printf 几乎不会在管道中失败,但在极端 I/O 错误场景下,printf 失败 + awk 成功会导致 users 为空,guard 被绕过。
  • 建议:可考虑 set -o pipefail,但由于这是 RPM scriptlet 上下文(不一定使用 bash),实际风险极低,不阻塞。

P2: yum swap 场景下 %preun 的行为说明

  • 实际问题:当执行 yum swap cosh-ng copilot-shell 时,RPM 先移除 cosh-ng(%preun 运行),再安装 copilot-shell。此时 /usr/bin/cosh 仍归 cosh-ng 所有,cosh_replacement_ready 谓词为 false。如果有用户仍引用 cosh,swap 会被阻塞。
  • 评估:这是 正确的 fail-closed 行为 — 用户应先迁移 shell 再执行 swap。cosh_replacement_ready 谓词的价值在于支持 copilot-shell 已独立接管 /usr/bin/cosh 的场景。建议在 PR 描述或 commit message 中明确说明这一行为,避免后续维护者困惑。

验证范围

  • ✅ diff 逻辑正确性:手动验证所有边界情况
  • ✅ 测试分层合规(AGENTS.md raw_cli target)
  • %post Lua 语义正确性(seek、read、write、close)
  • %preun shell 逻辑正确性(getent → awk → predicate 链)
  • ⚠️ 未实际运行 cargo test(需要 Linux + RPM 环境)
  • ⚠️ 未验证完整 RPM build + install/erase 机器周期(PR 已在容器中完成)

@SunnyQjm
SunnyQjm force-pushed the fix/cosh-2543-rpm-shells-scriptlet branch from 0f968bc to 569f1f5 Compare August 17, 2026 05:12
@SunnyQjm

SunnyQjm commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

PR number: #2599
head_sha: 569f1f5
reviewed_at: 2026-08-17T05:29:54Z

Findings

  • [P2] src/cosh-ng/cosh-ng.spec.in:4cosh_replacement_readyrpm -q --qf '%%{NAME}}\\n'\\n 转义存在静默失效风险:构建期宏展开只折叠 %%,反斜杠原样保留,运行时 queryformat 拿到 %{NAME}\\n,按 rpm queryformat 规则 \\ 渲染为字面反斜杠,输出为 copilot-shell\n(无真实换行),awk $0 == "copilot-shell" 永不命中,谓词恒 false。方向是 fail-closed(普通擦除仍被保护),但 PR 声明保留的 cosh-switch(yum swap)路径会被静默阻断。bash fixture 中 stub 的 rpm 完全忽略 --qf 参数,无法暴露此问题;PR 也明确排除了真实构建/swap 机器周期。建议合入前用构建产物核验(rpm -qp --scripts 查看展开后 scriptlet 字节,或一次真实 swap/erase 验证),并据此决定 \n\\n
  • [P3] src/cosh-ng/tests/test-package-rpm.sh:92-96(另见 :21-24)— %preun 提取 awk '/^%preun$/{f=1;next} /^%post/{f=0} f' 隐式依赖 section 顺序(%preun 须在 %post 之前;中途插入非 %post* 开头的 section 会被吞进 PREUN);post_script/^%/{f=0} 要求 lua 体永无 % 开头的行。结构性锚点(grep -q '^%preun$' 等)只钉存在性未钉顺序,段落重排会以 bash 语法错误而非可读断言失败暴露。可接受,建议注释说明或锚点补顺序断言。
  • [P3] src/cosh-ng/scripts/run-test-gates.sh:122-131run_rpm_packaging 并入 fast/all 后,fast gate 新增 shellcheck 硬依赖(缺失即失败),与 run_raw_packaging 不做 shellcheck 的处理不对称。CI Test cosh-ng fast checks 已 SUCCESS 说明 CI 镜像满足,但开发机无 shellcheck 时 fast gate 会在测试前失败;请确认这是有意收紧,或对齐 raw gate 策略。

未发现 blocking package/module/public API 组织问题:本 PR 未触碰 crates/cosh-shell/,无新增 crate、workspace 反向依赖、root src/*.rs implementation、lib.rs pub mod/pub use、production pubcosh_shell:: self-crate path,不涉及大文件阈值台账;spec 修复、测试 harness 与 gate 接线属同一打包能力的必要配套。scriptlet 测试按 codex P1 意见从 raw_cli/passthrough.rs 迁出到 tests/test-package-rpm.sh 的处理方向正确,passthrough.rs 已不在 diff 中。%post lua 幂等逻辑经矩阵推演正确:a+seek('set') 修复读到空、plain find 精确匹配避免子串误判、缺尾部换行时补分隔符,install/reinstall 字节级幂等断言充分;%preun fail-closed 枚举(getent 失败/空、awk 失败、owner 异常、不可执行 replacement 均阻断)与升级不阻塞口径和 PR 声明一致;%preun 12 场景 fixture 矩阵与 stub 替换链路核对无误。PR body 过期描述问题在本轮已修复(Review follow-up 段落已说明迁移)。

Open Questions

  • %preun 在 rpm 事务上下文内递归执行 rpm -q 访问 rpmdb,历来不被推荐,不同 rpm 版本行为可能有差异;PR 明确排除了完整 rpm -e/swap 机器周期,建议由 release gate 实测兜底。
  • %post lua 矩阵在 /usr/bin/rpm 缺失时静默 skip,实际覆盖依赖 Linux CI 装有 rpm;当前 Test cosh-ng 仍 IN_PROGRESS,无法确认 CI 上跑的是完整矩阵还是 skip 路径。
  • %post fail-open(/etc/shells 打不开即跳过)与 %preungetent 输出即阻断,均为 PR 明示设计取舍,请确认 owner 已认可。
  • 前序人工评审提到 %postun 存在类似的尾部换行/io.lines() 丢失末行问题(非本 PR 引入);建议单独 issue 跟踪,保持 %post/%postun 一致性。

Validation

  • 未运行(本轮评审不执行工具):check-layout.shcheck-test-inventory.sh、shellcheck、容器内 RPM 验收、真实 spec 构建。
  • 作者已报告:alinux3 容器内 %post FAIL→PASS 字节幂等对比、真实 useradd 驱动 %preun 验收、tests/test-package-rpm.sh 在容器(完整 lua 矩阵)与 macOS(skip 路径)均绿、shellcheck/fmt/clippy/check-layout/check-test-inventory 全绿。
  • CI 信号:Build cosh-ng release SUCCESS、Test cosh-ng fast checks SUCCESS、Test cosh-ng IN_PROGRESS。

@SunnyQjm
SunnyQjm force-pushed the fix/cosh-2543-rpm-shells-scriptlet branch from 569f1f5 to 3df039e Compare August 17, 2026 05:33
@SunnyQjm

Copy link
Copy Markdown
Collaborator Author

第二轮机器评审与 @KaiLongZhou 评审的逐条回应(head 569f1f5a3df039ef,改动已 fixup 进两个源提交):

1. [P2] cosh_replacement_ready\\n 转义正确性 — 已按建议做低成本实证核验(alinux3 容器,真实 rpm):

  • 宏展开:rpm --define 'cosh_replacement_ready <spec原文>' --eval '%{cosh_replacement_ready}' → 展开为 --qf '%{NAME}\n' -f …\\n 折叠为 \n%%{NAME} 折叠为 %{NAME})✅
  • 运行期:rpm -q --qf '%{NAME}\n' bash | od -cb a s h \n(真实换行);反之 qf 收到 \\n 才会输出字面 \ n
  • 结论:spec 现行 \\n 是正确写法;若按疑虑改成单 \n,宏展开后只剩 n 才是坏的。
  • 加固:tests/test-package-rpm.sh 新增断言——在 rpm 可用环境用 rpm --define 展开谓词并断言其含 --qf '%{NAME}\n' -f,把该宏展开合同钉进 CI,弥补 bash fixture 无法覆盖转义的问题。

2. [P3] scriptlet 提取的段落顺序依赖 — 采纳:结构锚定新增 awk 顺序断言(%preun < %post < %postun),段落重排会以可读断言失败暴露,并补注释说明切片依赖。

3. [P3] fast gate 的 shellcheck 硬依赖不对称 — 与事实不符:既有 run_raw_packaging 有完全相同的守卫(if ! command -v shellcheck …; return 1)并 shellcheck 四个文件,run_rpm_packaging 是同构复制;fast gate 今天就硬依赖 shellcheck,本 PR 未推高环境要求。

4. [P3] PR body 与 diff 漂移 — body 在迁移当时已更新(Changes/Tests 已指向 tests/test-package-rpm.sh);本轮再清理了 Excluded scope 里一处过期的 packaged_ filter 措辞。

5. @KaiLongZhou [P2] %postun io.lines 尾换行问题 — 认同,属存量缺陷(本 PR 未触碰 %postun),建议另建 issue 跟踪,与 %post 口径对齐修复。

6. @KaiLongZhou [P2] printf | awk 管道上游失败 — 认同风险极低的评估:getent 输出此时已成功捕获在内存变量中,printf 重发失败的形态(EPIPE/ENOMEM)在 awk 全量读取下几乎不可达;set -o pipefail 在 scriptlet 的 /bin/sh 上下文不可移植,维持现状。

7. @KaiLongZhou [P2] yum swap 语义说明 — 已在 PR body 补充:单事务 swap 中 rpm 先安装新提供者再跑旧包 %preun,谓词届时观察到 copilot-shell 已接管 → 放行;普通 erase(无替代)在仍有用户引用时按设计阻断,需先迁移用户。

8. Open question:rpm 事务内递归 rpm -q — 已知设计点(只读查询,配合 2>/dev/null 与 fail-closed 兜底:查询失败 → 谓词 false → 阻断而非放行);完整 rpm -e/swap 机器周期按本 PR 声明的边界留给 release gate 实测兜底。

验证:更新后的 harness 在 alinux3 容器(完整路径,含新宏展开断言)与 macOS host(skip 路径)均绿;shellcheck 干净;fmt/clippy/check-layout/check-test-inventory 重跑全绿。

@SunnyQjm

SunnyQjm commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

PR number: #2599
head_sha: 3df039e
reviewed_at: 2026-08-18T11:16:27Z

Findings

  • [P1] src/cosh-ng/tests/test-package-rpm.sh:115PREUN="${PREUN_RAW//'%{cosh_replacement_ready}'/$PREDICATE}" 的替换值未加引号且含 &&:Bash 5.2 默认启用 patsub_replacement,替换值中未引用的 & 会被展开为匹配文本,谓词被腐蚀为 [ -x ... ] '%{cosh_replacement_ready}%{cosh_replacement_ready}' rpm ...[ 参数过多退出 2 → 谓词恒 false → "atomic provider swap" 用例(期望 0)稳定失败。静态推演与 @kongche-jbw 在 ALinux4/Bash 5.2.15 的实测轨迹一致(关闭 patsub_replacement 即通过)。该 harness 已接入 fast/all 门禁(scripts/run-test-gates.sh:122-131),会稳定阻断 Bash 5.2 环境;CI 全绿不能覆盖此差异(CI runner 的 bash 未触发该默认特性,macOS 自带 bash 3.2 亦无此选项)。修复方向:替换值加引号(.../"$PREDICATE"},引号内 & 不参与展开)、先转义(${PREDICATE//&/\\&})、或改用 awk/sed 拼接;同文件 :116 的 ${PREUN//'%{_bindir}'/$STUB} 建议一并加固,并补 Bash 5.2 环境的回归覆盖。

  • [P3] src/cosh-ng/tests/test-package-rpm.sh:29-32 — 前轮已提、当前 head 未闭环:post_scriptsed "s|/etc/shells|$SHELLS|g"s|%{_bindir}/cosh|$COSH|g" 是静默 no-op 型替换;若 spec 重构使两个字面量消失,sed 不再生效,run_post 会经真实 rpm lua 改写运行环境真实 /etc/shells。建议锚点段补 grep -q '/etc/shells' "$SPEC"grep -q '%{_bindir}/cosh' "$SPEC",或对提取结果断言替换已生效。非阻断。

未发现 blocking package/module/public API 组织问题:本 PR 未触碰 crates/cosh-shell/,无新增 crate、workspace 反向依赖、root src/*.rs implementation、lib.rs pub mod/pub use、production pubcosh_shell:: self-crate path,不涉及大文件台账;spec 修复、packaging harness 与 gate 接线属同一 RPM 打包能力的必要配套,边界清晰。%post lua 幂等逻辑(seek('set')、plain find 精确匹配、缺尾换行补分隔符)与 %preun fail-closed 链路经矩阵推演自洽,前轮结论不变。

Open Questions

  • 完整 RPM 构建 + rpm -e/swap 机器周期按 PR 声明留给 release gate;rpm 事务内递归 rpm -q 为已知设计点(只读查询 + fail-closed 兜底)。
  • (存量,非本 PR 引入,评审双方已确认)%postunio.lines() 对无尾换行 /etc/shells 丢末行且 pairs 不保序,建议另立 issue 与 %post 口径对齐。
  • PKG-010 方向(保留 %preun 守卫)已经 owner 侧确认按现状合入;cosh-login 包化与注册路径收敛另立 issue 跟踪。

Validation

  • 本轮静态评审,未执行工具:harness、shellcheck、check-layout、容器 RPM 验收均未运行。
  • 作者已报告:alinux3 容器完整路径与 macOS skip 路径均绿,fmt/clippy/check-layout/check-test-inventory 全绿;CI 当前 head 全部 SUCCESS。
  • 新信号:ALinux4/Bash 5.2.15 实测 harness 默认运行稳定失败(见 P1),修复后需在该环境复测并保留回归覆盖。

@SunnyQjm

Copy link
Copy Markdown
Collaborator Author

回应第三轮评审的 open question「CI 是否真正执行了 %post 完整矩阵(非 skip 路径)」——已按建议查 gate 日志实证:

  • Job Test cosh-ng fast checks(run 31998344718 / job 95294099932,head 3df039ef)日志 2026-08-17T05:38:57Zcosh-ng rpm scriptlet tests passed,且全日志无 SKIP: rpm lua interpreter unavailable 行 → rpm lua 守卫通过,%post 七形态 × install/reinstall 矩阵、fail-open 用例与宏展开断言均在 CI 真实执行。
  • 该 runner 镜像自带可用的 rpm(含内嵌 lua);若未来镜像变更导致缺失,harness 会显式打印 SKIP 而非静默,届时可在 gate 日志直接观测。

存量问题 %postun io.lines 丢无尾换行末行(评审双方已确认非本 PR 引入)建议合入后另建 issue 跟踪。

@KaiLongZhou

Copy link
Copy Markdown
Collaborator

@SunnyQjm 这里卸载cosh软件包后,导致默认登录shell残留的问题当前已经不存在了,/etc/passwd里面注册为/usr/bin/cosh-login,如果/usr/bin/cosh不存在,则default到bash。
另外一个维度,rpm包里面并没有注册的逻辑,卸载时候与注册逻辑有些耦合。

@SunnyQjm

Copy link
Copy Markdown
Collaborator Author

@KaiLongZhou 感谢指出。已在 AgenticOS 实例(ALinux4,cosh-ng 0.16.1)上实证核查,两个维度分别回应:

1. cosh-login 机制确认属实,但它在包外,且与 %preun 守卫正交互补

实例事实:

  • /usr/bin/cosh-login 存在(bash wrapper:cosh 可执行则 exec -a "-cosh" /usr/bin/cosh,缺失则提示并 fallback exec -a "-bash" /bin/bash),root 的 passwd 注册的确是它 ✅
  • rpm -qf /usr/bin/cosh-loginnot owned by any package:它由镜像/部署层放置,/etc/shells 里的 cosh-login 行也是包外写入——包自身无法保证这道防线存在(非 AgenticOS 镜像的裸 dnf install cosh-ng 场景没有它)。
  • 同时 cosh-ng 自己的 %post 仍把 /usr/bin/cosh 注册进 /etc/shells——包宣告了它是合法登录 shell,管理员 chsh -s /usr/bin/cosh 是被允许的形态;该形态下 erase 残留风险仍真实存在(issue [P1][cosh-ng] RPM spec:%post /etc/shells 尾换行不幂等 + erase 无 %preun fail-closed(PKG-007/PKG-010) #2543 的双架构实证即此形态)。

正交性实证(该实例真实 passwd):%preun 守卫只匹配 $7 == "/usr/bin/cosh",对 cosh-login 注册形态判定结果为放行users=[])——即发行版推荐形态的卸载完全不受影响;守卫只保护"直接注册 /usr/bin/cosh"这个包自己宣告合法的形态。两道防线互补:cosh-login 兜底推荐形态,%preun 兜底直接注册形态。

2. 关于"卸载守卫与注册逻辑耦合"

守卫的判定依据只有包自身拥有的两个事实:包 Provides 的 %{_bindir}/cosh 路径、包自己在 %post 做的 /etc/shells 注册——不依赖任何包外的用户注册逻辑;swap 场景另有 cosh_replacement_ready 逃生口。若产品方向是"登录形态统一收敛到 cosh-login",更一致的做法是把 cosh-login 纳入包管理、并把 %post/etc/shells 注册路径一并调整——那是另一个 issue 的系统性改动;在此之前,只要 %post 还注册 /usr/bin/cosh,%preun 守卫就是它的配套下线保护。

基于以上事实请您裁决:a) 按现状合入(两防线正交,零误伤实证如上);b) 认为应随 cosh-login 方向系统化调整——那我把本 PR 缩窄到 PKG-007(%post 幂等),PKG-010 连同 cosh-login 包化、注册路径调整另立 issue 讨论。

@SunnyQjm

Copy link
Copy Markdown
Collaborator Author

经与 owner 侧确认,本 PR 决定按 a) 现状合入(保留 %preun 守卫)。补充"若撤守卫、只依赖 cosh-login"的缺口场景矩阵,作为该裁决的完整依据:

# 场景 后果
1 dnf install cosh-ng(非 AgenticOS 镜像) cosh-login 不存在(实证:not owned by any package,由镜像/部署层放置);管理员 chsh -s /usr/bin/cosh(%post 已宣告合法)后 rpm -e 成功 → 登录 shell 悬空。即 issue #2543 双架构实证形态
2 AgenticOS 镜像上直接 chsh -s /usr/bin/cosh cosh-login 防线只覆盖"passwd 注册 cosh-login"的用户,直接注册形态不在保护面内
3 cosh-login 被删除/镜像变体未部署 包外文件无 RPM 完整性保护(rpm -V 不覆盖、重装不恢复),防线可静默消失
4 %postun 清理面 只删 /etc/shells 的 /usr/bin/cosh 行,passwd 残留引用永久存在

反向代价为零:cosh-login 注册形态下守卫判定 users=[] 放行(已在真实实例 passwd 实证);swap 有 cosh_replacement_ready 逃生口;升级($1>=1)不进守卫。两道防线保护面不重叠、互不干扰。

"登录形态统一收敛到 cosh-login + cosh-login 包化 + %post 注册路径调整"作为系统性方向,建议另立 issue 跟踪;在其落地前撤守卫会打开上表 1/2/3 的保护空窗。

@KaiLongZhou 如无进一步异议,烦请 approve 放行合入(CI 三轮全绿、评审意见均已闭环)。

@kongche-jbw kongche-jbw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在最新 head 3df039ef 上复现到 1 个阻塞问题,因此请求修改。

验证环境为 ALinux4 arm64、RPM 4.18.2、Bash 5.2.15。默认运行新增 packaging harness 会稳定失败;仅关闭 Bash 5.2 默认启用的 patsub_replacement 后,同一 head 才能全部通过。当前 CI 全绿并不能覆盖这一 Bash 行为差异。

Comment thread src/cosh-ng/tests/test-package-rpm.sh
The %post lua scriptlet appended the login-shell registration with a
bare write, so an /etc/shells file without a trailing newline glued
the entry to the previous line (/bin/bash/usr/bin/cosh) and the
membership probe never matched again, growing the file on every
reinstall.

Normalize the read content before probing: plain-find the exact
newline-delimited entry, and prepend a separator newline when the
existing content lacks a trailing one. Registration stays fail-open
when /etc/shells cannot be opened.

Cover the install/reinstall matrix (missing file, empty, trailing
newline, missing trailing newline, existing registrations, substring
non-registration) plus the fail-open path with the real RPM Lua
interpreter in a dedicated packaging harness
(tests/test-package-rpm.sh) wired into the fast/all test gates,
skipping the rpm-backed matrix when rpm lua is unavailable.

Fixes: 4ce37f9 ("chore(cosh-ng): add cargo config, nightly rpm build, and rust dev standard")

Assisted-by: Qoder:1.24.2
Signed-off-by: SunnyQjm <mfeng@linux.alibaba.com>
The spec shipped no %preun, so rpm -e cosh-ng succeeded while passwd
entries still referenced %{_bindir}/cosh as their login shell,
leaving those users with a dangling shell and no warning.

Add a %preun guard that blocks plain erase ($1 = 0) and lists the
affected users on stderr. Enumeration stays fail-closed: a failing or
empty getent and a failing passwd filter all abort the erase. The
shared cosh_replacement_ready predicate keeps the documented
cosh-switch swap path working by allowing removal once
%{_bindir}/cosh is owned by copilot-shell. Upgrades ($1 >= 1) are
never blocked.

Extend the packaging harness with fixture-backed bash runs of the
extracted scriptlet across the guard matrix (referencing user, no
users, broken and empty enumeration, broken filter, replacement
readiness, swap, upgrade), pin the predicate's macro expansion to a
real-newline queryformat via rpm --define, and anchor the spec
lifecycle sections and their extraction order structurally. The
predicate splice disables patsub_replacement (a bash 5.2 default that
expands unquoted & in the replacement to the matched pattern) and
asserts the predicate lands verbatim in the generated scriptlet.

Fixes: 4ce37f9 ("chore(cosh-ng): add cargo config, nightly rpm build, and rust dev standard")

Assisted-by: Qoder:1.24.2
Signed-off-by: SunnyQjm <mfeng@linux.alibaba.com>
@SunnyQjm
SunnyQjm force-pushed the fix/cosh-2543-rpm-shells-scriptlet branch from 3df039e to 67373f6 Compare August 18, 2026 11:38
@SunnyQjm
SunnyQjm requested a review from kongche-jbw August 18, 2026 11:39
@SunnyQjm

SunnyQjm commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

PR number: #2599
head_sha: 67373f6
reviewed_at: 2026-08-18T11:52:44Z

Findings

  • [P3] src/cosh-ng/tests/test-package-rpm.sh:29-32 — 前轮已提、当前 head 仍未闭环:post_scriptsed "s|/etc/shells|$SHELLS|g"s|%{_bindir}/cosh|$COSH|g" 是静默 no-op 型替换;若 spec 后续重构使这两个字面量消失(例如改用 %{_sysconfdir}),sed 不再生效,run_post 会经真实 rpm lua 改写运行环境的真实 /etc/shells。建议在结构锚点段补 grep -q '/etc/shells' "$SPEC"grep -q '%{_bindir}/cosh' "$SPEC",或对提取结果断言替换已生效。非阻断。

未发现 blocking package/module/public API 组织问题:本 PR 未触碰 crates/cosh-shell/,无新增 crate、workspace 反向依赖、root src/*.rs implementation、lib.rs pub mod/pub use、production pubcosh_shell:: self-crate path,不涉及大文件台账;spec 修复、packaging harness 与 gate 接线属同一 RPM 打包能力的必要配套,边界清晰。

复核要点:

  • kongche-jbw 的 P1(Bash 5.2 patsub_replacement 腐蚀拼接谓词)在当前 head 已闭环:shopt -u patsub_replacement 2>/dev/null || : 置于全部 ${...//.../...} 拼接之前(旧 bash 静默兼容),并新增两层环境无关断言(%preun 不残留未展开宏、谓词逐字拼入 PREUN),shopt 行被移除会以可读断言失败暴露。修复方向正确,待复核者在 ALinux4/Bash 5.2 环境复测确认。
  • %post lua 幂等逻辑经矩阵推演自洽:a+seek('set') 修复读到空、plain find 精确匹配避免子串误判、缺尾换行补分隔符,install/reinstall 字节级幂等;cosh 独占首行、无尾换行等边界均覆盖。
  • %preun fail-closed 链路与 PR 声明一致:getent 失败/空、awk 失败、replacement 不可执行或 owner 异常均阻断,升级($1 >= 1)不进守卫;12 场景 fixture 矩阵逐一核对无误。
  • run_rpm_packaging 与既有 run_raw_packaging 同构(shellcheck + 执行),fast/all 接线一致。

Open Questions

  • 完整 RPM 构建 + rpm -e/swap 机器周期按 PR 声明留给 release gate;rpm 事务内递归 rpm -q 为已知设计点(只读查询 + fail-closed 兜底)。
  • (存量,非本 PR 引入,评审双方已确认)%postunio.lines() 对无尾换行 /etc/shells 丢末行且 pairs 不保序,建议另立 issue 与 %post 口径对齐。
  • 输入快照中 Test cosh-ng 仍 IN_PROGRESS,合入前需确认其转绿;Bash 5.2 环境复测尚待 kongche-jbw 确认后解除 CHANGES_REQUESTED。

Validation

  • 本轮静态评审,未执行工具:harness、shellcheck、check-layout、容器 RPM 验收均未运行。
  • 作者已报告:bash 5.2.15(anolisos 23.5 容器,FAIL→PASS 对照)、bash 4.4.20(alinux3)、bash 3.2(macOS)三环境 harness 全绿,shellcheck/fmt/clippy/check-layout/check-test-inventory 全绿;CI fast checks 当前 head SUCCESS。

@kongche-jbw kongche-jbw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review baseline: 33ed384643c6...67373f6c420f

[P2] 为新 %preun 声明运行期工具依赖

src/cosh-ng/cosh-ng.spec.in:104 起的守卫依赖 getentawk,但 spec 没有
对应的 Requires(preun)。在缺少任一工具的最小 RPM 系统中,cosh-ng 仍可安装,
随后即使没有用户引用 cosh,普通卸载也会 fail-closed 退出 1,必须手工补装工具
才能移除包。Possible direction: 为实际可执行文件提供者添加 qualified dependency,
或移除外部依赖;再用构建后的 RPM 核对 requires,并覆盖最小 installroot 的卸载。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1][cosh-ng] RPM spec:%post /etc/shells 尾换行不幂等 + erase 无 %preun fail-closed(PKG-007/PKG-010)

3 participants