From 7ae507681131e8540aad4d66a236505e8becaa79 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 13 Oct 2026 05:35:32 +0000 Subject: [PATCH] [infra] - Record Ubuntu 26.04 bwrap AppArmor policy on a non-required runner Co-authored-by: Chris Grady --- .github/actionlint.yaml | 7 ++ .github/workflows/ci.yml | 175 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 .github/actionlint.yaml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 000000000..a28c44555 --- /dev/null +++ b/.github/actionlint.yaml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7f0b84af..ac8f487eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" + 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 }}