diff --git a/ci/napcat-desktop-cn/README.md b/ci/napcat-desktop-cn/README.md index 10e75ab..be9d36d 100644 --- a/ci/napcat-desktop-cn/README.md +++ b/ci/napcat-desktop-cn/README.md @@ -58,4 +58,6 @@ docker exec $name sh /ci/napcat-desktop-cn/verify.sh docker rm -f $name ``` +`verify.sh` checks long-lived QQ/NapCat/Xvfb process `/proc//mountinfo` after the entrypoint guard has had a bounded convergence window. If a target process still exposes host paths after `MOUNTINFO_GUARD_TIMEOUT_SECONDS`, verification exits with 78 and prints the target PID, `comm`, `cmdline`, and first leaked mountinfo line. + Record the final image digest in `QQBOT_NAPCAT_IMAGE`, and keep the matching `fork-artifact.json` with the release evidence. For local Windows rehearsal, placeholder `$upstreamReleaseTag`, `$upstreamReleaseCommit`, and `$jenkinsBuildUrl` values are acceptable only if the image is not promoted. diff --git a/ci/napcat-desktop-cn/verify.sh b/ci/napcat-desktop-cn/verify.sh index 66eaf88..d99f8eb 100644 --- a/ci/napcat-desktop-cn/verify.sh +++ b/ci/napcat-desktop-cn/verify.sh @@ -62,14 +62,53 @@ grep -q '/sys/class/dmi/id/bios_version' /app/entrypoint.sh grep -q '/etc/hosts' /app/entrypoint.sh MOUNTINFO_HOST_LEAK_PATTERN='overlay|/vol1/docker|docker-init|/docker/containers|napcat-instances|btrfs|/dev/mapper/trim' -assert_no_mountinfo_host_leak() { +MOUNTINFO_GUARD_TIMEOUT_SECONDS="${MOUNTINFO_GUARD_TIMEOUT_SECONDS:-12}" + +mountinfo_has_host_leak() { kt_mountinfo="$1" if [ -r "$kt_mountinfo" ] && grep -E "$MOUNTINFO_HOST_LEAK_PATTERN" "$kt_mountinfo" >/dev/null 2>&1; then + return 0 + fi + return 1 +} + +assert_no_mountinfo_host_leak() { + kt_mountinfo="$1" + if mountinfo_has_host_leak "$kt_mountinfo"; then echo "NapCat desktop-cn verify failed: mountinfo-host-leak:$kt_mountinfo" >&2 exit 78 fi } +wait_for_mountinfo_guard_converged() { + kt_mountinfo_pid="$1" + kt_mountinfo_guard_deadline="$MOUNTINFO_GUARD_TIMEOUT_SECONDS" + kt_mountinfo_guard_elapsed=0 + while [ "$kt_mountinfo_guard_elapsed" -le "$kt_mountinfo_guard_deadline" ]; do + if [ ! -d "/proc/$kt_mountinfo_pid" ]; then + return 0 + fi + if ! mountinfo_has_host_leak "/proc/$kt_mountinfo_pid/mountinfo"; then + return 0 + fi + sleep 1 + kt_mountinfo_guard_elapsed=$((kt_mountinfo_guard_elapsed + 1)) + done + return 1 +} + +assert_target_mountinfo_guard_converged() { + kt_mountinfo_pid="$1" + if wait_for_mountinfo_guard_converged "$kt_mountinfo_pid"; then + return 0 + fi + kt_mountinfo_comm="$(cat "/proc/$kt_mountinfo_pid/comm" 2>/dev/null || true)" + kt_mountinfo_cmdline="$(tr '\000' ' ' < "/proc/$kt_mountinfo_pid/cmdline" 2>/dev/null || true)" + echo "NapCat desktop-cn verify failed: mountinfo-host-leak:/proc/$kt_mountinfo_pid/mountinfo comm=$kt_mountinfo_comm cmdline=$kt_mountinfo_cmdline" >&2 + grep -E "$MOUNTINFO_HOST_LEAK_PATTERN" "/proc/$kt_mountinfo_pid/mountinfo" 2>/dev/null | head -n 1 >&2 || true + exit 78 +} + kt_is_mountinfo_target() { kt_target_comm="$1" kt_target_cmdline="$2" @@ -111,6 +150,6 @@ for kt_mountinfo_pid_dir in /proc/[0-9]*; do kt_mountinfo_comm="$(cat "$kt_mountinfo_pid_dir/comm" 2>/dev/null || true)" kt_mountinfo_cmdline="$(tr '\000' ' ' < "$kt_mountinfo_pid_dir/cmdline" 2>/dev/null || true)" if kt_is_mountinfo_target "$kt_mountinfo_comm" "$kt_mountinfo_cmdline"; then - assert_no_mountinfo_host_leak "/proc/$kt_mountinfo_pid/mountinfo" + assert_target_mountinfo_guard_converged "$kt_mountinfo_pid" fi done diff --git a/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts b/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts index f17007c..d7a209b 100644 --- a/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts +++ b/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts @@ -63,6 +63,21 @@ describe('NapCat Chinese Desktop Runtime image assets', () => { expect(verify).toContain('Asia/Shanghai'); }); + it('waits for the mountinfo guard to converge before failing target process leaks', () => { + const verify = readSource('ci/napcat-desktop-cn/verify.sh'); + + expect(verify).toContain('MOUNTINFO_GUARD_TIMEOUT_SECONDS'); + expect(verify).toContain('wait_for_mountinfo_guard_converged'); + expect(verify).toContain('kt_mountinfo_guard_deadline='); + expect(verify).toContain('sleep 1'); + expect(verify).toContain( + 'assert_target_mountinfo_guard_converged "$kt_mountinfo_pid"', + ); + expect(verify).not.toContain( + 'assert_no_mountinfo_host_leak "/proc/$kt_mountinfo_pid/mountinfo"', + ); + }); + it('patches entrypoint device profile probes that QQCore opens at runtime', () => { const dockerfile = readSource('ci/napcat-desktop-cn/Dockerfile'); const entrypointPatch = readSource(