Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# actionlint 1.7.12 (pinned by ci.yml's workflow-lint job) stops its built-in
# GitHub-hosted runner list at ubuntu-24.04 and has no ignore-comment syntax.
# ubuntu-26.04 is a GitHub-hosted label for public repos (changelog 2026-09-17).
# This allowlist only makes that label lint-clean. It does not add a runner.
self-hosted-runner:
labels:
- ubuntu-26.04
175 changes: 175 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,181 @@ jobs:
$env:USERPROFILE = $OriginalUserProfile
}

# Non-required Ubuntu 26.04 probe for #1626. ubuntu-latest is still 24.04, so
# the required test leg's name and scripts stay put. continue-on-error keeps
# a red finding from failing the workflow. This job only records host policy
# and the same bwrap command the 24.04 leg runs; it does not write the
# userns sysctl.
ubuntu-26-04-bwrap-diagnose:
name: ubuntu-26-04-bwrap-diagnose
runs-on: ubuntu-26.04
continue-on-error: true
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install distro bubblewrap (26.04 has no noble pin)
shell: bash
run: |
set -u
sudo apt-get update
echo "--- apt-cache policy bubblewrap ---"
apt-cache policy bubblewrap
if apt-cache show 'bubblewrap=0.9.0-1ubuntu0.3' >/dev/null 2>&1; then
echo "noble_pin=present"
Comment on lines +546 to +547

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect an absent Noble version from command output

When the pinned Noble version is absent—the expected Ubuntu 26.04 case—apt-cache show 'bubblewrap=0.9.0-1ubuntu0.3' still exits successfully with no records; this occurs with apt 2.8.3, and the commit's hosted 26.04 evidence reports the same false positive. The Ubuntu apt-cache manual defines show as displaying matching package records, so its exit status is not an existence predicate. This branch therefore emits noble_pin=present despite the version being unavailable; test for non-empty exact-version output or inspect apt-cache policy/madison instead.

AGENTS.md reference: AGENTS.md:L49-L49

Useful? React with 👍 / 👎.

else
echo "noble_pin=absent bubblewrap=0.9.0-1ubuntu0.3"
fi
sudo apt-get install --yes --no-install-recommends bubblewrap apparmor-utils
echo "bwrap_path=$(command -v bwrap)"
bwrap --version
dpkg-query -W -f='installed_bubblewrap=${Version}\n' bubblewrap
dpkg-query -W -f='installed_${Package}=${Version}\n' apparmor apparmor-utils apparmor-profiles 2>/dev/null || true

- name: Record stock AppArmor, then the cyclaw-bwrap load
if: ${{ !cancelled() }}
shell: bash
run: |
set -u
set +e
set +o pipefail

section() {
printf '\n===== %s =====\n' "$1"
}

section "host"
awk -F= '/^(PRETTY_NAME|VERSION_ID|VERSION_CODENAME)=/{gsub(/"/, "", $2); print}' /etc/os-release
uname -a
apparmor_parser --version 2>&1 || true
aa-enabled 2>&1 || true
aa-status --version 2>&1 || true

section "sysctl"
for key in \
kernel.apparmor_restrict_unprivileged_userns \
kernel.apparmor_restrict_unprivileged_unconfined \
kernel.unprivileged_userns_clone \
user.max_user_namespaces
do
val="$(sysctl -n "$key" 2>/dev/null || echo missing)"
printf '%s=%s\n' "$key" "$val"
done

section "bwrap binary"
ls -l /usr/bin/bwrap || true
getcap /usr/bin/bwrap 2>/dev/null || true
bwrap --help 2>&1 | grep -E -n 'unshare-all|unshare-net|share-net|cap-drop' || true

section "apparmor paths"
sudo find /etc/apparmor.d /usr/share/apparmor \
\( -iname '*bwrap*' -o -iname '*userns*' \) -print 2>/dev/null | sort || true

section "profile texts"
while IFS= read -r path; do
[ -n "$path" ] || continue
printf '----- %s -----\n' "$path"
sudo ls -l "$path" || true
if [ -L "$path" ]; then
printf 'symlink_target=%s\n' "$(sudo readlink -f "$path" 2>/dev/null || true)"
fi
sudo awk 'NR<=260 {print}' "$path" || true
done < <(sudo find /etc/apparmor.d /usr/share/apparmor \
\( -iname '*bwrap*' -o -iname '*userns*' \) -print 2>/dev/null | sort)

