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..bfae006a487842 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=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') @@ -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' + '9cbc38f4bee27ab72d4a288f2741ab1eeb70112ca473a5c98a9695cdfcbe56f7' '6144af8b2e0f04ea615e8c4149e69c45ac49e630c46b2a58f0d1095d977798b2' '530948fc1a124de21c030a497e2ef5351742ba4ad4653484fe30fd1697ad7141' '644cf4a5ab57b7badec75843e2899b68d41490c4851d6885227180d6090c25ea' '9a26a232fd050b6207bd0d385035723f72ffd2b6b72a25d6ac92ae2f99ea5b66' '34f8bb3fa505718dab176b6283dde0d4de9cae4c3928922bca18e6052434bca7' - '7e7914877c5f1ccc85b985b071c838271608c78f858489d69b7a3ba64c4f51f0' - '8f19ebea2b8e669a764d7c4b40ecbc11e8234acfc3a2bfe7910e22de0b64a49d' + '093a9029dbc6cb43e1c2997baa9c591f29df86d6b242f7f36dbf32add29ba103' + '96c3101c15c73d26cd3086bc8ac4e8eb35d42300d495ca0ffd2bd03960ef3332' '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..a90c3b5d2bc143 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,13 @@ # 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). 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 # journal line (tag mesa-m3) with the same decision, reason and login identity, @@ -133,16 +140,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 +250,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 +320,147 @@ _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_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() { @@ -455,23 +562,44 @@ 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_bounded "${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 _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_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_self 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..f06c5d9f6312d9 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,12 @@ 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; + an LD_LIBRARY_PATH entry then also counts when it holds + 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 @@ -79,10 +86,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 +259,18 @@ 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. + # 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', + '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): return self.o['root'] + p @@ -297,11 +320,18 @@ 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) 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): @@ -335,15 +365,60 @@ 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}). - 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()) + n = re.escape(name) + strict = self.o['system_provider'] + if not strict: + # Drop the references to the previous value (also ${NAME:+:$NAME}). + 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, 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) + 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] + # 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:] + 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 == '': + note(place, f'{label} leaves an empty entry in {name} (the current directory)', sure=False) + continue + # 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: 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 new file mode 100644 index 00000000000000..c5b7c978049197 --- /dev/null +++ b/tools/aurora-sep/recipes/mesa-m3/tests/SYSTEM-HANDOFF.md @@ -0,0 +1,66 @@ +# 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. + +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, 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. 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. + +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. + +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 +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..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 @@ -665,5 +669,180 @@ 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" + # 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...] + _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 "$@" "$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:-}" + 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 +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" +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..f66a74c4dfffad --- /dev/null +++ b/tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py @@ -0,0 +1,199 @@ +#!/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_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_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_expands_like_the_shell(self): + folder = self.root / "home/tester/.config/uwsm" + folder.mkdir(parents=True) + (self.root / "provider/other").mkdir(parents=True) + 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" + 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): + (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_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.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) + (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): + 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() + (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() diff --git a/tools/aurora-sep/recipes/mesa-m3/user-setup.list b/tools/aurora-sep/recipes/mesa-m3/user-setup.list index 0bd73c8f394f6c..31f55e501f0885 100644 --- a/tools/aurora-sep/recipes/mesa-m3/user-setup.list +++ b/tools/aurora-sep/recipes/mesa-m3/user-setup.list @@ -15,6 +15,12 @@ # 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; +# entries split on ":" and ";" as glibc does, and a relative or +# 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