test(mosaic): prepare isolated EGL FX probe without native execution

This commit is contained in:
fy59 2026-10-17 07:07:37 +02:00
parent ccd534132d
commit 99f1fbeb40
8 changed files with 1331 additions and 0 deletions

View file

@ -1,5 +1,10 @@
# Documentation
[Isolated EGL/FX probe preparation](design/STEP20C_HEADLESS_EGL_FX_PROBE.md)
is EXPERIMENTAL / EGL_FX_PROBE_READY_FOR_APPROVAL; its source-only fixture and
reported offline tests await independent validation. Runtime NOT_RUN; separate
conversational approval is required for the single native measurement.
[Gate 2 headless boot](design/STEP20C_HEADLESS_BOOT_GATE2.md) records the single
V5 native failure: EXPERIMENTAL / PRISTINE_ALLOCATOR_BLOCKED / CORE_NOT_RUN,
without private IPC or rendering proof. The [allocator triage](design/STEP20C_HEADLESS_ALLOCATOR_TRIAGE.md)

View file

@ -0,0 +1,140 @@
# STEP20C — isolated EGL/FX probe preparation
Status: **EXPERIMENTAL / EGL_FX_PROBE_READY_FOR_APPROVAL / runtime NOT_RUN**.
The source-only fixture prepares one future measurement, without authorizing execution.
STEP20C remains NEXT; production multi-output NOT_YET_SUPPORTED.
The [Gate 2 V5 failure](STEP20C_HEADLESS_BOOT_GATE2.md) remains
PRISTINE_ALLOCATOR_BLOCKED / CORE_NOT_RUN; V4 failed before compositor execution.
Historical V4/V5 LABs and verdicts are immutable.
## Measurement contract
Use real pinned SceneFX/wlroots objects without sway_server, compositor,
Wayland display/socket, client, IPC or output capture. A headless backend needs
only its own event loop; it need not be started or create outputs. Inspect
software EGL separately from compatibility of FX render targets. No physical
host device, seat, desktop socket, bus or host network may be exposed.
| Quantity | Prepared observation / explicit limit |
| --- | --- |
| EGL vendor, version, display extensions | Query the actual display owned by the FX path; do not infer from installed metadata |
| Selected software EGL device | Query the device associated with that display and require EGL_MESA_device_software; missing identity is refusal |
| DRM FD | Both backend and renderer getters must return exactly -1; otherwise refusal, no hardware fallback |
| Backend and renderer target caps | Read real headless buffer_caps and renderer render_buffer_caps |
| Texture support | Query format sets separately for DMABUF, DATA_PTR and SHM; no invented texture_caps field |
| Target formats | Query the real FX render format set, distinct from texture formats |
| Target allocation | Always NOT_ATTEMPTED; report pinned-source eligibility separately from allocation or rendering |
Only source-backed, existing interfaces may be used. Internal interfaces must
be identified as such and bound to the exact pinned pair; no public ABI change
or invented getter is permitted. The reviewed choice is explicit
`wlr_egl_create_with_drm_fd(-1)` followed by `fx_renderer_create_egl(egl)`.
These are the real operations of SceneFX's software creation path, accessed
through the exact internal headers, without reading either private struct.
This measures those constructors; it does not exercise the public
fx_renderer_create(backend) entrypoint or its backend-selection policy.
Require EGL_EXT_device_query or EGL_EXT_device_base and query EGL_DEVICE_EXT from the
created display before FX creation. Require EGL_MESA_device_software and reject
DRM device extensions before FX initialization: its DRM getter can otherwise
attempt to open a render node. The selected device identity is borrowed until
EGL destruction, as specified by
[EGL_EXT_device_query](https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_device_query.txt). Before FX construction succeeds the helper owns EGL; after
success the renderer owns it. Destroy renderer (or untransferred EGL), then
backend, then event loop. Format sets are borrowed until renderer destruction.
The declared wlr_fx_renderer_get_egl has no export and must not be used.
No allocator is called: eligibility from real caps plus pinned source is
reported separately from target allocation, which is NOT_ATTEMPTED. Without
UDMABUF and with DMABUF-only FX targets/FD -1, no pinned allocator route is
compatible under this confinement. No general impossibility theorem is claimed.
FX construction initializes its own internal resources; the helper calls no
buffer/framebuffer creation or draw command. No invented texture_caps field is
used. JSONL records EGL identity before FX construction, then formats, caps,
FDs and completion. Strings are bounded to 16384 bytes, sets to 256 formats,
each format to 64 modifiers, and the four sets to 4096 modifiers total.
Missing identity or overflow refuses rather than truncates. Exit 0 means only
completed measurement; exit 20 means refused/incomplete. Inaccessible required
ABI or a mandatory host device blocks preparation instead of weakening isolation.
## Source-only preparation and identity boundary
The [six-file fixture](../../tooling/probes/step20c-egl-fx/README.md) contains
probe.c, prepare.py, run.py, test_probe.py, test_probe_mocks.c and README.md.
Its README gives exact preparation and inert-test commands. prepare.py creates
a new private mode0700 LAB, extracts SceneFX and wlroots from pinned Git
objects, checks every extracted blob, compares compiled installed headers and
copies only generated wlroots config/version headers. The pins and public
source URLs are recorded in the [allocator triage](STEP20C_HEADLESS_ALLOCATOR_TRIAGE.md).
Compilation uses C17 syntax checking and linking, SceneFX before wlroots;
neither the target loader, ldd nor the real helper is executed.
The private manifest seals argv, environments, compile commands, cc -M inputs,
source/header/script/tool/helper hashes, ELF identities and DT_NEEDED closure.
The prepared manifest covers 755 files, 80 ELF identities, 17 compared installed
headers and 557 source blobs. Installed ELF bytes/Build IDs are pinned, not
reproduced from source; header/export agreement proves no binary source identity.
Mesa EGL, swrast, GBM dri and gallium providers are explicit closure seeds;
conditional runtime dlopen remains unobserved. The inspected ELF closure has
no RPATH/RUNPATH; prepare.py does not enforce a general RPATH rejection policy.
Licences remain in private source archives; no binary, raw log or private
manifest belongs in Git.
## Fixed confinement and unconsumed budget
run.command fixes prlimit then Bubblewrap with --unshare-user --unshare-all
--disable-userns --new-session --die-with-parent --clearenv, read-only /usr,
/bin → usr/bin and /lib,/lib64 → usr/lib, private /proc and /dev, tmpfs
/dev/shm, /tmp and /run, and only the new helper bound read-only at /probe.
The working directory is /tmp. No host HOME, /etc, /sys, /run/user, /dev/dri,
/dev/input, /dev/udmabuf, seat, desktop socket, bus or host network is exposed.
The supervisor environment is exactly PATH=/usr/bin and LC_ALL=C; the helper
adds PWD=/tmp, WLR_RENDERER_FORCE_SOFTWARE=1, WLR_RENDERER_ALLOW_SOFTWARE=1,
LIBGL_ALWAYS_SOFTWARE=1, LP_NUM_THREADS=2 and
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json.
This selects the Mesa ICD; software identity still requires the associated-device
query. No caller-supplied mount/environment or automatic alternative ICD is allowed.
Budget: one launch, no retry; 8 s payload plus 2 s cleanup reserve within a
nominal supervised 10 s total; 1 MiB each raw stdout/stderr; AS 1 GiB, CPU 5 s,
NOFILE 64, FSIZE 1 MiB, CORE 0 and NPROC 256. AS/CPU apply per process; NPROC
applies to the global UID, not an aggregate cgroup. The supervisor closes
inherited FDs, drains pipes concurrently, uses pidfd when available and cleans
only its own unreaped leader/process group; bwrap PID namespace exit terminates
its descendants. Unproven collection is failure. This provides no hostile
same-UID-peer guarantee. Receipts/raw channels stay bounded and private, with
atomic writes/fsync. No systemd scope, compositor, client, socket or capture.
## Preparation and approval boundary
Offline evidence: 23 Python tests and eight inert C-mock scenarios passed;
the real helper passed C17 syntax checking and linking without execution.
The C harness replaces all EGL/Wayland/wlroots calls and links only libc.
Independent validation confirms the sealed target manifest, all 755 file
identities, fixture/source correspondence, passive plan, C17 syntax and inert
C tests. Python tests were also independently exercised on identical fixtures
in a separate preparation; its different path-dependent manifest is not drift
of the target LAB. Historical V5's 101 checked files are unchanged.
The unique independent code/protocol review is PASS, with no BLOCKING or
NON_BLOCKING defect. Final preparation verdict:
**EGL_FX_PROBE_READY_FOR_APPROVAL / runtime NOT_RUN**.
No ATTEMPT exists and the single-launch budget remains unconsumed. This verdict
qualifies preparation only; software EGL/FX success remains unmeasured.
The default run.py entrypoint only verifies and prints a passive plan: no files
created, Bubblewrap or ELF launched. Copy the manifest digest from the reviewed
preparation; do not recompute it to accept drift. A future command has this shape:
```sh
/usr/bin/env -i PATH=/usr/bin LC_ALL=C /usr/bin/python3 -I \
/var/tmp/NEW_PRIVATE_LAB/fixture/run.py \
--lab /var/tmp/NEW_PRIVATE_LAB --manifest-sha256 REVIEWED_DIGEST \
--approve-native-probe
```
This is **not authorization**: a separate conversational approval is required.
The flag durably consumes ATTEMPT before spawn; any failure, including spawn
failure, forbids retry. Existing ATTEMPT also refuses the passive plan.
Invoking the real helper, even --version, remains forbidden in this preparation.
No device access, compositor, scope, live IPC, installation or production ABI
change is authorized. Runtime remains NOT_RUN.