loaded_profiles() {
section "loaded profiles $1"
sudo awk 'BEGIN{IGNORECASE=1} /bwrap|userns/ {print}' \
/sys/kernel/security/apparmor/profiles 2>/dev/null || true
sudo aa-status 2>&1 | awk 'BEGIN{IGNORECASE=1} /bwrap|userns|profiles are|in enforce|in complain/ {print}' || true
}

dump_denials() {
section "kernel denials $1"
sudo dmesg -T 2>/dev/null | awk 'BEGIN{IGNORECASE=1} /apparmor|bwrap/ {print}' | tail -n 160 || true
sudo journalctl -k --no-pager -n 300 2>/dev/null | awk 'BEGIN{IGNORECASE=1} /apparmor|bwrap/ {print}' | tail -n 160 || true
}

probe() {
name="$1"
shift
err="${RUNNER_TEMP}/bwrap-probe-${name}.err"
out="${RUNNER_TEMP}/bwrap-probe-${name}.out"
section "probe ${name}"
printf 'cmd:'
printf ' %q' "$@"
printf '\n'
"$@" >"$out" 2>"$err"
printf 'exit:%s\n' "$?"
printf 'stdout:\n'
cat "$out" || true
printf 'stderr:\n'
cat "$err" || true
}

loaded_profiles "before"
printf 'runner_attr='
tr '\0' ' ' < /proc/self/attr/current 2>/dev/null || true
printf '\n'
sudo dmesg -c >/dev/null 2>&1 || true

probe stock-full bwrap --die-with-parent --new-session --unshare-all --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true
probe stock-no-capdrop bwrap --die-with-parent --new-session --unshare-all \
--ro-bind / / --proc /proc --dev /dev /bin/true
probe stock-share-net bwrap --die-with-parent --new-session --unshare-all --share-net --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true
probe stock-user-net bwrap --die-with-parent --unshare-user --unshare-net --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true
probe stock-user-only bwrap --die-with-parent --unshare-user --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true

section "child label under stock"
bwrap --die-with-parent --new-session --unshare-all --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev \
/bin/sh -c 'printf "child_attr="; tr "\0" " " < /proc/self/attr/current; printf "\n"; if [ -r /proc/self/attr/apparmor/current ]; then printf "child_apparmor="; tr "\0" " " < /proc/self/attr/apparmor/current; printf "\n"; fi; grep -E "^Cap(Eff|Prm|Bnd|Inh):" /proc/self/status'
printf 'child_label_exit:%s\n' "$?"

section "unprofiled copy"
copy="${RUNNER_TEMP}/bwrap-unprofiled"
cp -a /usr/bin/bwrap "$copy"
chmod 755 "$copy"
probe unprofiled-copy "$copy" --die-with-parent --new-session --unshare-all --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true
if command -v unshare >/dev/null 2>&1; then
probe unshare-user-net unshare --user --map-root-user --net /bin/true
else
echo "unshare_missing=1"
fi
dump_denials "after-stock"

section "load cyclaw-bwrap"
sudo install -m 0644 deploy/apparmor/cyclaw-bwrap /etc/apparmor.d/cyclaw-bwrap
parser_out="${RUNNER_TEMP}/apparmor-parser.out"
sudo apparmor_parser -r /etc/apparmor.d/cyclaw-bwrap 2>&1 | tee "$parser_out"
printf 'apparmor_parser_exit:%s\n' "${PIPESTATUS[0]}"
printf 'parser_output:\n'
cat "$parser_out" || true
loaded_profiles "after-cyclaw-load"
sudo dmesg -c >/dev/null 2>&1 || true
probe after-cyclaw-full bwrap --die-with-parent --new-session --unshare-all --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true
dump_denials "after-cyclaw"

section "unload cyclaw-bwrap"
sudo apparmor_parser -R /etc/apparmor.d/cyclaw-bwrap
printf 'apparmor_parser_remove_exit:%s\n' "$?"
sudo rm -f /etc/apparmor.d/cyclaw-bwrap
loaded_profiles "after-unload"
probe after-unload-full bwrap --die-with-parent --new-session --unshare-all --cap-drop ALL \
--ro-bind / / --proc /proc --dev /dev /bin/true

echo "DIAGNOSIS_COMPLETE"

test:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down
Loading