From bc08b91425baacc90e4f24abb46d58ecdd856276 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:20:28 -0700 Subject: [PATCH 1/7] mesa-m3: allow checked system Mesa after a file opt-out Keep qualified native /usr Mesa usable when the legacy mesa-m3 hook is still installed. Admit only J613/T8122 and J516S/T6030 after display, render-node, journal, user-setup and previous-login checks, followed by a bounded no-draw EGL device/context and implementation-path check. Keep kernel recovery flags and all failed admissions on the existing fallback. Preserve active legacy profiles and their attempt markers. Add host refusal controls and document the remaining packaged login test. Generated-by: LLM Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> --- tools/aurora-ci/host-tests.json | 32 +++ tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 15 +- .../recipes/mesa-m3/mesa-m3-session-env | 226 +++++++++++++----- .../recipes/mesa-m3/mesa-m3-system-check.c | 221 +++++++++++++++++ .../recipes/mesa-m3/mesa-m3-user-setup | 21 +- .../recipes/mesa-m3/tests/SYSTEM-HANDOFF.md | 46 ++++ .../recipes/mesa-m3/tests/test-session-env.sh | 151 ++++++++++++ .../recipes/mesa-m3/tests/test-system-board.c | 32 +++ .../mesa-m3/tests/test-system-provider.py | 79 ++++++ 9 files changed, 761 insertions(+), 62 deletions(-) create mode 100644 tools/aurora-sep/recipes/mesa-m3/mesa-m3-system-check.c create mode 100644 tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md create mode 100644 tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c create mode 100644 tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index 9e211b3193377d..1310ec2bb60146 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -17,6 +17,8 @@ "device-tree-compiler", "flex", "libarchive-tools", + "libegl-dev", + "libgles-dev", "libnotify-bin", "lld", "llvm", @@ -1819,6 +1821,36 @@ "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh" ] }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", + "group": "tools", + "setup": [ + [ + "cc", + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + "-pedantic", + "-O2", + "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", + "-ldl", + "-o", + "{output}" + ] + ], + "argv": [ + "{output}" + ] + }, { "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh", "group": "tools", diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index e9ea066a21b461..a120de243b1e1f 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -10,7 +10,7 @@ # No profile selection substitutes another Mac's firmware or calibration. pkgname=mesa-m3 pkgver=26.1.4.m3.2 -pkgrel=5 +pkgrel=6 _commit=7ff89a4da387bd561b78efd5f09c98ca8ae18538 pkgdesc="Mesa in /opt/mesa-m3 for the M3 Pro (G15S) and M3 (G15G) GPUs; used by uwsm sessions on T6030, and on T8122 when opted in" arch=('aarch64') @@ -65,7 +65,8 @@ source=("mesa-m3-src-${_commit:0:11}.tar.zst" 72-mesa-m3-render-node.rules mesa-m3-session-confirm mesa-m3-session-confirm.service - mesa-m3-wlcheck.c) + mesa-m3-wlcheck.c + mesa-m3-system-check.c) sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '535e26266e1d173bfd2f73e63be07eccf32eb9cb329ea8955e32a4fa45278ce8' '9be098fc0a1ec38632678ece7d6d7b743d25df4a1af59ce3d17ba3a4eed34db7' @@ -84,18 +85,19 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '10abda78df16e650a77443f50cc46a15e418c42d4d9680c6a61616b232c9bdef' '6c98fab6f9d70449bd7f5258c34eac773836908e060ce5bef86dc5b0ce595e61' '826016665cd0424a614b67add921c0c79c3c698a4d104ea5712518449b75f27c' - '2b2eb627459f73aa596812503c48fc64331e925015bb7121e1888d18cac65b5e' + '444fd73d505c3ffa7252bb44845457deac2550a411464bbcec075e14f21b2063' '6144af8b2e0f04ea615e8c4149e69c45ac49e630c46b2a58f0d1095d977798b2' '530948fc1a124de21c030a497e2ef5351742ba4ad4653484fe30fd1697ad7141' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - '7e7914877c5f1ccc85b985b071c838271608c78f858489d69b7a3ba64c4f51f0' + 'a446e2b3fa075a1ef239439539119eb12472b372a84ecb9411c4e65ca3112999' '8f19ebea2b8e669a764d7c4b40ecbc11e8234acfc3a2bfe7910e22de0b64a49d' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' '7eaae7de828e15208fccc84db2b910f38bb1288176419f7af7f4675b5c542ce4' - 'c888e9dce99e8379aa21e9c5895cbe1b7e879a9319bca144193eddc66f9d4051') + 'c888e9dce99e8379aa21e9c5895cbe1b7e879a9319bca144193eddc66f9d4051' + 'e80947130044c2dc3671c24c7bff4f523dcc033f7e96f71e334d89a663e2742e') # The qualified M3 Pro build's generated kernel files and libclc. build() stops # when one of ours differs. libagx_shaders.c is compared through kernel-digest.py: @@ -296,6 +298,8 @@ EOF ${CC:-cc} ${CFLAGS} -Wall -I. -o mesa-m3-probe mesa-m3-probe.c ${LDFLAGS} \ -lEGL -lGLESv2 -lvulkan -lwayland-client -lwayland-egl ${CC:-cc} ${CFLAGS} -Wall -o mesa-m3-loadcheck mesa-m3-loadcheck.c ${LDFLAGS} + ${CC:-cc} ${CFLAGS} -std=c11 -Wall -Wextra -Werror -I"$_src/include" \ + -o mesa-m3-system-check mesa-m3-system-check.c ${LDFLAGS} -ldl ${CC:-cc} ${CFLAGS} -Wall -I"$_src/include" $(pkg-config --cflags libdrm) \ -o mesa-m3-abi-check mesa-m3-abi-check.c ${LDFLAGS} $(pkg-config --libs libdrm) ${CC:-cc} ${CFLAGS} -Wall -DMESA_M3_GL_ONLY -I. -o mesa-m3-native-probe mesa-m3-probe.c ${LDFLAGS} \ @@ -341,6 +345,7 @@ package_mesa-m3() { install -Dm755 gpu-heat "${pkgdir}${_prefix}/bin/gpu-heat" install -Dm755 mesa-m3-probe "${pkgdir}${_prefix}/bin/mesa-m3-probe" install -Dm755 mesa-m3-loadcheck "${pkgdir}${_prefix}/libexec/mesa-m3-loadcheck" + install -Dm755 mesa-m3-system-check "${pkgdir}${_prefix}/libexec/mesa-m3-system-check" install -Dm644 mesa-m3-session-env "${pkgdir}${_prefix}/libexec/mesa-m3-session-env" install -Dm755 mesa-m3-user-setup "${pkgdir}${_prefix}/libexec/mesa-m3-user-setup" install -Dm644 user-setup.list "${pkgdir}${_prefix}/share/mesa-m3/user-setup.list" diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env index 5abbf89f8567f8..3308a963e8f1ba 100644 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env @@ -68,6 +68,12 @@ # user-setup it exports nothing: the session's own Mesa setup, which it uses, # decides. The state file says which (fallback=software|none). # +# A file opt-out (/etc/mesa-m3/disable or the user's mesa-m3/disable) on a +# J613 (T8122) or a J516S (T6030) is the exception: when the same node, +# display, kernel-log, user-setup and same-boot attempt checks pass and +# mesa-m3-system-check admits /usr, the session keeps system Mesa +# (fallback=none, no new attempt). mesa_m3=off stays on the software fallback. +# # Each login writes /run/user//mesa-m3-session.state (atomically, 0644, # key=value lines, first line schema=aurora.mesa-m3-session/2) and logs one # journal line (tag mesa-m3) with the same decision, reason and login identity, @@ -133,16 +139,9 @@ mesa_m3_gate() { echo "no-gpu render node ${_mm3_node##*/} is not the $_mm3_name GPU (apple,agx-$_mm3_soc)"; return 1 fi - _mm3_dcp= - for _mm3_c in "$_mm3_root"/sys/class/drm/card*; do - case "${_mm3_c##*/}" in *-*) continue ;; esac - if tr '\0' '\n' 2>/dev/null < "$_mm3_c/device/of_node/compatible" | grep -qx "apple,$_mm3_soc-display-subsystem"; then - _mm3_dcp=${_mm3_c##*/} - fi - done - if [ -z "$_mm3_dcp" ]; then + _mm3_dcp=$(_mm3_native_display "$_mm3_root" "$_mm3_soc") || { echo "no-display no native display card (apple,$_mm3_soc-display-subsystem)"; return 1 - fi + } _mm3_dev="$_mm3_root/dev/dri/${_mm3_node##*/}" if [ ! -r "$_mm3_dev" ] || [ ! -w "$_mm3_dev" ]; then @@ -250,53 +249,19 @@ mesa_m3_gate() { echo "load-failed mesa-m3-loadcheck exit $_mm3_rc: ${_mm3_out:-no output}"; return 1 fi - # The kernel log of this boot must be readable (a user without journal - # access would otherwise see no faults) and must show no fault of the M3 - # GPU or of the display. journalctl exits 1 both for "no match" and for - # errors, so the log is read whole and searched with grep: a failed read, - # an empty log or a grep error refuses (fail closed). The failure state of - # the M3 GPU is sticky for the boot, so a fault means software rendering - # until reboot. - _mm3_out=$(journalctl -k -b -q --no-pager -o cat 2>/dev/null) - _mm3_rc=$? - if [ "$_mm3_rc" -ne 0 ] || [ -z "$_mm3_out" ]; then - echo "log-unreadable this user cannot read this boot's kernel log (only root and the wheel, adm and systemd-journal groups can), so GPU faults cannot be ruled out; to allow GPU acceleration, add the user to the adm group and log in again" - return 1 - fi - _mm3_faults=$(printf '%s\n' "$_mm3_out" | grep -E \ - -e 'M3 scheduler: execution failed|M3 G15[SG]: firmware crashed|M3 firmware error|M3 bank [0-9]|native DCP flip failed|DCP firmware crashed' \ - -e 'M3 G15G verdict: (firmware-running-check-failed|initdata-rejected|cap-violated|job-failed-before-wait|job-faulted|job-ran-completion-missed|job-accepted-never-dispatched|job-timed-out)') - _mm3_rc=$? - if [ "$_mm3_rc" -eq 0 ]; then - echo "gpu-fault the M3 GPU or the display failed this boot, so GPU acceleration is off until reboot: $(printf '%s\n' "$_mm3_faults" | tail -n 1 | tr -d '\r')" - return 1 - elif [ "$_mm3_rc" -ne 1 ]; then - echo "log-unreadable this boot's kernel log could not be searched (grep exit $_mm3_rc)"; return 1 - fi + # The kernel log of this boot must be readable and must show no fault of + # the M3 GPU or of the display. The same fail-closed read is what a file + # opt-out uses before it will leave the system Mesa in place. + _mm3_klog=$(_mm3_kernel_log) || { echo "$_mm3_klog"; return 1; } # A login that chose /opt/mesa-m3 earlier this boot must have a success # marker bound to it (same boot_id, session_id and hook_run_id), written by # mesa-m3-session-confirm.service only after its compositor was alive and # answering on its Wayland socket after the settle time. Otherwise this # login stays off it: no login loop when the compositor cannot run on it. - _mm3_att="$_mm3_root${XDG_STATE_HOME:-$HOME/.local/state}/mesa-m3/attempt" - _mm3_ok="$_mm3_root${XDG_STATE_HOME:-$HOME/.local/state}/mesa-m3/session-ok" - if [ -f "$_mm3_att" ]; then - _mm3_boot= - [ -r "$_mm3_root/proc/sys/kernel/random/boot_id" ] && - IFS= read -r _mm3_boot < "$_mm3_root/proc/sys/kernel/random/boot_id" - if [ "$(sed -n 's/^boot_id=//p' "$_mm3_att" 2>/dev/null | head -n 1)" = "$_mm3_boot" ]; then - _mm3_v=$(sed -n 's/^hook_run_id=//p' "$_mm3_att" 2>/dev/null | head -n 1) - _mm3_val=$(sed -n 's/^session_id=//p' "$_mm3_att" 2>/dev/null | head -n 1) - if [ -z "$_mm3_v" ] || [ ! -f "$_mm3_ok" ] || - [ "$(head -n 1 "$_mm3_ok" 2>/dev/null)" != schema=aurora.mesa-m3-session-ok/1 ] || - [ "$(sed -n 's/^boot_id=//p' "$_mm3_ok" 2>/dev/null | head -n 1)" != "$_mm3_boot" ] || - [ "$(sed -n 's/^hook_run_id=//p' "$_mm3_ok" 2>/dev/null | head -n 1)" != "$_mm3_v" ] || - [ "$(sed -n 's/^session_id=//p' "$_mm3_ok" 2>/dev/null | head -n 1)" != "$_mm3_val" ]; then - echo "previous-failed the last login with /opt/mesa-m3 this boot (hook_run_id ${_mm3_v:-unknown}) has no success marker: its compositor was not seen alive and answering, so this login renders in software; to try the GPU again: rm $_mm3_att, then log out and in (or reboot)" - return 1 - fi - fi + if ! _mm3_legacy_attempt_clear "$_mm3_root"; then + echo "previous-failed the last login with /opt/mesa-m3 this boot (hook_run_id ${_mm3_v:-unknown}) has no success marker: its compositor was not seen alive and answering, so this login renders in software; to try the GPU again: rm $_mm3_att, then log out and in (or reboot)" + return 1 fi if [ "${_mm3_ign:-0}" -gt 0 ]; then @@ -354,6 +319,137 @@ _mm3_node_open() { return 1 } +# _mm3_native_display ROOT SOC: the card name of this SoC's native display, +# or nothing. A connector name (card2-eDP-1) is not a card. The last match +# wins, as the gate's own walk did. +_mm3_native_display() { + _mm3_dcp= + for _mm3_c in "$1"/sys/class/drm/card*; do + case "${_mm3_c##*/}" in *-*) continue ;; esac + if tr '\0' '\n' 2>/dev/null < "$_mm3_c/device/of_node/compatible" | grep -qx "apple,$2-display-subsystem"; then + _mm3_dcp=${_mm3_c##*/} + fi + done + [ -n "$_mm3_dcp" ] || return 1 + printf '%s\n' "$_mm3_dcp" +} + +# Fail closed. Prints one "log-unreadable ..." or "gpu-fault ..." line and +# returns 1 unless this boot's kernel log was read and contains none of the +# M3 GPU or display faults. journalctl exits 1 both for "no match" and for +# errors, so the log is read whole and searched with grep. A failed read, an +# empty log or a grep error refuses. The GPU failure state is sticky for the +# boot. +_mm3_kernel_log() { + _mm3_out=$(journalctl -k -b -q --no-pager -o cat 2>/dev/null) + _mm3_rc=$? + if [ "$_mm3_rc" -ne 0 ] || [ -z "$_mm3_out" ]; then + echo "log-unreadable this user cannot read this boot's kernel log (only root and the wheel, adm and systemd-journal groups can), so GPU faults cannot be ruled out; to allow GPU acceleration, add the user to the adm group and log in again" + return 1 + fi + _mm3_faults=$(printf '%s\n' "$_mm3_out" | grep -E \ + -e 'M3 scheduler: execution failed|M3 G15[SG]: firmware crashed|M3 firmware error|M3 bank [0-9]|native DCP flip failed|DCP firmware crashed' \ + -e 'M3 G15G verdict: (firmware-running-check-failed|initdata-rejected|cap-violated|job-failed-before-wait|job-faulted|job-ran-completion-missed|job-accepted-never-dispatched|job-timed-out)') + _mm3_rc=$? + if [ "$_mm3_rc" -eq 0 ]; then + echo "gpu-fault the M3 GPU or the display failed this boot, so GPU acceleration is off until reboot: $(printf '%s\n' "$_mm3_faults" | tail -n 1 | tr -d '\r')" + return 1 + elif [ "$_mm3_rc" -ne 1 ]; then + echo "log-unreadable this boot's kernel log could not be searched (grep exit $_mm3_rc)"; return 1 + fi + return 0 +} + +# Return 0 when this boot has no unmatched /opt/mesa-m3 attempt. Return 1 +# when an attempt for this boot_id has no success marker bound to its +# hook_run_id and session_id. Never creates, deletes or rewrites either file. +# On failure, _mm3_att and _mm3_v are the attempt path and its hook_run_id. +_mm3_legacy_attempt_clear() { + _mm3_att="$1${XDG_STATE_HOME:-$HOME/.local/state}/mesa-m3/attempt" + _mm3_ok="$1${XDG_STATE_HOME:-$HOME/.local/state}/mesa-m3/session-ok" + [ -f "$_mm3_att" ] || return 0 + _mm3_boot= + [ -r "$1/proc/sys/kernel/random/boot_id" ] && + IFS= read -r _mm3_boot < "$1/proc/sys/kernel/random/boot_id" + [ "$(sed -n 's/^boot_id=//p' "$_mm3_att" 2>/dev/null | head -n 1)" = "$_mm3_boot" ] || return 0 + _mm3_v=$(sed -n 's/^hook_run_id=//p' "$_mm3_att" 2>/dev/null | head -n 1) + _mm3_val=$(sed -n 's/^session_id=//p' "$_mm3_att" 2>/dev/null | head -n 1) + if [ -z "$_mm3_v" ] || [ ! -f "$_mm3_ok" ] || + [ "$(head -n 1 "$_mm3_ok" 2>/dev/null)" != schema=aurora.mesa-m3-session-ok/1 ] || + [ "$(sed -n 's/^boot_id=//p' "$_mm3_ok" 2>/dev/null | head -n 1)" != "$_mm3_boot" ] || + [ "$(sed -n 's/^hook_run_id=//p' "$_mm3_ok" 2>/dev/null | head -n 1)" != "$_mm3_v" ] || + [ "$(sed -n 's/^session_id=//p' "$_mm3_ok" 2>/dev/null | head -n 1)" != "$_mm3_val" ]; then + return 1 + fi + return 0 +} + +# File opt-out on J613/T8122 or J516S/T6030 may keep the system Mesa. Prints +# one detail line and returns 0 only when every check below passes. A helper +# exit of 0 is not enough on its own. Does not write an attempt or touch a +# marker. mesa_m3=off is not a file opt-out. +_mm3_system_admit() { + case ${XDG_CURRENT_DESKTOP:-} in Hyprland|hyprland) ;; *) return 1 ;; esac + _mm3_cmdline=$(cat "$1/proc/cmdline" 2>/dev/null) || return 1 + case " $_mm3_cmdline " in + *" mesa_m3=off "*|*" nomodeset "*) return 1 ;; + esac + if [ ! -e "$1/etc/mesa-m3/disable" ] && + [ ! -e "$1${XDG_CONFIG_HOME:-$HOME/.config}/mesa-m3/disable" ]; then + return 1 + fi + _mm3_dt=$(tr '\0' '\n' 2>/dev/null < "$1/proc/device-tree/compatible") + _mm3_soc=$(_mm3_soc_of "$1") + _mm3_chip= + _mm3_identity=$(printf '%s\n' "$_mm3_dt" | grep -E '^apple,j') + _mm3_socs=$(printf '%s\n' "$_mm3_dt" | grep -E '^apple,t') + case "$_mm3_soc:$_mm3_identity:$_mm3_socs" in + t8122:apple,j613:apple,t8122) _mm3_chip=t8122 ;; + t6030:apple,j516s:apple,t6030) _mm3_chip=t6030 ;; + *) return 1 ;; + esac + # Exactly one render node, and it is this SoC's asahi GPU, and this user + # can open it. An any-node match is not enough. + _mm3_nodes=0 + _mm3_node= + for _mm3_n in "$1"/sys/class/drm/renderD*; do + [ -e "$_mm3_n" ] || continue + _mm3_nodes=$((_mm3_nodes + 1)) + _mm3_node=$_mm3_n + done + [ "$_mm3_nodes" -eq 1 ] || return 1 + _mm3_drv=$(basename "$(readlink -f "$_mm3_node/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$_mm3_drv" = asahi ] || return 1 + tr '\0' '\n' 2>/dev/null < "$_mm3_node/device/of_node/compatible" | grep -qx "apple,agx-$_mm3_chip" || return 1 + _mm3_dev="$1/dev/dri/${_mm3_node##*/}" + [ -r "$_mm3_dev" ] && [ -w "$_mm3_dev" ] || return 1 + _mm3_dcp=$(_mm3_native_display "$1" "$_mm3_chip") || return 1 + _mm3_legacy_attempt_clear "$1" || return 1 + _mm3_klog=$(_mm3_kernel_log) || return 1 + if [ -n "$1" ]; then + _mm3_out=$("$1$_mm3_p/libexec/mesa-m3-user-setup" --home "$HOME" \ + --config-home "${XDG_CONFIG_HOME:-$HOME/.config}" --env --system-provider \ + --session-desktops "${XDG_CURRENT_DESKTOP:-}" --root "$1" 2>&1) + else + _mm3_out=$("$_mm3_p/libexec/mesa-m3-user-setup" --home "$HOME" \ + --config-home "${XDG_CONFIG_HOME:-$HOME/.config}" --env --system-provider \ + --session-desktops "${XDG_CURRENT_DESKTOP:-}" 2>&1) + fi + [ "$?" -eq 0 ] || return 1 + if [ -n "$1" ]; then + _mm3_helper=$1/opt/mesa-m3/libexec/mesa-m3-system-check + else + _mm3_helper=/opt/mesa-m3/libexec/mesa-m3-system-check + fi + [ -x "$_mm3_helper" ] || return 1 + # The helper sees a clean environment: a leaked GL preload or path must + # not decide /usr's renderer. Five seconds, then kill. + timeout -k 2 5 env -i PATH=/usr/bin:/bin "$_mm3_helper" \ + "/dev/dri/${_mm3_node##*/}" "$_mm3_chip" >/dev/null 2>&1 + [ "$?" -eq 0 ] || return 1 + printf '%s\n' "system Mesa on /usr admits /dev/dri/${_mm3_node##*/} ($_mm3_chip); /opt/mesa-m3 stays inactive" +} + # _mm3_state ROOT DECISION REASON DETAIL FALLBACK: write the state file and the journal # line. A value holding a newline or carriage return is refused. _mm3_state() { @@ -455,23 +551,43 @@ mesa_m3_session_env() { _mm3_attempt "${1:-}" else # Never the system Mesa on an M3 render node it can open (Hyprland - # would stop at start), except where this session's own setup chose. + # would stop at start), except where this session's own setup chose, + # or a file opt-out whose system-Mesa handoff passed. _mm3_fb=none - if [ "${_mm3_decision%% *}" != user-setup ] && _mm3_node_open "${1:-}"; then + _mm3_detail=${_mm3_decision#* } + _mm3_admitted=0 + if [ "${_mm3_decision%% *}" = opt-out ]; then + _mm3_sys=$(_mm3_system_admit "${1:-}") + if [ "$?" -eq 0 ]; then + _mm3_admitted=1 + _mm3_detail="$_mm3_detail; $_mm3_sys" + # Only the pair this hook itself exports. Anything else in the + # environment, including a user's own LIBGL_ALWAYS_SOFTWARE + # without this marker, stays. + if [ "${LIBGL_ALWAYS_SOFTWARE:-}" = 1 ] && [ "${MESA_M3_FALLBACK:-}" = software ]; then + unset LIBGL_ALWAYS_SOFTWARE + unset MESA_M3_FALLBACK + fi + fi + fi + if [ "$_mm3_admitted" -eq 0 ] && [ "${_mm3_decision%% *}" != user-setup ] && _mm3_node_open "${1:-}"; then export LIBGL_ALWAYS_SOFTWARE=1 export MESA_M3_FALLBACK=software _mm3_fb=software fi - _mm3_state "${1:-}" inactive "${_mm3_decision%% *}" "${_mm3_decision#* }" "$_mm3_fb" + _mm3_state "${1:-}" inactive "${_mm3_decision%% *}" "$_mm3_detail" "$_mm3_fb" fi } if [ -z "${MESA_M3_SESSION_ENV_NO_RUN:-}" ]; then mesa_m3_session_env - unset -f mesa_m3_gate mesa_m3_session_env _mm3_init _mm3_state _mm3_node_open _mm3_attempt _mm3_soc_of _mm3_25g83_board + unset -f mesa_m3_gate mesa_m3_session_env _mm3_init _mm3_state _mm3_node_open _mm3_attempt \ + _mm3_soc_of _mm3_25g83_board _mm3_native_display _mm3_kernel_log \ + _mm3_legacy_attempt_clear _mm3_system_admit fi unset _mm3_root _mm3_p _mm3_nodes _mm3_node _mm3_n _mm3_drv _mm3_dcp _mm3_c _mm3_dev _mm3_f _mm3_v \ _mm3_val _mm3_ifs _mm3_d _mm3_out _mm3_rc _mm3_decision _mm3_on \ _mm3_so _mm3_uid _mm3_sid _mm3_inv _mm3_ver _mm3_boot _mm3_cr _mm3_nl \ _mm3_body _mm3_kv _mm3_dir _mm3_tmp _mm3_fb _mm3_run _mm3_att _mm3_ok _mm3_faults _mm3_ign _mm3_igd \ - _mm3_soc _mm3_name _mm3_dt _mm3_base _mm3_profile _mm3_requested _mm3_files _mm3_gate_ifs _mm3_board _mm3_note + _mm3_soc _mm3_name _mm3_dt _mm3_base _mm3_profile _mm3_requested _mm3_files _mm3_gate_ifs _mm3_board _mm3_note \ + _mm3_klog _mm3_sys _mm3_admitted _mm3_detail _mm3_helper _mm3_chip _mm3_identity _mm3_socs _mm3_cmdline diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-system-check.c b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-system-check.c new file mode 100644 index 00000000000000..8f6177750196d5 --- /dev/null +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-system-check.c @@ -0,0 +1,221 @@ +/* SPDX-License-Identifier: MIT */ +/* Check system Mesa admission for one M3 render node, without drawing. + * This helper never selects /opt/mesa-m3 or enables an experimental GPU. + * The session hook invokes it with an empty environment and a timeout. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool +board_compat_matches(const char *bytes, size_t length, const char *soc) +{ + const char *board = !strcmp(soc, "t8122") ? "apple,j613" : + !strcmp(soc, "t6030") ? "apple,j516s" : NULL; + if (!board || !length || bytes[length - 1] != '\0') + return false; + unsigned boards = 0, socs = 0; + size_t offset = 0; + while (offset < length) { + size_t remaining = length - offset; + size_t size = strnlen(bytes + offset, remaining); + if (!size || size == remaining) + return false; + const char *value = bytes + offset; + if (!strncmp(value, "apple,j", 7)) { + if (strcmp(value, board)) + return false; + boards++; + } else if (!strncmp(value, "apple,t", 7)) { + if (strncmp(value + 6, soc, strlen(soc) + 1)) + return false; + socs++; + } + offset += size + 1; + } + return boards == 1 && socs == 1; +} + +static bool +qualified_board(const char *soc) +{ + FILE *file = fopen("/proc/device-tree/compatible", "rb"); + if (!file) + return false; + char bytes[4096]; + size_t length = fread(bytes, 1, sizeof(bytes), file); + bool complete = !ferror(file) && length < sizeof(bytes); + fclose(file); + return complete && board_compat_matches(bytes, length, soc); +} + +static bool +symbol(void *library, const char *name, void *destination, size_t size) +{ + void *address = dlsym(library, name); + if (!address || size != sizeof(address)) + return false; + memcpy(destination, &address, size); + return true; +} + +/* Check what actually loaded, rather than trusting a version or a package name. + * A role supplied through a legacy prefix, or a deleted implementation, fails. + */ +static bool +system_implementation(void) +{ + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) + return false; + char line[8192]; + bool egl = false, gallium = false, invalid = false; + while (fgets(line, sizeof(line), maps)) { + char *path = strchr(line, '/'); + if (!path) + continue; + bool is_egl = strstr(path, "libEGL_mesa.so") != NULL; + bool is_gallium = strstr(path, "libgallium-") != NULL; + bool is_asahi = strstr(path, "/dri/asahi_dri.so") != NULL; + if (!is_egl && !is_gallium && !is_asahi) + continue; + if (strncmp(path, "/usr/lib/", 9) || strstr(path, " (deleted)")) + invalid = true; + egl |= is_egl; + gallium |= is_gallium || is_asahi; + } + bool complete = !ferror(maps); + fclose(maps); + return complete && !invalid && egl && gallium; +} + +int +main(int argc, char **argv) +{ + if (argc != 3) { + fprintf(stderr, "usage: mesa-m3-system-check /dev/dri/renderD t8122|t6030\n"); + return 2; + } + const char *expected = !strcmp(argv[2], "t8122") ? "Apple M3 (G15G C0)" : + !strcmp(argv[2], "t6030") ? "Apple M3 Pro (G15S B1)" : NULL; + if (!expected) + return 2; + const char *node_prefix = "/dev/dri/renderD"; + size_t prefix_length = strlen(node_prefix); + if (strncmp(argv[1], node_prefix, prefix_length) || + !argv[1][prefix_length] || + strspn(argv[1] + prefix_length, "0123456789") != strlen(argv[1] + prefix_length)) + return 2; + if (!qualified_board(argv[2])) + return 1; + struct stat st; + if (lstat(argv[1], &st) || !S_ISCHR(st.st_mode)) + return 1; + + /* env -i at exec is also necessary: clearenv cannot undo LD_PRELOAD that + * the dynamic linker processed before main. Do not inherit opt-in flags. + */ + if (clearenv() || + setenv("__EGL_VENDOR_LIBRARY_FILENAMES", + "/usr/share/glvnd/egl_vendor.d/50_mesa.json", 1) || + setenv("LIBGL_DRIVERS_PATH", "/usr/lib/dri", 1) || + setenv("MESA_SHADER_CACHE_DISABLE", "true", 1)) + return 1; + alarm(8); + void *egl_library = dlopen("/usr/lib/libEGL.so.1", RTLD_NOW | RTLD_LOCAL); + void *gles_library = dlopen("/usr/lib/libGLESv2.so.2", RTLD_NOW | RTLD_LOCAL); + if (!egl_library || !gles_library) + return 1; + + PFNEGLGETPROCADDRESSPROC get_proc = NULL; + PFNEGLINITIALIZEPROC initialize = NULL; + PFNEGLBINDAPIPROC bind_api = NULL; + PFNEGLCHOOSECONFIGPROC choose_config = NULL; + PFNEGLCREATECONTEXTPROC create_context = NULL; + PFNEGLMAKECURRENTPROC make_current = NULL; + PFNEGLDESTROYCONTEXTPROC destroy_context = NULL; + PFNEGLTERMINATEPROC terminate = NULL; + const GLubyte *(*get_string)(GLenum) = NULL; +#define LOAD(library, function, name) \ + if (!symbol(library, name, &function, sizeof(function))) return 1 + LOAD(egl_library, get_proc, "eglGetProcAddress"); + LOAD(egl_library, initialize, "eglInitialize"); + LOAD(egl_library, bind_api, "eglBindAPI"); + LOAD(egl_library, choose_config, "eglChooseConfig"); + LOAD(egl_library, create_context, "eglCreateContext"); + LOAD(egl_library, make_current, "eglMakeCurrent"); + LOAD(egl_library, destroy_context, "eglDestroyContext"); + LOAD(egl_library, terminate, "eglTerminate"); + LOAD(gles_library, get_string, "glGetString"); +#undef LOAD + + PFNEGLQUERYDEVICESEXTPROC query_devices = NULL; + PFNEGLQUERYDEVICESTRINGEXTPROC device_string = NULL; + PFNEGLGETPLATFORMDISPLAYEXTPROC platform_display = NULL; +#define EXT(function, name) do { \ + __eglMustCastToProperFunctionPointerType address = get_proc(name); \ + if (!address || sizeof(address) != sizeof(function)) return 1; \ + memcpy(&function, &address, sizeof(function)); \ +} while (0) + EXT(query_devices, "eglQueryDevicesEXT"); + EXT(device_string, "eglQueryDeviceStringEXT"); + EXT(platform_display, "eglGetPlatformDisplayEXT"); +#undef EXT + EGLint count = 0; + EGLDeviceEXT devices[16]; + if (!query_devices(0, NULL, &count) || count < 1 || count > 16 || + !query_devices(count, devices, &count) || count < 1 || count > 16) + return 1; + EGLDeviceEXT selected = EGL_NO_DEVICE_EXT; + for (EGLint i = 0; i < count; ++i) { + const char *path = device_string(devices[i], EGL_DRM_RENDER_NODE_FILE_EXT); + if (path && !strcmp(path, argv[1])) { + if (selected != EGL_NO_DEVICE_EXT) + return 1; + selected = devices[i]; + } + } + if (selected == EGL_NO_DEVICE_EXT) + return 1; + + EGLDisplay display = platform_display(EGL_PLATFORM_DEVICE_EXT, selected, NULL); + if (display == EGL_NO_DISPLAY || !initialize(display, NULL, NULL)) + return 1; + EGLContext context = EGL_NO_CONTEXT; + bool accepted = false; + EGLConfig config; + EGLint matches = 0; + const EGLint attributes[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE, + }; + const EGLint context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + if (!bind_api(EGL_OPENGL_ES_API) || + !choose_config(display, attributes, &config, 1, &matches) || matches != 1) + goto done; + context = create_context(display, config, EGL_NO_CONTEXT, context_attributes); + if (context == EGL_NO_CONTEXT || + !make_current(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)) + goto done; + const char *renderer = (const char *)get_string(GL_RENDERER); + accepted = renderer && !strcmp(renderer, expected) && + system_implementation(); + if (accepted) + printf("system-mesa=available renderer=%s\n", renderer); +done: + make_current(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (context != EGL_NO_CONTEXT) + destroy_context(display, context); + terminate(display); + dlclose(gles_library); + dlclose(egl_library); + return accepted ? 0 : 1; +} diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index a581853b1404d0..80a459b2c2551b 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -4,6 +4,7 @@ mesa-m3-user-setup --home HOME [--config-home DIR] [--env | --environ FILE] [--session-desktops NAMES] [--list FILE] [--root DIR] + [--system-provider] The one definition of "the user's own M3 Mesa setup", used by the mesa-m3 session hook at every login and by the installer. What counts is listed in @@ -20,6 +21,8 @@ see the comments there. as in XDG_CURRENT_DESKTOP (uwsm exports them before it reads any environment file); empty or absent: unknown --root DIR prefix every path with DIR (tests) + --system-provider also refuse legacy /opt selectors and session flags when + checking whether the real desktop will use system Mesa Nothing is sourced or evaluated. Files are read as text and split into words the way a POSIX shell would (quotes, backslashes, comments, ";"), with no @@ -79,10 +82,14 @@ def fail(msg): def parse_args(argv): opts = {'home': None, 'config': None, 'env': False, 'environ': None, 'list': DEFAULT_LIST, 'root': '', - 'desktops': None} + 'desktops': None, 'system_provider': False} i = 0 while i < len(argv): a = argv[i] + if a == '--system-provider': + opts['system_provider'] = True + i += 1 + continue if a == '--env': opts['env'] = True i += 1 @@ -248,6 +255,14 @@ class Finder: for k, i in items: if k in ('variable', 'flag', 'prefix', 'ldpath'): self.kind[i] = k + if opts['system_provider']: + # These may be left over from an active legacy session. The + # admission helper's clean environment cannot prove that the + # real desktop will use /usr while these remain in its setup. + for name in ('LD_PRELOAD', 'LD_AUDIT', 'MESA_M3_SESSION', 'MESA_M3_PROFILE', + 'ASAHI_M3_EXPERIMENTAL', 'AGX_MESA_DEBUG', 'HK_DEBUG_LOG', + 'VK_LOADER_LAYERS_DISABLE', 'QSG_RHI_BACKEND'): + self.kind[name] = 'flag' def real(self, p): return self.o['root'] + p @@ -297,7 +312,9 @@ class Finder: def ours(self, path): p = os.path.normpath(path) if path.startswith('/') else path - return p == PREFIX or p.startswith(PREFIX + '/') + # A system-Mesa handoff must also detect the old prefix. The normal + # legacy gate deliberately accepts it and keeps its original behavior. + return not self.o['system_provider'] and (p == PREFIX or p.startswith(PREFIX + '/')) def holds_mesa(self, d): r = self.real(d) diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md new file mode 100644 index 00000000000000..dbe150d387dc08 --- /dev/null +++ b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md @@ -0,0 +1,46 @@ +# Legacy mesa-m3 to system Mesa + +With native Mesa installed in `/usr`, the legacy file opt-out still forced +`LIBGL_ALWAYS_SOFTWARE=1`. This prevented an accelerated login even when the +system driver worked. The legacy package owns this hook, so the native Mesa +package must not overwrite it or add an unconditional late unset. + +This change allows a file opt-out on J613/T8122 and J516S/T6030 to leave system +Mesa selected. It requires one accessible Asahi render node, the matching +native display, a readable healthy kernel journal, no unmatched same-boot +legacy login attempt, and no conflicting environment or later user settings. +A bounded helper then opens the exact EGL device using only `/usr` libraries +and verifies its native renderer and loaded implementation paths. It creates +a context but makes no draw, swap or compute calls. + +A failed check keeps the existing software fallback. `mesa_m3=off` and +`nomodeset` remain recovery choices. Other boards, including J615, remain +outside this system handoff. Explicit legacy profile selection is unchanged. +No opt-in flags, attempt records or success markers are added or removed. +Only the exact software-fallback pair previously exported by this hook can +be cleared. The state remains `inactive`, `reason=opt-out`, `fallback=none` +when system Mesa admission succeeds. + +Run the host checks from this directory (a writable TMPDIR may be needed): + +```sh +sh test-session-env.sh +bash test-session-env.sh +sh test-user-setup.sh +python3 test-system-provider.py +cc -std=c11 -Wall -Wextra -Werror -pedantic -O2 test-system-board.c -ldl -o /tmp/test-system-board +/tmp/test-system-board +``` + +Validation on J516S with kernel `7.2.11-mixtape-77648eb77168-g15` and the native +Mesa source `97a65d845f39418aecc693dcaf9dcb14a05849f6`: the helper ran as the +desktop user without opt-in variables and returned +`Apple M3 Pro (G15S B1)`, exit 0. No new kernel log messages, package changes +or reboot. This is device/context admission, not a packaged login test, +rendering qualification, CTS result or support claim for other boards. + +Before an image cutover, build the updated legacy package and verify a fresh +login alongside the actual pinned Omacom Mesa packages. Existing legacy users +and unqualified profiles must retain their recovery path; clean system-Mesa +images can omit the legacy package. The Mesa source and package PRs remain +separate from this hook update. diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh b/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh index 44f25289927084..6c7b2eb659b6fc 100755 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh @@ -665,5 +665,156 @@ echo j613-25g83-hal200 > "$J516S/etc/mesa-m3/t8122-profile" expect "$J516S" profile-mismatch J613 rm "$J516S/etc/mesa-m3/t8122-profile" + +# System Mesa handoff: opt-out must not strand a qualified native /usr +# installation in software rendering. Every failed admission retains recovery. +# The helper is a stub here; its real board/renderer/provenance tests are separate. +system_root() { + case "$2" in + air) _hd="apple,j613 apple,t8122"; _hg=$G15G ;; + *) _hd="apple,j516s apple,t6030"; _hg=$G15S ;; + esac + _hr=$(mkroot "handoff-$1" "$_hd" "$_hg" yes 1 yes) + mkdir -p "$_hr/etc/mesa-m3" + : > "$_hr/etc/mesa-m3/disable" + cat > "$_hr/opt/mesa-m3/libexec/mesa-m3-system-check" <<'H' +#!/bin/sh +here=${0%/*} +printf '%s\n' "$@" > "$here/system-check.args" +env | sort > "$here/system-check.env" +[ ! -e "$here/system-check.sleep" ] || sleep 8 +[ ! -e "$here/system-check.fail" ] +H + chmod +x "$_hr/opt/mesa-m3/libexec/mesa-m3-system-check" + echo "$_hr" +} +handoff() { # ROOT FALLBACK HELPER_CALLED [VAR=value...] + _hr=$1 _hfb=$2 _hc=$3 + shift 3 + rm -f "$_hr/opt/mesa-m3/libexec/system-check.args" + _ha="$_hr$T/home/.local/state/mesa-m3/attempt" + _hbefore=missing + [ ! -f "$_ha" ] || _hbefore=$(sha256sum "$_ha" | cut -d' ' -f1) + _ho=$(env -i PATH="$T/bin:/usr/bin:/bin" HOME="$T/home" FAKE_LOG="$T/log" \ + XDG_CURRENT_DESKTOP=Hyprland "$@" sh -c ' + MESA_M3_SESSION_ENV_NO_RUN=1 . "$1" + mesa_m3_session_env "$2" + printf "software=%s\nmarker=%s\nlegacy=%s\n" "${LIBGL_ALWAYS_SOFTWARE:-}" "${MESA_M3_FALLBACK:-}" "${MESA_M3_SESSION:-}" + cat "$2/run/user/$(id -u)/mesa-m3-session.state" + ' sh "$ENVFILE" "$_hr") + _hafter=missing + [ ! -f "$_ha" ] || _hafter=$(sha256sum "$_ha" | cut -d' ' -f1) + _hcalled=no + [ ! -e "$_hr/opt/mesa-m3/libexec/system-check.args" ] || _hcalled=yes + if printf '%s\n' "$_ho" | grep -qx "fallback=$_hfb" && + printf '%s\n' "$_ho" | grep -qx 'decision=inactive' && + printf '%s\n' "$_ho" | grep -qx 'reason=opt-out' && + printf '%s\n' "$_ho" | grep -qx 'legacy=' && + [ "$_hc" = "$_hcalled" ] && [ "$_hbefore" = "$_hafter" ]; then + echo "ok ${_hr##*/}: fallback=$_hfb helper=$_hc, legacy attempt unchanged" + else + echo "FAIL ${_hr##*/}: want fallback=$_hfb helper=$_hc; got helper=$_hcalled attempt=$_hbefore/$_hafter: $_ho" + fail=1 + fi + if [ "$_hfb" = none ]; then + if ! printf '%s\n' "$_ho" | grep -qx 'software=' || + ! printf '%s\n' "$_ho" | grep -qx 'marker=' || + ! printf '%s\n' "$_ho" | grep -q 'detail=.*system Mesa on /usr admits'; then + echo "FAIL ${_hr##*/}: system admission exports or state detail: $_ho"; fail=1 + fi + else + if ! printf '%s\n' "$_ho" | grep -qx 'software=1' || + ! printf '%s\n' "$_ho" | grep -qx 'marker=software' || + printf '%s\n' "$_ho" | grep -q 'detail=.*system Mesa on /usr admits'; then + echo "FAIL ${_hr##*/}: recovery exports or state detail: $_ho"; fail=1 + fi + fi +} +HRA=$(system_root user-optout air) +rm "$HRA/etc/mesa-m3/disable" +mkdir -p "$HRA$T/home/.config/mesa-m3" +: > "$HRA$T/home/.config/mesa-m3/disable" +handoff "$HRA" none yes +if [ "$(cat "$HRA/opt/mesa-m3/libexec/system-check.args")" = "$(printf '/dev/dri/renderD128\nt8122')" ] && + grep -qx 'PATH=/usr/bin:/bin' "$HRA/opt/mesa-m3/libexec/system-check.env" && + [ -z "$(grep -vE '^(PATH=/usr/bin:/bin|PWD=.*|SHLVL=[0-9]+|_=.*)$' "$HRA/opt/mesa-m3/libexec/system-check.env")" ]; then + echo 'ok handoff: helper receives exact device/SoC and empty selector environment' +else echo 'FAIL handoff: helper argv/environment'; fail=1; fi +HRP=$(system_root root-optout pro) +handoff "$HRP" none yes +handoff "$HRP" none yes LIBGL_ALWAYS_SOFTWARE=1 MESA_M3_FALLBACK=software +for mode in fail sleep; do + HR=$(system_root "probe-$mode" pro) + : > "$HR/opt/mesa-m3/libexec/system-check.$mode" + handoff "$HR" software yes +done +for mode in missing not-executable; do + HR=$(system_root "probe-$mode" pro) + if [ "$mode" = missing ]; then rm "$HR/opt/mesa-m3/libexec/mesa-m3-system-check"; + else chmod 600 "$HR/opt/mesa-m3/libexec/mesa-m3-system-check"; fi + handoff "$HR" software no +done +for mode in missing-cmdline missing-detector; do + HR=$(system_root "$mode" pro) + if [ "$mode" = missing-cmdline ]; then rm "$HR/proc/cmdline"; + else rm "$HR/opt/mesa-m3/libexec/mesa-m3-user-setup"; fi + handoff "$HR" software no +done +for flag in mesa_m3=off nomodeset; do + HR=$(system_root "kernel-$flag" pro) + echo "root=UUID=x rw $flag quiet" > "$HR/proc/cmdline" + handoff "$HR" software no +done +for journal in noperm error match bank dcp g15g verdict; do + HR=$(system_root "journal-$journal" pro) + handoff "$HR" software no FAKE_JOURNAL="$journal" +done +for dt in 'apple,j615 apple,t8122' 'apple,j514s apple,t6030' \ + 'apple,j614s apple,t6030' 'apple,j613 apple,j615 apple,t8122' \ + 'apple,j613 apple,t6030' 'apple,j516s apple,t6030 apple,t8122'; do + HR=$(system_root "board-$(printf '%s' "$dt" | tr ' ,' '--')" pro) + nul $dt > "$HR/proc/device-tree/compatible" + # Match the GPU to the ambiguous board's first SoC so the old fallback + # remains observable; the system handoff must still refuse it. + case "$dt" in *t8122*) nul apple,agx-t8122 apple,agx-t6030 > "$HR/sys/devices/platform/gpu0/of_node/compatible" ;; esac + handoff "$HR" software no +done +HR=$(system_root no-display pro) +rm "$HR/sys/class/drm/card2/device" +handoff "$HR" software no +HR=$(system_root wrong-display pro) +nul apple,t8122-display-subsystem > "$HR/sys/devices/platform/display/of_node/compatible" +handoff "$HR" software no +HR=$(system_root two-nodes pro) +mkdir "$HR/sys/class/drm/renderD129" +handoff "$HR" software no +for variable in LIBGL_ALWAYS_SOFTWARE=1 \ + LD_LIBRARY_PATH=/opt/mesa-m3/lib \ + VK_DRIVER_FILES=/opt/mesa-m3/share/vulkan/icd.d/asahi_icd.aarch64.json \ + __EGL_VENDOR_LIBRARY_FILENAMES=/opt/mesa-m3/share/glvnd/egl_vendor.d/50_mesa.json \ + ASAHI_M3_EXPERIMENTAL=1 XDG_CURRENT_DESKTOP=chonkstep; do + HR=$(system_root "override-${variable%%=*}" pro) + handoff "$HR" software no "$variable" +done +for value in 'LIBGL_DRIVERS_PATH=/opt/mesa-m3/lib/dri' 'VK_DRIVER_FILES=$UNKNOWN'; do + HR=$(system_root "late-${value%%=*}" pro) + mkdir -p "$HR$T/home/.config/uwsm" + printf '%s\n' "$value" > "$HR$T/home/.config/uwsm/env" + handoff "$HR" software no +done +HR=$(system_root failed-legacy-login pro) +HATT="$HR$T/home/.local/state/mesa-m3/attempt" +HOK="$HR$T/home/.local/state/mesa-m3/session-ok" +mkdir -p "${HATT%/*}" +printf 'schema=aurora.mesa-m3-attempt/2\nboot_id=boot-a\nsession_id=5\nhook_run_id=old-run\n' > "$HATT" +handoff "$HR" software no +printf 'schema=aurora.mesa-m3-session-ok/1\nboot_id=boot-a\nsession_id=wrong\nhook_run_id=old-run\n' > "$HOK" +handoff "$HR" software no +sed -i 's/session_id=wrong/session_id=5/' "$HOK" +handoff "$HR" none yes +rm "$HOK" +sed -i 's/boot_id=boot-a/boot_id=old-boot/' "$HATT" +handoff "$HR" none yes + [ "$fail" = 0 ] && echo "ALL PASS" || echo "SOME FAILED" exit "$fail" diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c new file mode 100644 index 00000000000000..1aa86befdcdba2 --- /dev/null +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: MIT */ +/* Pure board-parser controls: no device opening or EGL calls. */ +#define main system_check_main +#include "../mesa-m3-system-check.c" +#undef main +#include + +#define CHECK(expected, soc, bytes) \ + assert(board_compat_matches(bytes, sizeof(bytes) - 1, soc) == (expected)) + +int main(void) +{ + CHECK(true, "t8122", "apple,j613\0apple,t8122\0apple,arm-platform\0"); + CHECK(true, "t6030", "apple,j516s\0apple,t6030\0apple,arm-platform\0"); + CHECK(true, "t6030", "apple,arm-platform\0apple,t6030\0apple,j516s\0"); + CHECK(false, "t8122", "apple,j615\0apple,t8122\0"); + CHECK(false, "t6030", "apple,j514s\0apple,t6030\0"); + CHECK(false, "t6030", "apple,j516s\0apple,t6031\0"); + CHECK(false, "t8122", "apple,j613\0apple,j615\0apple,t8122\0"); + CHECK(false, "t8122", "apple,j613\0apple,j613\0apple,t8122\0"); + CHECK(false, "t6030", "apple,j516s\0apple,t6030\0apple,t6030\0"); + CHECK(false, "t6030", "apple,j516s\0apple,t6030\0apple,t8122\0"); + CHECK(false, "t8122", "apple,j613\0apple,t8122-extra\0"); + CHECK(false, "t8122", "apple,j613\0apple,t8122"); + CHECK(false, "t8122", "apple,j613\0\0apple,t8122\0"); + CHECK(false, "t6030", "apple,t6030\0"); + CHECK(false, "t6030", "apple,j516s\0"); + CHECK(false, "t8140", "apple,j516s\0apple,t6030\0"); + CHECK(false, "t6030", ""); + puts("17 board parser controls passed"); + return 0; +} diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py new file mode 100644 index 00000000000000..5efc680580b867 --- /dev/null +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""The system-provider decision must see legacy selectors as overrides.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + +RECIPE = Path(__file__).resolve().parents[1] + + +class SystemProviderTest(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.home = "/home/tester" + (self.root / "home/tester/.config").mkdir(parents=True) + prefix = self.root / "opt/mesa-m3/lib" + prefix.mkdir(parents=True) + (prefix / "libgallium-26.1.4.so").touch() + + def run_detector(self, environment=None, system=True): + command = [str(RECIPE / "mesa-m3-user-setup"), "--home", self.home, + "--root", str(self.root), "--list", str(RECIPE / "user-setup.list"), + "--session-desktops", "Hyprland", "--env"] + if system: + command.append("--system-provider") + return subprocess.run(command, env={"PATH": "/usr/bin:/bin", "HOME": self.home, + **(environment or {})}, + capture_output=True, text=True, timeout=10) + + def test_clean_setup(self): + self.assertEqual(self.run_detector().returncode, 0) + + def test_legacy_path_default_stays_accepted(self): + value = {"VK_DRIVER_FILES": "/opt/mesa-m3/share/vulkan/icd.d/asahi.json"} + self.assertEqual(self.run_detector(value, system=False).returncode, 0) + self.assertEqual(self.run_detector(value).returncode, 1) + + def test_legacy_loader_path_is_not_system_mesa(self): + value = {"LD_LIBRARY_PATH": "/opt/mesa-m3/lib"} + self.assertEqual(self.run_detector(value, system=False).returncode, 0) + self.assertEqual(self.run_detector(value).returncode, 1) + + def test_later_config_can_reselect_legacy_mesa(self): + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir() + (folder / "env").write_text("export LIBGL_DRIVERS_PATH=/opt/mesa-m3/lib/dri\n") + self.assertEqual(self.run_detector(system=False).returncode, 0) + self.assertEqual(self.run_detector().returncode, 1) + + def test_explicit_software_choice(self): + self.assertEqual(self.run_detector({"LIBGL_ALWAYS_SOFTWARE": "1"}).returncode, 1) + + def test_only_owned_software_pair_is_ignored(self): + self.assertEqual(self.run_detector({"LIBGL_ALWAYS_SOFTWARE": "1", + "MESA_M3_FALLBACK": "software"}).returncode, 0) + + def test_preload_and_legacy_flags_are_not_clean_system_environment(self): + for variable in ("LD_PRELOAD", "LD_AUDIT", "MESA_M3_SESSION", + "ASAHI_M3_EXPERIMENTAL", "AGX_MESA_DEBUG"): + with self.subTest(variable=variable): + # File assignments avoid loading a preload into the test + # interpreter itself while exercising the same detector. + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir(exist_ok=True) + (folder / "env").write_text(variable + "=/opt/mesa-m3/choice\n") + self.assertEqual(self.run_detector().returncode, 1) + + def test_missing_file_does_not_mask_unresolved_choice(self): + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir() + (folder / "env").write_text("VK_DRIVER_FILES=$OTHER_MESA\n") + result = self.run_detector() + self.assertNotEqual(result.returncode, 0, result.stdout) + + +if __name__ == "__main__": + unittest.main() From 592cad39998b94e087d5ca56b0d7575d0385525f Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Sun, 25 Oct 2026 18:55:36 +1000 Subject: [PATCH 2/7] mesa-m3: refuse hidden providers and bound the system-Mesa handoff The handoff probe runs in a clean environment against /usr, but the desktop keeps the session's own setup. Under --system-provider, also refuse an LD_LIBRARY_PATH entry that holds its own libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm, treat a relative entry as unknown, and refuse GBM_BACKEND, __GLX_VENDOR_LIBRARY_NAME and the Mesa GL/GLES/GLSL version overrides. The legacy gate keeps its existing rules. Run the whole admission in its own sh under one 20-second timeout, so a stalled journal, home directory or scanner refuses and keeps the software fallback instead of holding the login. Run the handoff cases under the suite's own shell so the bash run covers the new path, add stall and override cases, document that the helper does not render and that a later failure is not recorded, and bump pkgrel to 7 with the new checksums. Signed-off-by: Marcelo Alcantara --- tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 8 ++-- .../recipes/mesa-m3/mesa-m3-session-env | 20 ++++++++-- .../recipes/mesa-m3/mesa-m3-user-setup | 19 +++++++++- .../recipes/mesa-m3/tests/SYSTEM-HANDOFF.md | 16 ++++++++ .../recipes/mesa-m3/tests/test-session-env.sh | 30 ++++++++++++++- .../mesa-m3/tests/test-system-provider.py | 37 +++++++++++++++++++ .../recipes/mesa-m3/user-setup.list | 3 ++ 7 files changed, 122 insertions(+), 11 deletions(-) diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index a120de243b1e1f..aca33a6c849525 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -10,7 +10,7 @@ # No profile selection substitutes another Mac's firmware or calibration. pkgname=mesa-m3 pkgver=26.1.4.m3.2 -pkgrel=6 +pkgrel=7 _commit=7ff89a4da387bd561b78efd5f09c98ca8ae18538 pkgdesc="Mesa in /opt/mesa-m3 for the M3 Pro (G15S) and M3 (G15G) GPUs; used by uwsm sessions on T6030, and on T8122 when opted in" arch=('aarch64') @@ -85,14 +85,14 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '10abda78df16e650a77443f50cc46a15e418c42d4d9680c6a61616b232c9bdef' '6c98fab6f9d70449bd7f5258c34eac773836908e060ce5bef86dc5b0ce595e61' '826016665cd0424a614b67add921c0c79c3c698a4d104ea5712518449b75f27c' - '444fd73d505c3ffa7252bb44845457deac2550a411464bbcec075e14f21b2063' + '9cbc38f4bee27ab72d4a288f2741ab1eeb70112ca473a5c98a9695cdfcbe56f7' '6144af8b2e0f04ea615e8c4149e69c45ac49e630c46b2a58f0d1095d977798b2' '530948fc1a124de21c030a497e2ef5351742ba4ad4653484fe30fd1697ad7141' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - 'a446e2b3fa075a1ef239439539119eb12472b372a84ecb9411c4e65ca3112999' - '8f19ebea2b8e669a764d7c4b40ecbc11e8234acfc3a2bfe7910e22de0b64a49d' + '5052ece23b7b028e8762fbf0322a967237861da504cb4dd95fa7f3aace1fd6fe' + '4cae43d33e9fd8b7f703d433b74814968662982c7342c60f059c32eb45648f8a' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' '7eaae7de828e15208fccc84db2b910f38bb1288176419f7af7f4675b5c542ce4' diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env index 3308a963e8f1ba..a90c3b5d2bc143 100644 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-session-env @@ -72,7 +72,8 @@ # J613 (T8122) or a J516S (T6030) is the exception: when the same node, # display, kernel-log, user-setup and same-boot attempt checks pass and # mesa-m3-system-check admits /usr, the session keeps system Mesa -# (fallback=none, no new attempt). mesa_m3=off stays on the software fallback. +# (fallback=none, no new attempt). The whole check runs in its own sh and is +# refused after 20 seconds. mesa_m3=off stays on the software fallback. # # Each login writes /run/user//mesa-m3-session.state (atomically, 0644, # key=value lines, first line schema=aurora.mesa-m3-session/2) and logs one @@ -450,6 +451,16 @@ _mm3_system_admit() { printf '%s\n' "system Mesa on /usr admits /dev/dri/${_mm3_node##*/} ($_mm3_chip); /opt/mesa-m3 stays inactive" } +# _mm3_system_admit in its own sh, under one timeout for the whole check. A +# stalled journal, home directory or scanner would otherwise hold this login +# before any fallback is exported. A timeout refuses, as any failure does. +_mm3_system_admit_bounded() { + _mm3_self=${1:-}/opt/mesa-m3/libexec/mesa-m3-session-env + [ -r "$_mm3_self" ] || return 1 + timeout -k 2 20 sh -c 'MESA_M3_SESSION_ENV_NO_RUN=1 . "$1" && _mm3_p=$3 && _mm3_system_admit "$2"' \ + sh "$_mm3_self" "${1:-}" "$_mm3_p" +} + # _mm3_state ROOT DECISION REASON DETAIL FALLBACK: write the state file and the journal # line. A value holding a newline or carriage return is refused. _mm3_state() { @@ -557,7 +568,7 @@ mesa_m3_session_env() { _mm3_detail=${_mm3_decision#* } _mm3_admitted=0 if [ "${_mm3_decision%% *}" = opt-out ]; then - _mm3_sys=$(_mm3_system_admit "${1:-}") + _mm3_sys=$(_mm3_system_admit_bounded "${1:-}") if [ "$?" -eq 0 ]; then _mm3_admitted=1 _mm3_detail="$_mm3_detail; $_mm3_sys" @@ -583,11 +594,12 @@ if [ -z "${MESA_M3_SESSION_ENV_NO_RUN:-}" ]; then mesa_m3_session_env unset -f mesa_m3_gate mesa_m3_session_env _mm3_init _mm3_state _mm3_node_open _mm3_attempt \ _mm3_soc_of _mm3_25g83_board _mm3_native_display _mm3_kernel_log \ - _mm3_legacy_attempt_clear _mm3_system_admit + _mm3_legacy_attempt_clear _mm3_system_admit _mm3_system_admit_bounded fi unset _mm3_root _mm3_p _mm3_nodes _mm3_node _mm3_n _mm3_drv _mm3_dcp _mm3_c _mm3_dev _mm3_f _mm3_v \ _mm3_val _mm3_ifs _mm3_d _mm3_out _mm3_rc _mm3_decision _mm3_on \ _mm3_so _mm3_uid _mm3_sid _mm3_inv _mm3_ver _mm3_boot _mm3_cr _mm3_nl \ _mm3_body _mm3_kv _mm3_dir _mm3_tmp _mm3_fb _mm3_run _mm3_att _mm3_ok _mm3_faults _mm3_ign _mm3_igd \ _mm3_soc _mm3_name _mm3_dt _mm3_base _mm3_profile _mm3_requested _mm3_files _mm3_gate_ifs _mm3_board _mm3_note \ - _mm3_klog _mm3_sys _mm3_admitted _mm3_detail _mm3_helper _mm3_chip _mm3_identity _mm3_socs _mm3_cmdline + _mm3_klog _mm3_sys _mm3_admitted _mm3_detail _mm3_helper _mm3_chip _mm3_identity _mm3_socs _mm3_cmdline \ + _mm3_self diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index 80a459b2c2551b..5df557a1102198 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -22,7 +22,10 @@ see the comments there. any environment file); empty or absent: unknown --root DIR prefix every path with DIR (tests) --system-provider also refuse legacy /opt selectors and session flags when - checking whether the real desktop will use system Mesa + checking whether the real desktop will use system Mesa; + an LD_LIBRARY_PATH entry then also counts when it holds + only libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm, + and a relative entry makes the result "unknown" Nothing is sourced or evaluated. Files are read as text and split into words the way a POSIX shell would (quotes, backslashes, comments, ";"), with no @@ -259,9 +262,13 @@ class Finder: # These may be left over from an active legacy session. The # admission helper's clean environment cannot prove that the # real desktop will use /usr while these remain in its setup. + # The rest choose the desktop's GL/GBM provider or its version, + # which the clean-environment helper never sees. for name in ('LD_PRELOAD', 'LD_AUDIT', 'MESA_M3_SESSION', 'MESA_M3_PROFILE', 'ASAHI_M3_EXPERIMENTAL', 'AGX_MESA_DEBUG', 'HK_DEBUG_LOG', - 'VK_LOADER_LAYERS_DISABLE', 'QSG_RHI_BACKEND'): + 'VK_LOADER_LAYERS_DISABLE', 'QSG_RHI_BACKEND', + 'GBM_BACKEND', '__GLX_VENDOR_LIBRARY_NAME', 'MESA_GL_VERSION_OVERRIDE', + 'MESA_GLES_VERSION_OVERRIDE', 'MESA_GLSL_VERSION_OVERRIDE'): self.kind[name] = 'flag' def real(self, p): @@ -319,6 +326,11 @@ class Finder: def holds_mesa(self, d): r = self.real(d) pats = ('libgallium*', 'libEGL_mesa*', 'libvulkan_asahi*', 'dri/*_dri.so') + if self.o['system_provider']: + # Any of these replaces what the desktop loads from /usr, even + # without a Mesa driver of its own next to it. + pats += ('libEGL.so*', 'libEGL_*', 'libGLESv2.so*', 'libGL.so*', 'libGLX*', + 'libglapi.so*', 'libgbm.so*', 'gbm/*') return any(glob.glob(os.path.join(glob.escape(r), p)) for p in pats) def expand_home(self, s): @@ -361,6 +373,9 @@ class Finder: continue if '$' in comp or '`' in comp: note(place, f'{label} puts an unresolved entry in {name} ({comp})', sure=False) + elif not comp.startswith('/') and self.o['system_provider']: + # Resolved against whatever directory the desktop starts in. + note(place, f'{label} puts a relative entry in {name} ({comp})', sure=False) elif comp.startswith('/') and not self.ours(comp) and self.holds_mesa(os.path.normpath(comp)): note(place, f'{label} puts another Mesa in {name} ({os.path.normpath(comp)})') return diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md index dbe150d387dc08..e24eeed6f15864 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md +++ b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md @@ -13,6 +13,14 @@ A bounded helper then opens the exact EGL device using only `/usr` libraries and verifies its native renderer and loaded implementation paths. It creates a context but makes no draw, swap or compute calls. +With `--system-provider`, the user-setup check also refuses settings the +clean-environment helper cannot see: an `LD_LIBRARY_PATH` entry holding its +own libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm (relative entries +are unknown), and `GBM_BACKEND`, `__GLX_VENDOR_LIBRARY_NAME` and the Mesa +GL/GLES/GLSL version overrides. The whole admission runs in its own `sh` +under a 20-second timeout, so a stalled journal, home directory or scanner +refuses instead of holding the login. + A failed check keeps the existing software fallback. `mesa_m3=off` and `nomodeset` remain recovery choices. Other boards, including J615, remain outside this system handoff. Explicit legacy profile selection is unchanged. @@ -21,6 +29,14 @@ Only the exact software-fallback pair previously exported by this hook can be cleared. The state remains `inactive`, `reason=opt-out`, `fallback=none` when system Mesa admission succeeds. +The helper proves only that the system EGL device and a context open. It does +not allocate GBM buffers, render or present. A system-Mesa login that passes +admission and then fails later is not recorded: this path writes no attempt, +so the legacy success-marker check cannot see it, and the next login is +admitted again. Recover with `mesa_m3=off` on the kernel command line or by +removing the opt-out file. Separate system-Mesa attempt and success markers +would make that recovery automatic. + Run the host checks from this directory (a writable TMPDIR may be needed): ```sh diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh b/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh index 6c7b2eb659b6fc..c23665597b6e34 100755 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh @@ -10,6 +10,9 @@ T=$(mktemp -d) trap 'rm -rf "$T"' EXIT fail=0 UID_=$(id -u) +# The shell this suite runs under, for the cases that must exercise it. +TSH=sh +[ -z "${BASH_VERSION:-}" ] || TSH=bash mkdir -p "$T/bin" "$T/home" # journalctl, as the hook calls it (the whole kernel log of this boot): @@ -36,6 +39,7 @@ verdictok) normal; echo "asahi 292400000.gpu: M3 G15G verdict: firmware-running: g15g) normal; echo "asahi 292400000.gpu: M3 G15G: firmware crashed, retained crashlog bytes=4096" ;; noperm) : ;; error) echo "Failed to open journal: Permission denied" >&2; exit 1 ;; +stall) sleep 60; normal ;; esac exit 0 J @@ -686,6 +690,8 @@ env | sort > "$here/system-check.env" [ ! -e "$here/system-check.fail" ] H chmod +x "$_hr/opt/mesa-m3/libexec/mesa-m3-system-check" + # The bounded admission sources the installed hook again in its own sh. + cp "$ENVFILE" "$_hr/opt/mesa-m3/libexec/mesa-m3-session-env" echo "$_hr" } handoff() { # ROOT FALLBACK HELPER_CALLED [VAR=value...] @@ -696,7 +702,7 @@ handoff() { # ROOT FALLBACK HELPER_CALLED [VAR=value...] _hbefore=missing [ ! -f "$_ha" ] || _hbefore=$(sha256sum "$_ha" | cut -d' ' -f1) _ho=$(env -i PATH="$T/bin:/usr/bin:/bin" HOME="$T/home" FAKE_LOG="$T/log" \ - XDG_CURRENT_DESKTOP=Hyprland "$@" sh -c ' + XDG_CURRENT_DESKTOP=Hyprland "$@" "$TSH" -c ' MESA_M3_SESSION_ENV_NO_RUN=1 . "$1" mesa_m3_session_env "$2" printf "software=%s\nmarker=%s\nlegacy=%s\n" "${LIBGL_ALWAYS_SOFTWARE:-}" "${MESA_M3_FALLBACK:-}" "${MESA_M3_SESSION:-}" @@ -802,6 +808,28 @@ for value in 'LIBGL_DRIVERS_PATH=/opt/mesa-m3/lib/dri' 'VK_DRIVER_FILES=$UNKNOWN printf '%s\n' "$value" > "$HR$T/home/.config/uwsm/env" handoff "$HR" software no done +for variable in GBM_BACKEND=nvidia-drm MESA_GL_VERSION_OVERRIDE=4.6; do + HR=$(system_root "override-${variable%%=*}" pro) + handoff "$HR" software no "$variable" +done +HR=$(system_root no-installed-hook pro) +rm "$HR/opt/mesa-m3/libexec/mesa-m3-session-env" +handoff "$HR" software no +# A stalled journal or scanner refuses within the bound (20 s, then 2 s to +# kill) and keeps the software fallback, instead of holding the login. +HR=$(system_root stalled-journal pro) +_hstart=$(date +%s) +handoff "$HR" software no FAKE_JOURNAL=stall +_hspent=$(( $(date +%s) - _hstart )) +if [ "$_hspent" -le 30 ]; then echo "ok stalled-journal: refused after ${_hspent}s" +else echo "FAIL stalled-journal: took ${_hspent}s"; fail=1; fi +HR=$(system_root stalled-detector pro) +printf '#!/bin/sh\nsleep 60\n' > "$HR/opt/mesa-m3/libexec/mesa-m3-user-setup" +_hstart=$(date +%s) +handoff "$HR" software no +_hspent=$(( $(date +%s) - _hstart )) +if [ "$_hspent" -le 30 ]; then echo "ok stalled-detector: refused after ${_hspent}s" +else echo "FAIL stalled-detector: took ${_hspent}s"; fail=1; fi HR=$(system_root failed-legacy-login pro) HATT="$HR$T/home/.local/state/mesa-m3/attempt" HOK="$HR$T/home/.local/state/mesa-m3/session-ok" diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py index 5efc680580b867..1d6b99eaea314d 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -67,6 +67,43 @@ def test_preload_and_legacy_flags_are_not_clean_system_environment(self): (folder / "env").write_text(variable + "=/opt/mesa-m3/choice\n") self.assertEqual(self.run_detector().returncode, 1) + def test_loader_only_directories_are_another_provider(self): + for library in ("libEGL.so.1", "libGLESv2.so.2", "libGL.so.1", "libGLX_mesa.so.0", + "libglapi.so.0", "libgbm.so.1", "gbm/dri_gbm.so"): + with self.subTest(library=library): + folder = self.root / "provider" / library.replace("/", "-") + (folder / library).parent.mkdir(parents=True, exist_ok=True) + (folder / library).touch() + value = {"LD_LIBRARY_PATH": "/provider/" + library.replace("/", "-")} + self.assertEqual(self.run_detector(value, system=False).returncode, 0) + self.assertEqual(self.run_detector(value).returncode, 1) + + def test_unrelated_library_directory_stays_clean(self): + (self.root / "provider/other").mkdir(parents=True) + (self.root / "provider/other/libfoo.so.1").touch() + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "/provider/other"}).returncode, 0) + + def test_relative_loader_entry_is_unknown(self): + for value in ("lib", "./lib", "/usr/lib/extra:lib"): + with self.subTest(value=value): + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}, system=False).returncode, 0) + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}).returncode, 3) + + def test_provider_and_version_overrides(self): + for variable, value in (("GBM_BACKEND", "nvidia-drm"), + ("__GLX_VENDOR_LIBRARY_NAME", "nvidia"), + ("MESA_GL_VERSION_OVERRIDE", "4.6"), + ("MESA_GLES_VERSION_OVERRIDE", "3.2"), + ("MESA_GLSL_VERSION_OVERRIDE", "460")): + with self.subTest(variable=variable): + self.assertEqual(self.run_detector({variable: value}, system=False).returncode, 0) + self.assertEqual(self.run_detector({variable: value}).returncode, 1) + folder = self.root / "home/tester/.config/hypr" + folder.mkdir(parents=True, exist_ok=True) + (folder / "env.conf").write_text(f"env = {variable},{value}\n") + self.assertEqual(self.run_detector().returncode, 1) + (folder / "env.conf").unlink() + def test_missing_file_does_not_mask_unresolved_choice(self): folder = self.root / "home/tester/.config/uwsm" folder.mkdir() diff --git a/tools/aurora-sep/recipes/mesa-m3/user-setup.list b/tools/aurora-sep/recipes/mesa-m3/user-setup.list index 0bd73c8f394f6c..6661c1dd5b8cfc 100644 --- a/tools/aurora-sep/recipes/mesa-m3/user-setup.list +++ b/tools/aurora-sep/recipes/mesa-m3/user-setup.list @@ -15,6 +15,9 @@ # file, for an assignment that replaces it (does not keep # $LD_LIBRARY_PATH), since that drops /opt/mesa-m3/lib after the # session hook ran. Unrelated entries added to it are no finding. +# With --system-provider, an entry holding libEGL, libGLESv2, libGL, +# libGLX, libglapi, libgbm or a gbm/ directory is a finding too, and +# a relative entry is unknown. # envfile a file that sets session variables, searched for the variables # above. Shell, environment.d and /etc/environment lines are split # like a POSIX shell (quotes, comments, "export", ";"); Hyprland From 0d8f3844ce322c4e2cf155149b91de7bd9220cbb Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Sun, 25 Oct 2026 19:05:04 +1000 Subject: [PATCH 3/7] mesa-m3: parse LD_LIBRARY_PATH like glibc for the system handoff glibc also splits LD_LIBRARY_PATH on ";" and reads an empty entry as the current directory, so ":", "/a:" or "/a;/b" could still hide another provider from --system-provider. Split on both and treat a real empty entry as unknown. A reference to the previous value still counts as no entry. Signed-off-by: Marcelo Alcantara --- tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 4 ++-- .../recipes/mesa-m3/mesa-m3-user-setup | 20 ++++++++++++---- .../recipes/mesa-m3/tests/SYSTEM-HANDOFF.md | 12 ++++++---- .../mesa-m3/tests/test-system-provider.py | 23 +++++++++++++++++++ .../recipes/mesa-m3/user-setup.list | 5 ++-- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index aca33a6c849525..0e9b7b2b0a7c92 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -91,8 +91,8 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - '5052ece23b7b028e8762fbf0322a967237861da504cb4dd95fa7f3aace1fd6fe' - '4cae43d33e9fd8b7f703d433b74814968662982c7342c60f059c32eb45648f8a' + '3a04f0b3fd3e137b5b12fba73f41cbc6d4a5935c61145d9738bb4e0e8ee09bf0' + 'c9c37dead02e3098e69e85a1f0fea73b6244ce2b497e57e85127ea9341b91664' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' '7eaae7de828e15208fccc84db2b910f38bb1288176419f7af7f4675b5c542ce4' diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index 5df557a1102198..0f1db53250cfd3 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -24,8 +24,9 @@ see the comments there. --system-provider also refuse legacy /opt selectors and session flags when checking whether the real desktop will use system Mesa; an LD_LIBRARY_PATH entry then also counts when it holds - only libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm, - and a relative entry makes the result "unknown" + only libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm; + entries split on ":" and ";", and a relative or empty + entry makes the result "unknown" Nothing is sourced or evaluated. Files are read as text and split into words the way a POSIX shell would (quotes, backslashes, comments, ";"), with no @@ -365,10 +366,19 @@ class Finder: note(place, f'{label} replaces {name} (without ${name})') return # Drop the references to the previous value (also ${NAME:+:$NAME}). + # For a system-provider check they leave a mark instead, so that + # an empty entry left by the removal is not taken for a real one. + strict = self.o['system_provider'] + prev = '\0' if strict else '' rest = re.sub(r'\$\{' + re.escape(name) + r'(?::?[-+=?][^}]*)?\}|\$' + re.escape(name) + r'(?![A-Za-z0-9_])', - '', value) - for comp in rest.split(':'): - comp = self.expand_home(comp.strip()) + prev, value) + # glibc's loader also splits on ";" and reads an empty entry as + # the current directory. + for comp in (re.split('[:;]', rest) if strict else rest.split(':')): + if strict and comp.strip() == '': + note(place, f'{label} puts an empty entry in {name} (the current directory)', sure=False) + continue + comp = self.expand_home(comp.replace('\0', '').strip()) if comp == '': continue if '$' in comp or '`' in comp: diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md index e24eeed6f15864..ddfa0afa41cd97 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md +++ b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md @@ -15,11 +15,13 @@ a context but makes no draw, swap or compute calls. With `--system-provider`, the user-setup check also refuses settings the clean-environment helper cannot see: an `LD_LIBRARY_PATH` entry holding its -own libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm (relative entries -are unknown), and `GBM_BACKEND`, `__GLX_VENDOR_LIBRARY_NAME` and the Mesa -GL/GLES/GLSL version overrides. The whole admission runs in its own `sh` -under a 20-second timeout, so a stalled journal, home directory or scanner -refuses instead of holding the login. +own libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm, and +`GBM_BACKEND`, `__GLX_VENDOR_LIBRARY_NAME` and the Mesa GL/GLES/GLSL version +overrides. It splits `LD_LIBRARY_PATH` on `:` and `;` as glibc does, and a +relative or empty entry (the current directory) is unknown. The admission +returns within a 20-second timeout, so a stalled journal, home directory or +scanner refuses instead of holding the login. The helper keeps its own +5-second timeout. A failed check keeps the existing software fallback. `mesa_m3=off` and `nomodeset` remain recovery choices. Other boards, including J615, remain diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py index 1d6b99eaea314d..8aecf03d92dd83 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -89,6 +89,29 @@ def test_relative_loader_entry_is_unknown(self): self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}, system=False).returncode, 0) self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}).returncode, 3) + def test_loader_path_parsed_like_glibc(self): + (self.root / "provider/other").mkdir(parents=True) + (self.root / "provider/evil").mkdir(parents=True) + (self.root / "provider/evil/libEGL.so.1").touch() + for value, code in ((":", 3), ("/provider/other:", 3), (":/provider/other", 3), + ("/provider/other::/usr/lib/extra", 3), + ("/provider/other;/provider/evil", 1)): + with self.subTest(value=value): + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}).returncode, code) + + def test_previous_value_reference_is_not_an_empty_entry(self): + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir(parents=True) + (self.root / "provider/other").mkdir(parents=True) + for line in ("export LD_LIBRARY_PATH=/provider/other:$LD_LIBRARY_PATH\n", + "export LD_LIBRARY_PATH=/provider/other${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\n", + "export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/provider/other\n"): + with self.subTest(line=line): + (folder / "env").write_text(line) + self.assertEqual(self.run_detector().returncode, 0) + (folder / "env").write_text("export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:\n") + self.assertEqual(self.run_detector().returncode, 3) + def test_provider_and_version_overrides(self): for variable, value in (("GBM_BACKEND", "nvidia-drm"), ("__GLX_VENDOR_LIBRARY_NAME", "nvidia"), diff --git a/tools/aurora-sep/recipes/mesa-m3/user-setup.list b/tools/aurora-sep/recipes/mesa-m3/user-setup.list index 6661c1dd5b8cfc..1a6e6bd888f4cd 100644 --- a/tools/aurora-sep/recipes/mesa-m3/user-setup.list +++ b/tools/aurora-sep/recipes/mesa-m3/user-setup.list @@ -16,8 +16,9 @@ # $LD_LIBRARY_PATH), since that drops /opt/mesa-m3/lib after the # session hook ran. Unrelated entries added to it are no finding. # With --system-provider, an entry holding libEGL, libGLESv2, libGL, -# libGLX, libglapi, libgbm or a gbm/ directory is a finding too, and -# a relative entry is unknown. +# libGLX, libglapi, libgbm or a gbm/ directory is a finding too; +# entries split on ":" and ";" as glibc does, and a relative or +# empty entry (the current directory) is unknown. # envfile a file that sets session variables, searched for the variables # above. Shell, environment.d and /etc/environment lines are split # like a POSIX shell (quotes, comments, "export", ";"); Hyprland From 6ab1ab38b91baa1f6bfa62ab464532584c7ccd6a Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Sun, 25 Oct 2026 19:14:27 +1000 Subject: [PATCH 4/7] mesa-m3: expand LD_LIBRARY_PATH references with the inherited value A placeholder for $LD_LIBRARY_PATH hid the current-directory entry that "/a:$LD_LIBRARY_PATH" or "${LD_LIBRARY_PATH}:/a" leave when the inherited value is unset or empty. For --system-provider, expand plain references with the inherited value and ${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} as the shell does, treat any other form as unknown, and ignore a wholly empty value as glibc does. Signed-off-by: Marcelo Alcantara --- tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 2 +- .../recipes/mesa-m3/mesa-m3-user-setup | 42 ++++++++++++++----- .../mesa-m3/tests/test-system-provider.py | 30 +++++++++---- 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index 0e9b7b2b0a7c92..da82358e9202de 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -91,7 +91,7 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - '3a04f0b3fd3e137b5b12fba73f41cbc6d4a5935c61145d9738bb4e0e8ee09bf0' + 'f89871d26ee542df9ba41ffa42aceebe48f8fd7fe7d82cd1d4ed1b51d019a2d4' 'c9c37dead02e3098e69e85a1f0fea73b6244ce2b497e57e85127ea9341b91664' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index 0f1db53250cfd3..a09a558aabbda2 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -255,6 +255,7 @@ class Finder: self.paths, self.details, self.findings = [], [], [] self.present = self.unknown = False self.cur_kind = None + self.env = None self.kind = {} for k, i in items: if k in ('variable', 'flag', 'prefix', 'ldpath'): @@ -365,20 +366,38 @@ class Finder: # while the other Mesa variables still point into the prefix. note(place, f'{label} replaces {name} (without ${name})') return - # Drop the references to the previous value (also ${NAME:+:$NAME}). - # For a system-provider check they leave a mark instead, so that - # an empty entry left by the removal is not taken for a real one. + ref = r'\$\{' + re.escape(name) + r'(?::?[-+=?][^}]*)?\}|\$' + re.escape(name) + r'(?![A-Za-z0-9_])' strict = self.o['system_provider'] - prev = '\0' if strict else '' - rest = re.sub(r'\$\{' + re.escape(name) + r'(?::?[-+=?][^}]*)?\}|\$' + re.escape(name) + r'(?![A-Za-z0-9_])', - prev, value) - # glibc's loader also splits on ";" and reads an empty entry as - # the current directory. - for comp in (re.split('[:;]', rest) if strict else rest.split(':')): + if not strict: + # Drop the references to the previous value (also ${NAME:+:$NAME}). + rest = re.sub(ref, '', value) + comps = rest.split(':') + else: + # Expand them with the inherited value instead: "/a:$NAME" with + # NAME unset leaves "/a:", whose empty entry glibc's loader + # reads as the current directory. Only plain references and + # ${NAME:+:$NAME} have a known result. glibc also splits on ";" + # and ignores a wholly empty value. + prev = (self.env or {}).get(name, '') + plain = ('$' + name, '${' + name + '}') + cond = tuple('${' + name + ':+:' + r + '}' for r in plain) + def expand(m): + t = m.group(0) + if t in plain: + return prev + if t in cond: + return ':' + prev if prev else '' + return '\0' + rest = re.sub(ref, expand, value) + comps = re.split('[:;]', rest) if rest.strip() else [] + for comp in comps: + if strict and '\0' in comp: + note(place, f'{label} uses {name} in a form whose result is not known', sure=False) + continue if strict and comp.strip() == '': - note(place, f'{label} puts an empty entry in {name} (the current directory)', sure=False) + note(place, f'{label} leaves an empty entry in {name} (the current directory)', sure=False) continue - comp = self.expand_home(comp.replace('\0', '').strip()) + comp = self.expand_home(comp.strip()) if comp == '': continue if '$' in comp or '`' in comp: @@ -522,6 +541,7 @@ class Finder: 'uses its own default prefix') def run(self, env): + self.env = env if env is not None: self.check_env(env, 'the environment') env_prefix = env.get('CHONKSTEP_M3_MESA_PREFIX') if env else None diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py index 8aecf03d92dd83..40841a56153a3c 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -99,18 +99,30 @@ def test_loader_path_parsed_like_glibc(self): with self.subTest(value=value): self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}).returncode, code) - def test_previous_value_reference_is_not_an_empty_entry(self): + def test_previous_value_expands_like_the_shell(self): folder = self.root / "home/tester/.config/uwsm" folder.mkdir(parents=True) (self.root / "provider/other").mkdir(parents=True) - for line in ("export LD_LIBRARY_PATH=/provider/other:$LD_LIBRARY_PATH\n", - "export LD_LIBRARY_PATH=/provider/other${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\n", - "export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/provider/other\n"): - with self.subTest(line=line): - (folder / "env").write_text(line) - self.assertEqual(self.run_detector().returncode, 0) - (folder / "env").write_text("export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:\n") - self.assertEqual(self.run_detector().returncode, 3) + plain_after = "export LD_LIBRARY_PATH=/provider/other:$LD_LIBRARY_PATH\n" + plain_before = "export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/provider/other\n" + conditional = "export LD_LIBRARY_PATH=/provider/other${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\n" + other_form = "export LD_LIBRARY_PATH=/provider/other:${LD_LIBRARY_PATH:-/x}\n" + # inherited value: unset, empty, set + for inherited, expect in ((None, {plain_after: 3, plain_before: 3, conditional: 0}), + ("", {plain_after: 3, plain_before: 3, conditional: 0}), + ("/usr/lib/extra", {plain_after: 0, plain_before: 0, conditional: 0})): + environment = {} if inherited is None else {"LD_LIBRARY_PATH": inherited} + for line, code in expect.items(): + with self.subTest(inherited=inherited, line=line): + (folder / "env").write_text(line) + self.assertEqual(self.run_detector(environment).returncode, code) + (folder / "env").write_text(other_form) + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "/usr/lib/extra"}).returncode, 3) + (folder / "env").write_text(conditional) + self.assertEqual(self.run_detector(system=False).returncode, 0) + + def test_empty_inherited_value_is_no_entry(self): + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": ""}).returncode, 0) def test_provider_and_version_overrides(self): for variable, value in (("GBM_BACKEND", "nvidia-drm"), From 9ad538efe524ae3b5f081d619610b0eb445e1985 Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Sun, 25 Oct 2026 19:19:37 +1000 Subject: [PATCH 5/7] mesa-m3: count a plain LD_LIBRARY_PATH reference as possibly empty An earlier file can unset LD_LIBRARY_PATH, so its inherited value does not show what "/a:$LD_LIBRARY_PATH" expands to. For --system-provider, take a plain reference as empty, accept ${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} and its ${...} spelling, keep spaces in an entry as glibc does, and treat only a truly empty value as no entry. Signed-off-by: Marcelo Alcantara --- tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 4 +-- .../recipes/mesa-m3/mesa-m3-user-setup | 34 ++++++++----------- .../recipes/mesa-m3/tests/SYSTEM-HANDOFF.md | 4 ++- .../mesa-m3/tests/test-system-provider.py | 14 +++++--- .../recipes/mesa-m3/user-setup.list | 4 ++- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index da82358e9202de..3b356fe63d475d 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -91,8 +91,8 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - 'f89871d26ee542df9ba41ffa42aceebe48f8fd7fe7d82cd1d4ed1b51d019a2d4' - 'c9c37dead02e3098e69e85a1f0fea73b6244ce2b497e57e85127ea9341b91664' + 'e1b592b6c0981627f2c0649294cbf41c703cc29f3e2c95e67771524e6cfc592d' + '96c3101c15c73d26cd3086bc8ac4e8eb35d42300d495ca0ffd2bd03960ef3332' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' '7eaae7de828e15208fccc84db2b910f38bb1288176419f7af7f4675b5c542ce4' diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index a09a558aabbda2..b092a7e0de82e5 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -255,7 +255,6 @@ class Finder: self.paths, self.details, self.findings = [], [], [] self.present = self.unknown = False self.cur_kind = None - self.env = None self.kind = {} for k, i in items: if k in ('variable', 'flag', 'prefix', 'ldpath'): @@ -366,38 +365,34 @@ class Finder: # while the other Mesa variables still point into the prefix. note(place, f'{label} replaces {name} (without ${name})') return - ref = r'\$\{' + re.escape(name) + r'(?::?[-+=?][^}]*)?\}|\$' + re.escape(name) + r'(?![A-Za-z0-9_])' + n = re.escape(name) + ref = (r'\$\{' + n + r':\+:\$\{' + n + r'\}\}|\$\{' + n + r'(?::?[-+=?][^}]*)?\}|\$' + n + + r'(?![A-Za-z0-9_])') strict = self.o['system_provider'] if not strict: # Drop the references to the previous value (also ${NAME:+:$NAME}). rest = re.sub(ref, '', value) comps = rest.split(':') else: - # Expand them with the inherited value instead: "/a:$NAME" with - # NAME unset leaves "/a:", whose empty entry glibc's loader - # reads as the current directory. Only plain references and - # ${NAME:+:$NAME} have a known result. glibc also splits on ";" - # and ignores a wholly empty value. - prev = (self.env or {}).get(name, '') + # The previous value may be empty here (unset, or cleared by an + # earlier file), and then "/a:$NAME" leaves "/a:", whose empty + # entry glibc's loader reads as the current directory. So a + # plain reference counts as empty; ${NAME:+:$NAME} adds nothing + # then and is the safe form. Any other form is unknown. glibc + # also splits on ";" and ignores a wholly empty value. plain = ('$' + name, '${' + name + '}') cond = tuple('${' + name + ':+:' + r + '}' for r in plain) - def expand(m): - t = m.group(0) - if t in plain: - return prev - if t in cond: - return ':' + prev if prev else '' - return '\0' - rest = re.sub(ref, expand, value) - comps = re.split('[:;]', rest) if rest.strip() else [] + rest = re.sub(ref, lambda m: '' if m.group(0) in plain + cond else '\0', value) + comps = re.split('[:;]', rest) if rest != '' else [] for comp in comps: if strict and '\0' in comp: note(place, f'{label} uses {name} in a form whose result is not known', sure=False) continue - if strict and comp.strip() == '': + if strict and comp == '': note(place, f'{label} leaves an empty entry in {name} (the current directory)', sure=False) continue - comp = self.expand_home(comp.strip()) + # glibc takes an entry as written, spaces included. + comp = self.expand_home(comp if strict else comp.strip()) if comp == '': continue if '$' in comp or '`' in comp: @@ -541,7 +536,6 @@ class Finder: 'uses its own default prefix') def run(self, env): - self.env = env if env is not None: self.check_env(env, 'the environment') env_prefix = env.get('CHONKSTEP_M3_MESA_PREFIX') if env else None diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md index ddfa0afa41cd97..c5b7c978049197 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md +++ b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md @@ -18,7 +18,9 @@ clean-environment helper cannot see: an `LD_LIBRARY_PATH` entry holding its own libEGL, libGLESv2, libGL, libGLX, libglapi or libgbm, and `GBM_BACKEND`, `__GLX_VENDOR_LIBRARY_NAME` and the Mesa GL/GLES/GLSL version overrides. It splits `LD_LIBRARY_PATH` on `:` and `;` as glibc does, and a -relative or empty entry (the current directory) is unknown. The admission +relative or empty entry (the current directory) is unknown. A plain +`$LD_LIBRARY_PATH` reference counts as empty, because an earlier file may +have cleared it; `${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}` passes. The admission returns within a 20-second timeout, so a stalled journal, home directory or scanner refuses instead of holding the login. The helper keeps its own 5-second timeout. diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py index 40841a56153a3c..4b051e34ff2f4d 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -107,10 +107,11 @@ def test_previous_value_expands_like_the_shell(self): plain_before = "export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/provider/other\n" conditional = "export LD_LIBRARY_PATH=/provider/other${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\n" other_form = "export LD_LIBRARY_PATH=/provider/other:${LD_LIBRARY_PATH:-/x}\n" - # inherited value: unset, empty, set - for inherited, expect in ((None, {plain_after: 3, plain_before: 3, conditional: 0}), - ("", {plain_after: 3, plain_before: 3, conditional: 0}), - ("/usr/lib/extra", {plain_after: 0, plain_before: 0, conditional: 0})): + nested = "export LD_LIBRARY_PATH=/provider/other${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\n" + # An earlier file can clear the inherited value, so a plain reference + # counts as possibly empty even when the session inherits one. + for inherited in (None, "", "/usr/lib/extra"): + expect = {plain_after: 3, plain_before: 3, conditional: 0, nested: 0} environment = {} if inherited is None else {"LD_LIBRARY_PATH": inherited} for line, code in expect.items(): with self.subTest(inherited=inherited, line=line): @@ -124,6 +125,11 @@ def test_previous_value_expands_like_the_shell(self): def test_empty_inherited_value_is_no_entry(self): self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": ""}).returncode, 0) + def test_spaces_are_a_relative_entry(self): + for value in (" ", "/usr/lib/extra: "): + with self.subTest(value=value): + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}).returncode, 3) + def test_provider_and_version_overrides(self): for variable, value in (("GBM_BACKEND", "nvidia-drm"), ("__GLX_VENDOR_LIBRARY_NAME", "nvidia"), diff --git a/tools/aurora-sep/recipes/mesa-m3/user-setup.list b/tools/aurora-sep/recipes/mesa-m3/user-setup.list index 1a6e6bd888f4cd..31f55e501f0885 100644 --- a/tools/aurora-sep/recipes/mesa-m3/user-setup.list +++ b/tools/aurora-sep/recipes/mesa-m3/user-setup.list @@ -18,7 +18,9 @@ # With --system-provider, an entry holding libEGL, libGLESv2, libGL, # libGLX, libglapi, libgbm or a gbm/ directory is a finding too; # entries split on ":" and ";" as glibc does, and a relative or -# empty entry (the current directory) is unknown. +# empty entry (the current directory) is unknown. A plain reference +# to the previous value counts as empty, since an earlier file can +# clear it; ${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} is the safe form. # envfile a file that sets session variables, searched for the variables # above. Shell, environment.d and /etc/environment lines are split # like a POSIX shell (quotes, comments, "export", ";"); Hyprland From 9c32049ee134f59804632ab6a97ac46beae0da80 Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Sun, 25 Oct 2026 19:24:56 +1000 Subject: [PATCH 6/7] mesa-m3: refuse LD_LIBRARY_PATH references joined to other text "${LD_LIBRARY_PATH}/x" or "/a$LD_LIBRARY_PATH" name a directory the detector cannot see, so for --system-provider a reference counts only when it stands alone between separators; anything else is unknown. Take the environment's own value literally, and keep the legacy gate's parsing exactly as it was. Signed-off-by: Marcelo Alcantara --- tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 2 +- .../recipes/mesa-m3/mesa-m3-user-setup | 26 ++++++++++++----- .../mesa-m3/tests/test-system-provider.py | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index 3b356fe63d475d..8b0f9912be7c36 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -91,7 +91,7 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - 'e1b592b6c0981627f2c0649294cbf41c703cc29f3e2c95e67771524e6cfc592d' + '263c17b51d750d3bee1329e2e57ab806ab06f652fd65580443c32e37c2114736' '96c3101c15c73d26cd3086bc8ac4e8eb35d42300d495ca0ffd2bd03960ef3332' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index b092a7e0de82e5..75c3ecc37809c6 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -366,23 +366,35 @@ class Finder: note(place, f'{label} replaces {name} (without ${name})') return n = re.escape(name) - ref = (r'\$\{' + n + r':\+:\$\{' + n + r'\}\}|\$\{' + n + r'(?::?[-+=?][^}]*)?\}|\$' + n + - r'(?![A-Za-z0-9_])') strict = self.o['system_provider'] if not strict: # Drop the references to the previous value (also ${NAME:+:$NAME}). - rest = re.sub(ref, '', value) + rest = re.sub(r'\$\{' + n + r'(?::?[-+=?][^}]*)?\}|\$' + n + r'(?![A-Za-z0-9_])', '', value) comps = rest.split(':') + elif place.startswith('environment:'): + # Already expanded: taken literally, as glibc does. + comps = re.split('[:;]', value) if value != '' else [] else: # The previous value may be empty here (unset, or cleared by an # earlier file), and then "/a:$NAME" leaves "/a:", whose empty # entry glibc's loader reads as the current directory. So a - # plain reference counts as empty; ${NAME:+:$NAME} adds nothing - # then and is the safe form. Any other form is unknown. glibc - # also splits on ";" and ignores a wholly empty value. + # plain reference counts as empty, and ${NAME:+:$NAME} (which + # then adds nothing) is the safe form. A reference must stand + # alone between separators: "${NAME}/x" names a directory we + # cannot see. Anything else is unknown. glibc also splits on + # ";" and ignores a wholly empty value. + ref = (r'\$\{' + n + r':\+:\$\{' + n + r'\}\}|\$\{' + n + r'(?::?[-+=?][^}]*)?\}|\$' + n + + r'(?![A-Za-z0-9_])') plain = ('$' + name, '${' + name + '}') cond = tuple('${' + name + ':+:' + r + '}' for r in plain) - rest = re.sub(ref, lambda m: '' if m.group(0) in plain + cond else '\0', value) + sep = (':', ';') + rest, at = '', 0 + for m in re.finditer(ref, value): + t, before, after = m.group(0), value[m.start() - 1:m.start()], value[m.end():m.end() + 1] + alone = (after in ('',) + sep) and (t in cond or before in ('',) + sep) + rest += value[at:m.start()] + ('' if alone and t in plain + cond else '\0') + at = m.end() + rest += value[at:] comps = re.split('[:;]', rest) if rest != '' else [] for comp in comps: if strict and '\0' in comp: diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py index 4b051e34ff2f4d..565f288e98fd6a 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -125,6 +125,34 @@ def test_previous_value_expands_like_the_shell(self): def test_empty_inherited_value_is_no_entry(self): self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": ""}).returncode, 0) + def test_reference_with_a_suffix_is_unknown(self): + (self.root / "provider/suffix").mkdir(parents=True) + (self.root / "provider/suffix/libEGL.so.1").touch() + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir(parents=True) + for line in ("export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}/suffix\n", + "export LD_LIBRARY_PATH=/a${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}/suffix\n", + "export LD_LIBRARY_PATH=/prefix$LD_LIBRARY_PATH\n"): + with self.subTest(line=line): + (folder / "env").write_text(line) + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "/provider"}).returncode, 3) + + def test_environment_value_is_literal(self): + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "$LD_LIBRARY_PATH"}).returncode, 3) + + def test_legacy_ldpath_rules_are_unchanged(self): + (self.root / "provider/mesa").mkdir(parents=True) + (self.root / "provider/mesa/libgallium-26.so").touch() + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir(parents=True) + for line, code in (("export LD_LIBRARY_PATH=/x:$LD_LIBRARY_PATH\n", 0), + ("export LD_LIBRARY_PATH=/provider/mesa:$LD_LIBRARY_PATH\n", 1), + ("export LD_LIBRARY_PATH=lib:$LD_LIBRARY_PATH\n", 0), + ("export LD_LIBRARY_PATH=/x::$LD_LIBRARY_PATH\n", 0)): + with self.subTest(line=line): + (folder / "env").write_text(line) + self.assertEqual(self.run_detector(system=False).returncode, code) + def test_spaces_are_a_relative_entry(self): for value in (" ", "/usr/lib/extra: "): with self.subTest(value=value): From ee712c7f985d5f835d38a1c8addae89cd2836f0f Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Sun, 25 Oct 2026 19:29:53 +1000 Subject: [PATCH 7/7] mesa-m3: no empty entry from a leading conditional LD_LIBRARY_PATH append "${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" with nothing before it expands to ":/prev", a current-directory entry, so for --system-provider a conditional append needs an entry before it. Do not expand ~ or $HOME in the environment's own value either; glibc does not. Signed-off-by: Marcelo Alcantara --- tools/aurora-sep/recipes/mesa-m3/PKGBUILD | 2 +- .../recipes/mesa-m3/mesa-m3-user-setup | 13 ++++++++++--- .../mesa-m3/tests/test-system-provider.py | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD index 8b0f9912be7c36..bfae006a487842 100644 --- a/tools/aurora-sep/recipes/mesa-m3/PKGBUILD +++ b/tools/aurora-sep/recipes/mesa-m3/PKGBUILD @@ -91,7 +91,7 @@ sha256sums=('7ed24969985dce91f089d34dbe1c55cbcb8c95765e3e5bc0f268d27fbead2222' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - '263c17b51d750d3bee1329e2e57ab806ab06f652fd65580443c32e37c2114736' + '093a9029dbc6cb43e1c2997baa9c591f29df86d6b242f7f36dbf32add29ba103' '96c3101c15c73d26cd3086bc8ac4e8eb35d42300d495ca0ffd2bd03960ef3332' '9c7c3050d3ec15fce80a6e59d099567b396f1844a43695aa4377e02ac696f566' '2a15cfdbc160b78e5a3a8e99d5898d40a54db28592f1a4ce04a600990a731f87' diff --git a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup index 75c3ecc37809c6..f06c5d9f6312d9 100755 --- a/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup +++ b/tools/aurora-sep/recipes/mesa-m3/mesa-m3-user-setup @@ -391,7 +391,10 @@ class Finder: rest, at = '', 0 for m in re.finditer(ref, value): t, before, after = m.group(0), value[m.start() - 1:m.start()], value[m.end():m.end() + 1] - alone = (after in ('',) + sep) and (t in cond or before in ('',) + sep) + # A conditional append needs an entry before it, or it + # starts the value with an empty entry. + edge = ('',) + sep + alone = after in edge and ((before not in edge) if t in cond else (before in edge)) rest += value[at:m.start()] + ('' if alone and t in plain + cond else '\0') at = m.end() rest += value[at:] @@ -403,8 +406,12 @@ class Finder: if strict and comp == '': note(place, f'{label} leaves an empty entry in {name} (the current directory)', sure=False) continue - # glibc takes an entry as written, spaces included. - comp = self.expand_home(comp if strict else comp.strip()) + # glibc takes an entry as written, spaces included, and an + # environment value has no ~ or $HOME left to expand. + if not strict: + comp = self.expand_home(comp.strip()) + elif not place.startswith('environment:'): + comp = self.expand_home(comp) if comp == '': continue if '$' in comp or '`' in comp: diff --git a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py index 565f288e98fd6a..f66a74c4dfffad 100644 --- a/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -138,7 +138,21 @@ def test_reference_with_a_suffix_is_unknown(self): self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "/provider"}).returncode, 3) def test_environment_value_is_literal(self): - self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "$LD_LIBRARY_PATH"}).returncode, 3) + (self.root / "home/tester/provider").mkdir(parents=True) + (self.root / "home/tester/provider/libEGL.so.1").touch() + for value in ("$LD_LIBRARY_PATH", "$HOME/provider", "~/provider"): + with self.subTest(value=value): + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": value}).returncode, 3) + + def test_conditional_append_needs_an_entry_before_it(self): + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir(parents=True) + for line in ("export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\n", + "export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\n", + "export LD_LIBRARY_PATH=/x:${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\n"): + with self.subTest(line=line): + (folder / "env").write_text(line) + self.assertEqual(self.run_detector({"LD_LIBRARY_PATH": "/usr/lib/extra"}).returncode, 3) def test_legacy_ldpath_rules_are_unchanged(self): (self.root / "provider/mesa").mkdir(parents=True)