View file

@ -0,0 +1,109 @@
# STEP20C — fixture source-only EGL/FX
**EXPERIMENTAL / runtime NOT_RUN.** Cette fixture prépare une unique mesure
future. Elle n'ajoute aucune dépendance de production, ne démarre aucun
compositeur et ne démontre ni allocation ni rendu. Le document de référence est
[la proposition de sonde](../../../docs/design/STEP20C_HEADLESS_EGL_FX_PROBE.md).
Les anciens LAB restent en lecture seule.
`probe.c` crée seulement un event loop, un backend headless non démarré et EGL
via le constructeur interne pinned `wlr_egl_create_with_drm_fd(-1)`. Il interroge
**le device associé à ce display**, exige `EGL_MESA_device_software` et refuse
les extensions DRM avant `fx_renderer_create_egl`. Le constructeur FX initialise
ses ressources internes ; la sonde n'appelle ni allocateur, ni création de
buffer/framebuffer, ni commande de dessin. Les formats de texture DMABUF,
DATA_PTR et SHM sont distincts des formats cibles. Les sets et FDs sont
empruntés ; EGL est détruit par le renderer seulement après transfert réussi.
Les chaînes sont limitées à 16384 octets, chaque set à 256 formats, chaque
format à 64 modifiers et leur total à 4096. Les identités absentes ou un
dépassement refusent la mesure ; aucune troncature n'est qualifiée de succès.
Sorties JSONL, étape EGL flushée avant FX, statut 0 pour une mesure terminée,
20 pour un refus ou une mesure incomplète. `target_allocation=NOT_ATTEMPTED`
reste vrai ; l'éligibilité conditionnelle des allocateurs pinned est une
interprétation de capacités, pas une allocation mesurée.
Préparation passive, sans exécuter le helper, dans un **nouveau** LAB privé :
```sh
python3 tooling/probes/step20c-egl-fx/prepare.py \
--lab /var/tmp/NOUVEAU_LAB_PRIVE \
--source-store /var/tmp/swayfx-mosaic-corec-v2-i516p6po/sources
```
Le script lit les objets git pinned SceneFX
`37ccd723bef49e6891156ffafce8f549f01446cc` et wlroots
`d783533489e1f75d6886c2ab5c5960090ef268f8`, extrait leurs archives dans le
nouveau LAB et vérifie chaque blob. Il copie les seuls headers générés de
l'installation et compare les headers publics compilés aux sources pinned.
Le contrôle syntaxique utilise C17 avant le link, SceneFX avant wlroots.
Le manifeste contient les commandes exactes, les dépendances `cc -M`, les
SHA256 des sources/headers/scripts/helper/outils, les identités ELF et la
fermeture `readelf` DT_NEEDED. Aucun `ldd`, loader ou helper n'est exécuté.
Les bibliothèques installées sont scellées par leurs bytes et Build IDs ; elles
ne sont **pas reproduites depuis les sources**, et la correspondance des
headers/exportations ne prouve pas leur provenance source. Les licences restent
dans les archives privées ; aucun binaire, log ou manifeste privé n'entre dans Git.
Les tests Python ne lancent que des processus Python inertes pour exercer les
pipes, le timeout et la collecte. Aucun test n'invoque bwrap ou le helper :
```sh
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover \
-s tooling/probes/step20c-egl-fx -p test_probe.py -v
```
Le harnais C substitue **tous** les appels EGL/Wayland/wlroots et inclut le
corps réel de `probe.c`. Ses huit scénarios discriminent le device sélectionné,
les extensions DRM, le transfert EGL, l'ordre de destruction et les formats.
Il lie uniquement libc, sans bibliothèque EGL/FX ; son exécution reste un test
inerte, distinct de l'exécution interdite du helper réel. Après préparation :
```sh
LAB=/var/tmp/NOUVEAU_LAB_PRIVE
cc -std=c17 -Wall -Wextra -Werror -DWLR_USE_UNSTABLE \
-I"$LAB/generated" -I"$LAB/sources/scenefx/include" \
-I"$LAB/sources/wlroots/include" \
$(pkg-config --cflags scenefx-0.5 wlroots-0.20 wayland-server egl glesv2 pixman-1 libdrm) \
tooling/probes/step20c-egl-fx/test_probe_mocks.c -o "$LAB/bin/inert-c-tests"
readelf -dW "$LAB/bin/inert-c-tests"
"$LAB/bin/inert-c-tests"
```
Le préparateur imprime le digest du manifeste et la commande exacte du **plan
passif**. Recopier ce digest depuis la préparation revue, sans le recalculer
au moment d'exécuter. Le plan ne crée aucun fichier et n'exécute aucun ELF. Un LAB dont `ATTEMPT`
existe refuse aussi le plan, car son budget est déjà consommé.
Après une **nouvelle autorisation conversationnelle**, la même commande avec
`--approve-native-probe` consomme durablement `ATTEMPT` avant spawn ; toute
erreur, tout timeout et même un échec de spawn interdisent un retry :
```sh
/usr/bin/env -i PATH=/usr/bin LC_ALL=C /usr/bin/python3 -I \
/var/tmp/NOUVEAU_LAB_PRIVE/fixture/run.py \
--lab /var/tmp/NOUVEAU_LAB_PRIVE --manifest-sha256 DIGEST_REVU \
--approve-native-probe
```
Le lanceur refuse tout argument de mount/env libre et tout drift des identités.
Le protocole bwrap exact est construit dans `run.command` et scellé. `/usr` est
RO ; seul le helper neuf est lié RO à `/probe`. `/proc`, `/dev`, `/dev/shm`,
`/tmp` et `/run` sont privés ; pas de HOME, `/etc`, `/sys`, réseau hôte,
socket/bus/session, `/dev/dri`, `/dev/input`, `/dev/udmabuf` ou FD hérité.
`PWD=/tmp` est explicite. L'ICD est restreint à Mesa par son JSON sous `/usr` ;
ce choix ne prouve pas l'identité logicielle, vérifiée séparément par EGL.
Mesa EGL, swrast, GBM dri et gallium plus leurs DT_NEEDED sont scellés ; des
chargements conditionnels runtime restent non observés jusqu'à l'unique essai.
Le budget est exactement un lancement, 8 s de payload et 2 s de réserve de
nettoyage dans un délai nominal supervisé de 10 s au total, 1 MiB par canal brut, AS 1 GiB, CPU 5 s,
NOFILE 64, FSIZE 1 MiB, CORE 0, NPROC 256. AS/CPU sont par processus et NPROC
s'applique à l'UID global ; ce n'est pas une limite agrégée cgroup. Le
superviseur crée sa session, ferme les FDs, lit les deux pipes concurremment,
utilise pidfd si disponible, tue seulement son groupe non récolté et récolte
son enfant. La fin du namespace PID bwrap termine ses descendants. Une
collecte non prouvée est un échec explicite ; aucun PID déjà récolté n'est tué.
Ce contrat ne prétend pas durcir le système contre un pair hostile du même UID,
ni retirer les capacités de diagnostic `/proc` déjà imposées au protocole.
Les canaux bruts et reçus bornés restent privés, avec écritures atomiques et
fsync ; aucun scope systemd n'est créé.

View file

@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Préparation passive STEP20C. Compile/inspecte, n'exécute jamais le helper."""
import argparse
import hashlib
import io
import json
import os
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import tarfile
from run import ENV, HOST_ENV, command, digest, durable
PINS = {
"scenefx": "37ccd723bef49e6891156ffafce8f549f01446cc",
"wlroots": "d783533489e1f75d6886c2ab5c5960090ef268f8",
}
FIXTURE = Path(__file__).resolve().parent
def capture(argv):
return subprocess.check_output(argv, env=HOST_ENV, stderr=subprocess.STDOUT)
def archive_sources(bare, pin, destination):
"""git archive lit seulement le dépôt historique ; vérifier chaque blob extrait."""
actual = capture(["/usr/bin/git", "--git-dir", str(bare), "rev-parse", pin + "^{commit}"]).decode().strip()
if actual != pin:
raise ValueError("commit différent du pin")
archive = capture(["/usr/bin/git", "--git-dir", str(bare), "archive", pin])
destination.mkdir()
with tarfile.open(fileobj=io.BytesIO(archive)) as stream:
stream.extractall(destination, filter="data")
tree = capture(["/usr/bin/git", "--git-dir", str(bare), "ls-tree", "-rz", pin])
count = 0
for record in tree.split(b"\0"):
if not record:
continue
metadata, raw_name = record.split(b"\t", 1)
mode, kind, oid = metadata.split()
if kind != b"blob":
raise ValueError("type git inattendu")
path = destination / os.fsdecode(raw_name)
content = os.readlink(path).encode() if mode == b"120000" else path.read_bytes()
identity = hashlib.sha1(b"blob " + str(len(content)).encode() + b"\0" + content).hexdigest()
if identity != oid.decode():
raise ValueError(f"blob différent : {path}")
count += 1
return {"commit": pin, "archive_sha256": hashlib.sha256(archive).hexdigest(),
"verified_blobs": count}
def resolve_library(name):
if "/" in name:
path = Path(name)
if path.is_absolute() and path.is_relative_to("/usr") and path.exists():
return path
raise ValueError(f"bibliothèque hors /usr : {name}")
path = Path("/usr/lib") / name
if not path.is_file():
raise ValueError(f"dépendance ELF introuvable : {name}")
return path
def elf_closure(seeds):
"""Fermeture DT_NEEDED passive ; aucune utilisation de ldd/loader."""
result = {}
pending = list(seeds)
seen = set()
while pending:
path = pending.pop()
if str(path) in result:
continue
data = capture(["/usr/bin/readelf", "-dW", str(path)]).decode()
notes = capture(["/usr/bin/readelf", "-nW", str(path)]).decode()
needed = re.findall(r"\(NEEDED\).*\[([^]]+)\]", data)
build_id = re.search(r"Build ID: ([0-9a-f]+)", notes)
result[str(path)] = {"needed": needed,
"build_id": build_id.group(1) if build_id else None}
canonical = path.resolve()
if canonical not in seen:
seen.add(canonical)
pending.extend(resolve_library(name) for name in needed)
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--lab", type=Path, required=True)
parser.add_argument("--source-store", type=Path, required=True)
args = parser.parse_args()
lab = args.lab
if not lab.is_absolute() or lab.resolve() != lab:
raise ValueError("LAB absolu sans symlink requis")
if lab.exists():
if lab.stat().st_uid != os.getuid() or lab.stat().st_mode & 0o777 != 0o700:
raise ValueError("LAB privé mode0700 requis")
if any(path.name != "evidence" for path in lab.iterdir()):
raise ValueError("LAB déjà préparé ; aucune réutilisation")
else:
lab.mkdir(mode=0o700)
for name in ("fixture", "sources", "generated", "bin", "evidence"):
(lab / name).mkdir(exist_ok=True)
for name in ("probe.c", "run.py", "prepare.py", "test_probe.py", "test_probe_mocks.c", "README.md"):
shutil.copyfile(FIXTURE / name, lab / "fixture" / name)
provenance = {}
for name, pin in PINS.items():
provenance[name] = archive_sources(args.source_store / (name + ".git"), pin,
lab / "sources" / name)
# Only generated config/version headers come from the installed build.
generated = lab / "generated/wlr"
generated.mkdir()
for name in ("config.h", "version.h"):
shutil.copyfile(Path("/usr/include/wlroots-0.20/wlr") / name, generated / name)
packages = ["scenefx-0.5", "wlroots-0.20", "wayland-server", "egl", "glesv2", "pixman-1", "libdrm"]
flags = shlex.split(capture(["/usr/bin/pkg-config", "--cflags", *packages]).decode())
libs = shlex.split(capture(["/usr/bin/pkg-config", "--libs", *packages]).decode())
if libs.index("-lscenefx-0.5") >= libs.index("-lwlroots-0.20"):
raise ValueError("ordre de liaison SceneFX puis wlroots requis")
base = ["/usr/bin/cc", "-std=c17", "-Wall", "-Wextra", "-Werror",
"-DWLR_USE_UNSTABLE", "-I" + str(lab / "generated"),
"-I" + str(lab / "sources/scenefx/include"),
"-I" + str(lab / "sources/wlroots/include"), *flags]
source = str(lab / "fixture/probe.c")
commands = [base + ["-fsyntax-only", source],
base + ["-M", "-MF", str(lab / "evidence/compiled.d"), source],
base + [source, "-Wl,-z,now", "-Wl,-z,relro", "-o", str(lab / "bin/probe"), *libs]]
for index, argv in enumerate(commands):
output = capture(argv)
durable(lab / "evidence" / f"compile-{index}.log", output)
deptext = (lab / "evidence/compiled.d").read_text().replace("\\\n", " ")
dependencies = [Path(name) for name in shlex.split(deptext.split(":", 1)[1])]
compared = []
for dependency in dependencies:
for project, installed in (("scenefx", "/usr/include/scenefx-0.5"),
("wlroots", "/usr/include/wlroots-0.20")):
root = lab / "sources" / project / "include"
if dependency.is_relative_to(root):
counterpart = Path(installed) / dependency.relative_to(root)
if counterpart.is_file():
if dependency.read_bytes() != counterpart.read_bytes():
raise ValueError(f"header installé différent du pin : {counterpart}")
compared.append(str(counterpart))
vendor_json = Path(ENV["__EGL_VENDOR_LIBRARY_FILENAMES"])
vendor_library = resolve_library(json.loads(vendor_json.read_text())["ICD"]["library_path"])
# Mesa uses dlopen as well as DT_NEEDED. Seal explicit software DRI,
# GBM plugin and gallium provider, then their entire DT_NEEDED closure.
seeds = [lab / "bin/probe", Path("/usr/bin/bwrap"), Path("/usr/bin/prlimit"),
Path("/usr/bin/python3"), Path("/usr/bin/env"), Path("/usr/lib/ld-linux-x86-64.so.2"),
vendor_library, Path("/usr/lib/dri/swrast_dri.so")]
seeds.extend(Path("/usr/lib").glob("libgallium-*.so"))
seeds.append(Path("/usr/lib/gbm/dri_gbm.so"))
closure = elf_closure(seeds)
# Symbols must be definitions in the expected ELF, not merely declarations.
symbols = {}
expected = {
"/usr/lib/libscenefx-0.5.so": ["wlr_egl_create_with_drm_fd", "wlr_egl_destroy",
"wlr_egl_get_display", "fx_renderer_create_egl"],
"/usr/lib/libwlroots-0.20.so": ["wlr_renderer_get_render_formats"],
}
for filename, names in expected.items():
table = capture(["/usr/bin/readelf", "--dyn-syms", "-W", filename]).decode()
for name in names:
if not any(line.split()[-1:] == [name] and " UND " not in line for line in table.splitlines()):
raise ValueError(f"symbole défini absent : {name}")
symbols[filename] = names
files = set(dependencies + [Path(name) for name in closure] + [vendor_json])
files.update(Path(name) for name in compared)
files.update(path for root in ("fixture", "sources", "generated")
for path in (lab / root).rglob("*") if path.is_file())
files.update(Path("/usr/bin") / name for name in ("cc", "pkg-config", "readelf", "git"))
files.update(Path("/usr/lib/pkgconfig") / (name + ".pc") for name in packages)
identities = {str(path): {"sha256": digest(path), "resolved": str(path.resolve())}
for path in sorted(files)}
manifest = {
"schema": 1, "lab": str(lab), "runtime": "NOT_RUN", "budget": 1,
"argv": command(lab), "environment": ENV, "supervisor_environment": HOST_ENV,
"pins": provenance, "files": identities, "elf_closure": closure,
"symbols": symbols, "compile_commands": commands,
"compared_installed_headers": compared,
"provenance_limit": "Installed ELF bytes are pinned, not reproduced from source. "
"Header equality and exported symbols do not prove binary source identity.",
"dlopen_limit": "Explicit Mesa EGL, swrast, GBM and gallium providers plus DT_NEEDED "
"are sealed; conditional runtime dlopen discovery remains NOT_RUN.",
}
durable(lab / "manifest.json", json.dumps(manifest, indent=2, sort_keys=True).encode() + b"\n")
seal = digest(lab / "manifest.json")
print(json.dumps({"runtime": "NOT_RUN", "manifest_sha256": seal, "plan_command":
["/usr/bin/env", "-i", "PATH=/usr/bin", "LC_ALL=C",
"/usr/bin/python3", "-I", str(lab / "fixture/run.py"), "--lab", str(lab),
"--manifest-sha256", seal]}, indent=2))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,253 @@
#define _POSIX_C_SOURCE 200809L
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wayland-server-core.h>
#include <wlr/backend/headless.h>
#include <wlr/render/drm_format_set.h>
#include <wlr/render/egl.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_buffer.h>
/* CONTRACT: existing pinned internal declarations, never private layouts. */
#include "render/egl.h"
#include "render/fx_renderer/fx_renderer.h"
#include "render/wlr_renderer.h"
extern char **environ;
static const char *const environment[] = {
"PATH=/usr/bin", "PWD=/tmp", "LC_ALL=C",
"WLR_RENDERER_FORCE_SOFTWARE=1", "WLR_RENDERER_ALLOW_SOFTWARE=1",
"LIBGL_ALWAYS_SOFTWARE=1", "LP_NUM_THREADS=2",
"__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json",
};
static bool guard_environment(void) {
size_t count = 0;
for (char **entry = environ; *entry != NULL; entry++) {
bool found = false;
for (size_t i = 0; i < sizeof(environment) / sizeof(environment[0]); i++) {
found |= strcmp(*entry, environment[i]) == 0;
}
if (!found) {
return false;
}
count++;
}
if (count != sizeof(environment) / sizeof(environment[0])) {
return false;
}
/* Duplicates cannot replace a required variable. */
for (size_t i = 0; i < sizeof(environment) / sizeof(environment[0]); i++) {
bool found = false;
for (char **entry = environ; *entry != NULL; entry++) {
found |= strcmp(*entry, environment[i]) == 0;
}
if (!found) {
return false;
}
}
return true;
}
static bool bounded(const char *value) {
return value != NULL && strnlen(value, 16385) <= 16384;
}
static bool extension(const char *list, const char *name) {
size_t length = strlen(name);
for (const char *at = list; (at = strstr(at, name)) != NULL; at += length) {
if ((at == list || at[-1] == ' ') &&
(at[length] == '\0' || at[length] == ' ')) {
return true;
}
}
return false;
}
static void json_string(const char *value) {
putchar('"');
for (const unsigned char *at = (const unsigned char *)value; *at; at++) {
if (*at == '"' || *at == '\\') {
printf("\\%c", *at);
} else if (*at < 32 || *at >= 127) {
printf("\\u%04x", *at);
} else {
putchar(*at);
}
}
putchar('"');
}
static bool formats_valid(const struct wlr_drm_format_set *set) {
/* NULL means this buffer capability is unsupported; sets remain borrowed. */
if (set == NULL) {
return true;
}
if (set->len > 256 || (set->len && set->formats == NULL)) {
return false;
}
for (size_t i = 0; i < set->len; i++) {
const struct wlr_drm_format *format = &set->formats[i];
if (format->len > 64 || (format->len && format->modifiers == NULL)) {
return false;
}
}
return true;
}
static void formats_print(const char *name, const struct wlr_drm_format_set *set) {
printf("{\"stage\":\"formats\",\"usage\":");
json_string(name);
printf(",\"supported\":%s,\"formats\":[", set ? "true" : "false");
for (size_t i = 0; set && i < set->len; i++) {
const struct wlr_drm_format *format = &set->formats[i];
printf("%s{\"fourcc\":%u,\"modifiers\":[", i ? "," : "", format->format);
for (size_t j = 0; j < format->len; j++) {
/* Strings preserve all 64 bits in JSON consumers. */
printf("%s\"%016llx\"", j ? "," : "",
(unsigned long long)format->modifiers[j]);
}
printf("]}");
}
puts("]}");
}
int main(int argc, char **argv) {
(void)argv;
int status = 20; /* 0: measurement complete; 20: refused/incomplete. */
const char *reason = "environnement_non_conforme";
struct wl_event_loop *loop = NULL;
struct wlr_backend *backend = NULL;
struct wlr_egl *egl = NULL;
struct wlr_renderer *renderer = NULL;
if (argc != 1 || !guard_environment()) {
goto cleanup;
}
reason = "creation_event_loop";
if ((loop = wl_event_loop_create()) == NULL) {
goto cleanup;
}
reason = "creation_backend";
if ((backend = wlr_headless_backend_create(loop)) == NULL) {
goto cleanup;
}
reason = "creation_egl_logiciel";
if ((egl = wlr_egl_create_with_drm_fd(-1)) == NULL) {
goto cleanup;
}
EGLDisplay display = wlr_egl_get_display(egl);
const char *client = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
const char *vendor = eglQueryString(display, EGL_VENDOR);
const char *version = eglQueryString(display, EGL_VERSION);
const char *extensions = eglQueryString(display, EGL_EXTENSIONS);
reason = "identite_egl_absente_ou_trop_longue";
if (display == EGL_NO_DISPLAY || !bounded(client) || !bounded(vendor) ||
!bounded(version) || !bounded(extensions) || !*vendor || !*version) {
goto cleanup;
}
reason = "device_query_absent";
if (!extension(client, "EGL_EXT_device_query") &&
!extension(client, "EGL_EXT_device_base")) {
goto cleanup;
}
PFNEGLQUERYDISPLAYATTRIBEXTPROC query_display =
(PFNEGLQUERYDISPLAYATTRIBEXTPROC)eglGetProcAddress("eglQueryDisplayAttribEXT");
PFNEGLQUERYDEVICESTRINGEXTPROC query_device =
(PFNEGLQUERYDEVICESTRINGEXTPROC)eglGetProcAddress("eglQueryDeviceStringEXT");
EGLAttrib device_attribute = 0;
reason = "device_selectionne_non_identifiable";
if (!query_display || !query_device ||
!query_display(display, EGL_DEVICE_EXT, &device_attribute) ||
device_attribute == 0) {
goto cleanup;
}
/* WHY: inspect the display's device, never an unrelated enumerated device. */
const char *device_extensions = query_device((EGLDeviceEXT)device_attribute,
EGL_EXTENSIONS);
if (!bounded(device_extensions)) {
goto cleanup;
}
bool software = extension(device_extensions, "EGL_MESA_device_software");
bool drm = extension(device_extensions, "EGL_EXT_device_drm") ||
extension(device_extensions, "EGL_EXT_device_drm_render_node");
printf("{\"stage\":\"egl_selected\",\"vendor\":"); json_string(vendor);
printf(",\"version\":"); json_string(version);
printf(",\"extensions\":"); json_string(extensions);
printf(",\"client_extensions\":"); json_string(client);
printf(",\"device_extensions\":"); json_string(device_extensions);
printf(",\"selected_device_software\":%s,\"selected_device_drm\":%s}\n",
software ? "true" : "false", drm ? "true" : "false");
/* Evidence survives a constructor failure or a supervisor timeout. */
if (fflush(stdout) != 0) {
reason = "sortie_egl";
goto cleanup;
}
reason = "device_non_logiciel_ou_drm";
if (!software || drm) {
goto cleanup;
}
reason = "creation_fx";
if ((renderer = fx_renderer_create_egl(egl)) == NULL) {
goto cleanup;
}
/* INVARIANT: success transfers ownership; destroy renderer OR EGL. */
egl = NULL;
int backend_fd = wlr_backend_get_drm_fd(backend);
int renderer_fd = wlr_renderer_get_drm_fd(renderer);
reason = "fd_drm_inattendu";
if (backend_fd != -1 || renderer_fd != -1) {
goto cleanup;
}
const struct wlr_drm_format_set *sets[] = {
wlr_renderer_get_render_formats(renderer),
wlr_renderer_get_texture_formats(renderer, WLR_BUFFER_CAP_DMABUF),
wlr_renderer_get_texture_formats(renderer, WLR_BUFFER_CAP_DATA_PTR),
wlr_renderer_get_texture_formats(renderer, WLR_BUFFER_CAP_SHM),
};
const char *names[] = {"render_target", "texture_dmabuf", "texture_data_ptr", "texture_shm"};
reason = "formats_hors_bornes";
size_t modifiers = 0;
for (size_t i = 0; i < 4; i++) {
if (!formats_valid(sets[i])) {
goto cleanup;
}
for (size_t j = 0; sets[i] && j < sets[i]->len; j++) {
modifiers += sets[i]->formats[j].len;
}
}
if (modifiers > 4096) {
goto cleanup;
}
for (size_t i = 0; i < 4; i++) {
formats_print(names[i], sets[i]);
}
bool blocked = renderer->render_buffer_caps == WLR_BUFFER_CAP_DMABUF;
printf("{\"stage\":\"measurement\",\"backend_drm_fd\":%d,"
"\"renderer_drm_fd\":%d,\"backend_buffer_caps\":%u,"
"\"renderer_render_buffer_caps\":%u,\"target_allocation\":\"NOT_ATTEMPTED\","
"\"allocator_eligibility\":\"%s\"}\n", backend_fd, renderer_fd,
backend->buffer_caps, renderer->render_buffer_caps,
blocked ? "PINNED_DMABUF_ROUTE_BLOCKED_WITHOUT_DRM_OR_UDMABUF" : "UNDETERMINED");
status = 0;
reason = "mesure_terminee_allocation_non_tentee";
cleanup:
if (renderer != NULL) {
wlr_renderer_destroy(renderer);
} else if (egl != NULL) {
wlr_egl_destroy(egl);
}
if (backend != NULL) {
wlr_backend_destroy(backend);
}
if (loop != NULL) {
wl_event_loop_destroy(loop);
}
printf("{\"stage\":\"completion\",\"status\":%d,\"reason\":", status);
json_string(reason);
puts("}");
return fflush(stdout) == 0 && !ferror(stdout) ? status : 20;
}

View file

@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""Sonde spécifique STEP20C : plan passif, puis au plus une tentative approuvée."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import selectors
import signal
import stat
import subprocess
import sys
import time
LIMIT = 1024 * 1024
ENV = {
"PATH": "/usr/bin", "PWD": "/tmp", "LC_ALL": "C",
"WLR_RENDERER_FORCE_SOFTWARE": "1", "WLR_RENDERER_ALLOW_SOFTWARE": "1",
"LIBGL_ALWAYS_SOFTWARE": "1", "LP_NUM_THREADS": "2",
"__EGL_VENDOR_LIBRARY_FILENAMES": "/usr/share/glvnd/egl_vendor.d/50_mesa.json",
}
HOST_ENV = {"PATH": "/usr/bin", "LC_ALL": "C"}
class Refusal(Exception):
"""Le contrat scellé n'est pas satisfait ; ne pas franchir la frontière native."""
def digest(path):
with open(path, "rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def command(lab):
"""Liste fermée : aucun mount, argument ou environnement fourni par l'appelant."""
argv = [
"/usr/bin/prlimit", "--as=1073741824", "--cpu=5", "--nofile=64",
"--fsize=1048576", "--core=0", "--nproc=256", "--",
"/usr/bin/bwrap", "--unshare-user", "--unshare-all", "--disable-userns",
"--new-session", "--die-with-parent", "--clearenv", "--ro-bind", "/usr", "/usr",
"--symlink", "usr/bin", "/bin", "--symlink", "usr/lib", "/lib",
"--symlink", "usr/lib", "/lib64", "--proc", "/proc", "--dev", "/dev",
"--tmpfs", "/dev/shm", "--tmpfs", "/tmp", "--tmpfs", "/run",
"--ro-bind", str(lab / "bin/probe"), "/probe", "--chdir", "/tmp",
]
for key, value in ENV.items():
argv.extend(["--setenv", key, value])
return argv + ["--", "/probe"]
def verify(lab, expected):
"""Lecture seule, y compris pour le plan ; le digest doit venir de la revue."""
if dict(os.environ) != HOST_ENV:
raise Refusal("environnement superviseur non conforme ; utiliser env -i")
if not lab.is_absolute() or lab.resolve() != lab:
raise Refusal("LAB absolu sans symlink requis")
info = lab.stat()
if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700:
raise Refusal("LAB privé mode0700 requis")
manifest_path = lab / "manifest.json"
if digest(manifest_path) != expected:
raise Refusal("digest du manifeste différent de l'approbation")
manifest = json.loads(manifest_path.read_text())
if manifest["schema"] != 1 or manifest["lab"] != str(lab):
raise Refusal("identité du manifeste")
if manifest["argv"] != command(lab) or manifest["environment"] != ENV:
raise Refusal("montages, devices ou environnement altérés")
if manifest["supervisor_environment"] != HOST_ENV:
raise Refusal("environnement du superviseur altéré")
required = {str(lab / "bin/probe"), str(lab / "fixture/run.py"),
"/usr/bin/bwrap", "/usr/bin/prlimit"}
if not required.issubset(manifest["files"]):
raise Refusal("identités requises absentes")
if Path(__file__).resolve() != lab / "fixture/run.py":
raise Refusal("utiliser le lanceur scellé du LAB")
for name, identity in manifest["files"].items():
path = Path(name)
if not (path.is_relative_to(lab) or path.is_relative_to("/usr")):
raise Refusal("dépendance hors LAB neuf ou /usr")
if digest(path) != identity["sha256"]:
raise Refusal(f"dérive identité : {path}")
if str(path.resolve()) != identity["resolved"]:
raise Refusal(f"dérive symlink : {path}")
return manifest
def durable(path, data, exclusive=False):
"""Reçu borné et durable ; O_EXCL rend le budget irréversible avant spawn."""
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
destination = path if exclusive else path.with_suffix(path.suffix + ".tmp")
fd = os.open(destination, flags, 0o600)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
if not exclusive:
os.replace(destination, path)
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
except BaseException:
# A partial ATTEMPT still consumes the budget; do not remove it.
raise
def supervise(argv, payload_seconds=8.0, total_seconds=10.0):
"""Deux pipes bornés, collecte du seul groupe possédé, aucun retry.
bwrap conserve le PID namespace jusqu'à la fin du payload ; sa disparition
termine les descendants de ce namespace. Pas de kill d'un PID déjà récolté.
Les limites AS/CPU sont par processus ; NPROC porte sur l'UID, pas un cgroup.
"""
started = time.monotonic()
channels = [bytearray(), bytearray()]
cause = "exit"
diagnostic = None
process = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=HOST_ENV, close_fds=True,
start_new_session=True)
pidfd = None
try:
if hasattr(os, "pidfd_open"):
try:
pidfd = os.pidfd_open(process.pid)
except OSError:
# Popen remains unreaped and owns this PID during cleanup.
pidfd = None
with selectors.DefaultSelector() as selector:
for index, stream in enumerate((process.stdout, process.stderr)):
os.set_blocking(stream.fileno(), False)
selector.register(stream, selectors.EVENT_READ, index)
killed = False
while selector.get_map():
elapsed = time.monotonic() - started
if not killed and elapsed >= payload_seconds:
cause = "timeout"
if not killed and cause != "exit":
# Do not poll/reap before killpg: an unreaped child prevents
# PID reuse, even if the child exited between these steps.
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass # Already exited, still explicitly collected below.
killed = True
if elapsed >= total_seconds:
cause = "cleanup_unproven"
break
for key, _ in selector.select(min(0.05, total_seconds - elapsed)):
chunk = os.read(key.fileobj.fileno(), 65536)
if not chunk:
selector.unregister(key.fileobj)
continue
target = channels[key.data]
room = LIMIT - len(target)
target.extend(chunk[:room])
if len(chunk) > room and cause == "exit":
cause = "output_overflow"
elapsed = time.monotonic() - started
wait_budget = total_seconds if killed else payload_seconds
try:
code = process.wait(timeout=max(0.001, wait_budget - elapsed))
except subprocess.TimeoutExpired:
# Pipes can close while the leader still runs. Preserve the
# cleanup reserve instead of spending it waiting for payload.
if cause == "exit":
cause = "timeout"
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
code = process.wait(timeout=max(0.001, total_seconds -
(time.monotonic() - started)))
except subprocess.TimeoutExpired:
code = None
cause = "cleanup_unproven"
except BaseException as error:
# Popen has not been reaped on exceptional drain paths. Never abandon
# a child because selector/read/collection itself failed.
cause = "supervisor_failure"
diagnostic = f"{type(error).__name__}: {error}"[:512]
code = process.returncode
if code is None:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
code = process.wait(timeout=max(0.001, total_seconds -
(time.monotonic() - started)))
except subprocess.TimeoutExpired:
code = None
cause = "cleanup_unproven"
if not isinstance(error, Exception):
# Interruption is recorded after cleanup, not allowed to lose the
# irreversible attempt or raw evidence.
cause = "interrupted" if code is not None else "cleanup_unproven"
finally:
process.stdout.close()
process.stderr.close()
if pidfd is not None:
os.close(pidfd)
return {"cause": cause, "returncode": code, "error": diagnostic,
"collected": code is not None, "elapsed": time.monotonic() - started}, channels
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--lab", required=True, type=Path)
parser.add_argument("--manifest-sha256", required=True)
parser.add_argument("--approve-native-probe", action="store_true")
args = parser.parse_args()
try:
manifest = verify(args.lab, args.manifest_sha256)
if (args.lab / "ATTEMPT").exists():
raise Refusal("budget déjà consommé ; aucun retry")
if not args.approve_native_probe:
print(json.dumps({"runtime": "NOT_RUN", "budget": 1,
"argv": manifest["argv"], "environment": ENV}, indent=2))
return 0
durable(args.lab / "ATTEMPT", json.dumps({
"manifest_sha256": args.manifest_sha256, "budget_consumed": 1,
}).encode() + b"\n", exclusive=True)
try:
result, channels = supervise(manifest["argv"])
except OSError as error:
result = {"cause": "spawn_failure", "error": str(error), "collected": True}
channels = [b"", b""]
for name, data in zip(("stdout.raw", "stderr.raw"), channels):
durable(args.lab / name, data)
durable(args.lab / "result.json", json.dumps(result, sort_keys=True).encode() + b"\n")
print(json.dumps(result))
return 0 if result.get("cause") == "exit" and result.get("returncode") == 0 else 20
except (Refusal, OSError, ValueError, KeyError, TypeError) as error:
print(f"Refus : {error}", file=sys.stderr)
return 20
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,225 @@
"""Tests offline uniquement ; aucun bwrap, aucun helper EGL/FX exécuté."""
import contextlib
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
import run
class ProtocolTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.lab = Path(self.temporary.name)
self.lab.chmod(0o700)
(self.lab / "bin").mkdir()
(self.lab / "fixture").mkdir()
(self.lab / "bin/probe").write_bytes(b"INERT-NOT-ELF")
(self.lab / "fixture/run.py").write_text("# inert\n")
self.files = [self.lab / "bin/probe", self.lab / "fixture/run.py",
Path("/usr/bin/bwrap"), Path("/usr/bin/prlimit")]
self.manifest = {
"schema": 1, "lab": str(self.lab), "argv": run.command(self.lab),
"environment": run.ENV, "supervisor_environment": run.HOST_ENV,
"files": {str(path): {"sha256": run.digest(path), "resolved": str(path.resolve())}
for path in self.files},
}
def seal(self):
(self.lab / "manifest.json").write_text(json.dumps(self.manifest))
return run.digest(self.lab / "manifest.json")
def verify(self, seal=None):
if seal is None:
seal = self.seal()
with patch.object(run, "__file__", str(self.lab / "fixture/run.py")), \
patch.dict(os.environ, run.HOST_ENV, clear=True):
return run.verify(self.lab, seal)
def test_valid_identity(self):
self.assertEqual(self.verify()["schema"], 1)
def test_manifest_drift(self):
seal = self.seal()
(self.lab / "manifest.json").write_text("{}")
with self.assertRaises(run.Refusal):
self.verify(seal)
def test_binary_drift(self):
seal = self.seal()
(self.lab / "bin/probe").write_bytes(b"CHANGED")
with self.assertRaises(run.Refusal):
self.verify(seal)
def test_script_identity_drift(self):
seal = self.seal()
(self.lab / "fixture/run.py").write_text("# changed\n")
with self.assertRaises(run.Refusal):
self.verify(seal)
def test_forbidden_mount_and_devices(self):
for device in ("/dev/dri", "/dev/input", "/dev/udmabuf", "/etc", "/sys", "/run/user"):
with self.subTest(device=device):
self.manifest["argv"] = run.command(self.lab) + ["--ro-bind", device, device]
with self.assertRaises(run.Refusal):
self.verify()
def test_environment_pwd_and_extra(self):
for altered in ({**run.ENV, "PWD": "/"}, {**run.ENV, "HOME": "/home/host"}):
self.manifest["environment"] = altered
with self.assertRaises(run.Refusal):
self.verify()
def test_supervisor_inherited_environment(self):
seal = self.seal()
with patch.dict(os.environ, {**run.HOST_ENV, "LD_PRELOAD": "bad"}, clear=True):
with self.assertRaises(run.Refusal):
run.verify(self.lab, seal)
def test_wrong_launcher_identity(self):
seal = self.seal()
with patch.dict(os.environ, run.HOST_ENV, clear=True):
with self.assertRaises(run.Refusal):
run.verify(self.lab, seal)
def test_private_mode(self):
self.lab.chmod(0o755)
with self.assertRaises(run.Refusal):
self.verify()
def test_budget_exclusive_no_retry(self):
run.durable(self.lab / "ATTEMPT", b"one", exclusive=True)
with self.assertRaises(FileExistsError):
run.durable(self.lab / "ATTEMPT", b"two", exclusive=True)
self.assertEqual((self.lab / "ATTEMPT").read_bytes(), b"one")
def test_default_plan_has_no_write_or_spawn(self):
before = sorted(self.lab.rglob("*"))
with patch.object(sys, "argv", ["run.py", "--lab", str(self.lab), "--manifest-sha256", "x"]), \
patch.object(run, "verify", return_value=self.manifest), \
patch.object(run, "durable", side_effect=AssertionError("write")), \
patch.object(subprocess, "Popen", side_effect=AssertionError("native boundary")), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.main(), 0)
self.assertEqual(sorted(self.lab.rglob("*")), before)
def test_invalid_identity_stops_before_native_boundary(self):
with patch.object(sys, "argv", ["run.py", "--lab", str(self.lab), "--manifest-sha256", "bad",
"--approve-native-probe"]), \
patch.object(run, "supervise", side_effect=AssertionError("native boundary")), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(run.main(), 20)
self.assertFalse((self.lab / "ATTEMPT").exists())
def test_consumed_budget_refuses_passive_plan(self):
run.durable(self.lab / "ATTEMPT", b"one", exclusive=True)
with patch.object(sys, "argv", ["run.py", "--lab", str(self.lab), "--manifest-sha256", "x"]), \
patch.object(run, "verify", return_value=self.manifest), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(run.main(), 20)
def test_spawn_failure_consumes_budget(self):
arguments = ["run.py", "--lab", str(self.lab), "--manifest-sha256", "test", "--approve-native-probe"]
with patch.object(sys, "argv", arguments), \
patch.object(run, "verify", return_value=self.manifest), \
patch.object(run, "supervise", side_effect=OSError("mock spawn failure")) as native, \
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(run.main(), 20)
self.assertEqual(run.main(), 20)
self.assertEqual(native.call_count, 1)
self.assertEqual(json.loads((self.lab / "result.json").read_text())["cause"], "spawn_failure")
class DrainTests(unittest.TestCase):
def fake_python(self, script, **kwargs):
return run.supervise([sys.executable, "-I", "-c", script], **kwargs)
def test_exit_and_channels(self):
result, channels = self.fake_python("import os; os.write(1,b'out'); os.write(2,b'err')")
self.assertEqual(result["returncode"], 0)
self.assertTrue(result["collected"])
self.assertEqual(channels, [b"out", b"err"])
def test_nonzero_exit(self):
result, _ = self.fake_python("raise SystemExit(7)")
self.assertEqual(result["returncode"], 7)
self.assertEqual(result["cause"], "exit")
def test_exact_output_bound(self):
result, channels = self.fake_python(f"import os; os.write(1,b'x'*{run.LIMIT}); os.write(2,b'y'*{run.LIMIT})")
self.assertEqual(result["cause"], "exit")
self.assertEqual(list(map(len, channels)), [run.LIMIT, run.LIMIT])
def test_output_overflow(self):
result, channels = self.fake_python(f"import os; os.write(1,b'x'*{run.LIMIT + 1})")
self.assertEqual(result["cause"], "output_overflow")
self.assertEqual(len(channels[0]), run.LIMIT)
self.assertTrue(result["collected"])
def test_timeout_collection(self):
result, _ = self.fake_python("import time; time.sleep(30)", payload_seconds=0.1, total_seconds=0.8)
self.assertEqual(result["cause"], "timeout")
self.assertTrue(result["collected"])
self.assertLess(result["elapsed"], 1.0)
def test_closed_pipes_preserve_cleanup_reserve(self):
result, _ = self.fake_python("import os,time; os.close(1); os.close(2); time.sleep(30)",
payload_seconds=0.1, total_seconds=0.8)
self.assertEqual(result["cause"], "timeout")
self.assertTrue(result["collected"])
self.assertLess(result["elapsed"], 0.6)
def test_read_failure_collects_owned_child(self):
real_popen = subprocess.Popen
children = []
def remember(*args, **kwargs):
child = real_popen(*args, **kwargs)
children.append(child)
return child
real_read = os.read
def fail_drain(fd, size):
if size == 65536:
raise OSError("mock read failure")
return real_read(fd, size)
with patch.object(run.subprocess, "Popen", side_effect=remember), \
patch.object(run.os, "read", side_effect=fail_drain):
result, _ = self.fake_python("import os,time; os.write(1,b'x'); time.sleep(30)")
self.assertEqual(result["cause"], "supervisor_failure")
self.assertIsNotNone(children[0].returncode)
def test_collection_failure_explicit(self):
real_wait = subprocess.Popen.wait
def delayed_wait(child, *args, **kwargs):
real_wait(child, timeout=1)
raise subprocess.TimeoutExpired("mock collection", 0)
with patch.object(subprocess.Popen, "wait", delayed_wait):
result, _ = self.fake_python("pass", payload_seconds=0.05, total_seconds=0.2)
self.assertEqual(result["cause"], "cleanup_unproven")
self.assertFalse(result["collected"])
class SourceTests(unittest.TestCase):
def test_device_guard_precedes_fx_and_ownership(self):
source = Path(__file__).with_name("probe.c").read_text()
self.assertLess(source.index("query_display(display, EGL_DEVICE_EXT"),
source.index("renderer = fx_renderer_create_egl(egl)"))
self.assertLess(source.index("if (!software || drm)"), source.index("renderer = fx_renderer_create_egl(egl)"))
self.assertIn("} else if (egl != NULL)", source)
self.assertIn("egl = NULL;", source)
for forbidden in ("eglQueryDevicesEXT(", "wlr_fx_renderer_get_egl(", "wl_display_create(",
"wlr_backend_start(", "wlr_headless_add_output(", "wlr_allocator_autocreate(",
"wlr_buffer_create(", "glDraw", "->texture_caps", "->egl", "->display"):
self.assertNotIn(forbidden, source)
self.assertIn('"PWD=/tmp"', source)
self.assertIn("modifiers > 4096", source)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,158 @@
/* Offline harness: every native EGL/Wayland/wlroots entrypoint is replaced.
* This executable is NOT the real probe and links no EGL/FX/wlroots library. */
#define main probe_main
#define environ mock_environ
#define wl_event_loop_create mock_loop_create
#define wl_event_loop_destroy mock_loop_destroy
#define wlr_headless_backend_create mock_backend_create
#define wlr_backend_destroy mock_backend_destroy
#define wlr_backend_get_drm_fd mock_backend_fd
#define wlr_egl_create_with_drm_fd mock_egl_create
#define wlr_egl_destroy mock_egl_destroy
#define wlr_egl_get_display mock_egl_display
#define eglQueryString mock_query_string
#define eglGetProcAddress mock_get_proc
#define fx_renderer_create_egl mock_fx_create
#define wlr_renderer_destroy mock_renderer_destroy
#define wlr_renderer_get_drm_fd mock_renderer_fd
#define wlr_renderer_get_render_formats mock_render_formats
#define wlr_renderer_get_texture_formats mock_texture_formats
#include "probe.c"
#undef main
#include <assert.h>
static int mode, fx_calls, egl_destroyed, renderer_destroyed, order;
static unsigned texture_queries;
static struct wlr_backend backend_object;
static struct wlr_renderer renderer_object;
static struct wlr_drm_format format_object = {.format = 875713112};
static struct wlr_drm_format_set format_set = {.len = 1, .formats = &format_object};
static struct wlr_drm_format_set excessive_set = {.len = 257};
static const char *device_extensions;
static char *mock_environment[] = {
"PATH=/usr/bin", "PWD=/tmp", "LC_ALL=C", "WLR_RENDERER_FORCE_SOFTWARE=1",
"WLR_RENDERER_ALLOW_SOFTWARE=1", "LIBGL_ALWAYS_SOFTWARE=1", "LP_NUM_THREADS=2",
"__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json", NULL,
};
char **mock_environ = mock_environment;
struct wl_event_loop *mock_loop_create(void) {
return (struct wl_event_loop *)(uintptr_t)1;
}
void mock_loop_destroy(struct wl_event_loop *loop) {
assert(loop == (struct wl_event_loop *)(uintptr_t)1 && order == 2);
order = 3;
}
struct wlr_backend *mock_backend_create(struct wl_event_loop *loop) {
assert(loop == (struct wl_event_loop *)(uintptr_t)1);
return &backend_object;
}
void mock_backend_destroy(struct wlr_backend *backend) {
assert(backend == &backend_object && order == 1);
order = 2;
}
int mock_backend_fd(struct wlr_backend *backend) {
assert(backend == &backend_object);
return -1;
}
struct wlr_egl *mock_egl_create(int fd) {
assert(fd == -1);
return (struct wlr_egl *)(uintptr_t)2;
}
void mock_egl_destroy(struct wlr_egl *egl) {
assert(egl == (struct wlr_egl *)(uintptr_t)2 && order == 0);
assert(renderer_destroyed == 0);
egl_destroyed++;
order = 1;
}
EGLDisplay mock_egl_display(struct wlr_egl *egl) {
assert(egl == (struct wlr_egl *)(uintptr_t)2);
return (EGLDisplay)(uintptr_t)3;
}
const char *mock_query_string(EGLDisplay display, EGLint name) {
if (display == EGL_NO_DISPLAY) {
return "EGL_EXT_device_query";
}
assert(display == (EGLDisplay)(uintptr_t)3);
return name == EGL_EXTENSIONS ? "EGL_EXT_image_dma_buf_import" : "mock";
}
static EGLBoolean mock_query_display(EGLDisplay display, EGLint attribute, EGLAttrib *value) {
assert(display == (EGLDisplay)(uintptr_t)3 && attribute == EGL_DEVICE_EXT);
*value = 4;
return mode != 5;
}
static const char *mock_query_device(EGLDeviceEXT device, EGLint name) {
/* An arbitrary enumerated device would violate this assertion. */
assert(device == (EGLDeviceEXT)(uintptr_t)4 && name == EGL_EXTENSIONS);
return device_extensions;
}
__eglMustCastToProperFunctionPointerType mock_get_proc(const char *name) {
if (strcmp(name, "eglQueryDisplayAttribEXT") == 0) {
return (__eglMustCastToProperFunctionPointerType)mock_query_display;
}
assert(strcmp(name, "eglQueryDeviceStringEXT") == 0);
return (__eglMustCastToProperFunctionPointerType)mock_query_device;
}
struct wlr_renderer *mock_fx_create(struct wlr_egl *egl) {
assert(egl == (struct wlr_egl *)(uintptr_t)2);
fx_calls++;
return mode == 1 ? NULL : &renderer_object;
}
void mock_renderer_destroy(struct wlr_renderer *renderer) {
assert(renderer == &renderer_object && order == 0 && egl_destroyed == 0);
renderer_destroyed++;
/* Mock renderer consumes the transferred EGL, without a second helper free. */
order = 1;
}
int mock_renderer_fd(struct wlr_renderer *renderer) {
assert(renderer == &renderer_object);
return mode == 7 ? 42 : -1;
}
const struct wlr_drm_format_set *mock_render_formats(struct wlr_renderer *renderer) {
assert(renderer == &renderer_object);
return mode == 6 ? &excessive_set : &format_set;
}
const struct wlr_drm_format_set *mock_texture_formats(struct wlr_renderer *renderer,
uint32_t caps) {
assert(renderer == &renderer_object);
assert(caps == WLR_BUFFER_CAP_DMABUF || caps == WLR_BUFFER_CAP_DATA_PTR ||
caps == WLR_BUFFER_CAP_SHM);
assert((texture_queries & caps) == 0);
texture_queries |= caps;
return caps == WLR_BUFFER_CAP_DMABUF ? &format_set : NULL;
}
int main(void) {
/* Keep bounded test output separate from simulated JSONL evidence. */
assert(freopen("/dev/null", "w", stdout) != NULL);
for (mode = 0; mode < 8; mode++) {
fx_calls = egl_destroyed = renderer_destroyed = order = 0;
texture_queries = 0;
device_extensions = "EGL_MESA_device_software";
if (mode == 2) {
device_extensions = "EGL_EXT_device_drm";
} else if (mode == 3) {
device_extensions = "EGL_MESA_device_software EGL_EXT_device_drm_render_node";
} else if (mode == 4) {
device_extensions = "EGL_MESA_device_software_suffix";
}
renderer_object.render_buffer_caps = WLR_BUFFER_CAP_DMABUF;
char *argv[] = {"inert-mock", NULL};
assert(probe_main(1, argv) == (mode == 0 ? 0 : 20));
assert(order == 3);
if (mode >= 2 && mode <= 5) {
assert(fx_calls == 0 && egl_destroyed == 1 && renderer_destroyed == 0);
} else if (mode == 1) {
assert(fx_calls == 1 && egl_destroyed == 1 && renderer_destroyed == 0);
} else {
assert(fx_calls == 1 && egl_destroyed == 0 && renderer_destroyed == 1);
}
if (mode == 0 || mode == 6) {
assert(texture_queries == (WLR_BUFFER_CAP_DMABUF | WLR_BUFFER_CAP_DATA_PTR |
WLR_BUFFER_CAP_SHM));
}
}
fputs("8 mock C scenarios passed; no EGL/FX library linked\n", stderr);
return 0;
}