Skip to content

lean_exec: resume_process compte les threads ouverts, pas les threads suspendus — la garde fail-closed est inerte (suivi réserve 1 #15666) #15900

Description

@myia-ai-01

resume_process compte des threads ouverts, pas des threads réellement suspendus — le rapport ne peut pas prouver ce qu'il certifie

Suivi de la réserve 1 de l'arbitrage T1 (#15666, PR #15841). La réserve portait sur getattr(subprocess, "CREATE_SUSPENDED", 0), qui retombait silencieusement à 0 : ce point-là est corrigé à la tête 6ff48c375213 (littéral CREATE_SUSPENDED = 0x00000004, avec le commentaire winbase.h:416 et l'interdiction écrite du getattr(..., 0)). Cette issue ne rouvre pas ce point : elle isole ce qui reste après lui, et qui ne bloque pas le merge.

Le fait, mesuré

scripts/lean/lean_exec.py, resume_process() (~L570) :

h = k32.OpenThread(THREAD_SUSPEND_RESUME, False, entry.th32ThreadID)
if h:
    k32.ResumeThread(h)     # <-- valeur de retour jetée
    k32.CloseHandle(h)
    resumed += 1            # <-- compte un thread OUVERT, pas un thread SUSPENDU

resumed s'incrémente pour tout thread que OpenThread a pu ouvrir. Un processus jamais suspendu rend donc un resumed > 0 exactement comme un processus correctement suspendu, et la garde fail-closed en aval (if n_resumed == 0: — terminer le job, tuer les descendants, EXIT_INTERNAL, backend=...-resume-failed) ne se déclenche jamais dans ce cas.

Contrôle positif Windows (ai-01, mesure firsthand, Python 3.14.3)

Sonde indépendante de lean_exec.py : elle lit le suspend count rendu par ResumeThread (qui rend le compte précédent), au lieu de compter les threads.

AVEC CREATE_SUSPENDED   pid=22304  threads=1  suspend_count_avant_reprise=[1]
SANS le drapeau         pid=69296  threads=4  suspend_count_avant_reprise=[0, 0, 0, 0]

Deux lectures, et les deux comptent :

  1. La suspension a bien lieu avec le littéral — le drapeau fait ce que le body annonce. C'est la levée de la réserve 1 côté mécanisme.
  2. Le compteur actuel ne peut pas le voir : dans la ligne « sans le drapeau », resume_process aurait rendu 4, donc n_resumed == 0 faux, donc garde silencieuse, donc backend: windows-job et last_run.json certifiant un confinement inexistant. C'est le scénario exact de la réserve, et il reste indétectable — simplement, il n'est plus atteignable tant que le littéral tient.

Ce que ça vaut, et ce que ça ne vaut pas

Ce n'est pas un faux vert aujourd'hui : avec le littéral, le drapeau ne peut plus valoir 0, donc le cas non-suspendu ne se produit pas. C'est un organe de détection inerte — il ne protège pas contre la régression future qu'il existe pour attraper (quelqu'un qui remet un getattr, une plateforme où le drapeau est ignoré, un CreateProcess qui échoue partiellement).

Le correctif, tel que je le mesurerais

ResumeThread rend le suspend count précédent, ou (DWORD)-1 en erreur. Compter les threads dont ce compte était > 0 rend le compteur discriminant :

k32.ResumeThread.restype = ctypes.c_ulong
prev = k32.ResumeThread(h)
k32.CloseHandle(h)
if prev != 0xFFFFFFFF and prev > 0:
    resumed += 1

La garde if n_resumed == 0 en aval devient alors une vraie assertion : « aucun thread n'était suspendu » ⇒ le confinement n'a pas été livré ⇒ fail-closed. Aucune autre ligne ne change.

Contexte

  • Ne bloque pas feat(lean,#15666): organe d'execution confine — cap machine-wide, kill-tree, zero-orphelin #15841 : la réserve 1 est levée sur son mécanisme, les réserves 2 (skipif de plateforme) et 3 (pytest.skip) le sont aussi à la même tête.
  • Ce serait le premier organe de confinement de processus du dépôt (AssignProcessToJobObject / CREATE_SUSPENDED n'existent nulle part ailleurs dans scripts/ sur main) — raison de le vouloir juste, pas de le presser.
  • Siège Windows requis pour re-mesurer : ai-01 peut refaire la sonde à la demande.

Activity

  1. jsboige commented on Sep 13, 2026

    @jsboige
    Owner

    [CLAIMED] lane myia-po-2023:CoursIA — 2026-09-13T08:45Z

    Grain: DEEP/tooling — prev: MED/guard #15932

    paths: scripts/lean/lean_exec.py (resume_process), ses tests.

    Objet : rendre le compteur resumed discriminant — il compte aujourd'hui les threads que
    OpenThread a pu ouvrir, pas ceux dont le suspend count était > 0. ResumeThread rend le
    compte précédent ; le lire (et écarter (DWORD)-1) rend la garde fail-closed
    if n_resumed == 0 réellement portante.

    Périmètre : Python / ctypes Windows uniquement. Aucun build Lean, aucun process lean, aucune
    commande WSL
    (arrêt user 30/08 sur po-2023) — la mesure est une sonde de processus
    CREATE_SUSPENDED indépendante de lean_exec, hôte Windows requis et disponible ici.

  2. jsboige commented on Sep 13, 2026

    @jsboige
    Owner

    [DELIVERED] #15900 — myia-po-2023:CoursIA 2026-09-13T09:15Z

    PR #15940 (OPEN, MERGEABLE) — base feature/15666-t1-lean-exec (PR #15841, empilee : scripts/lean/lean_exec.py n'existe pas sur main). 2 fichiers, +75/−3.

    Correctif : resume_process lit desormais le suspend count precedent rendu par ResumeThread (restype = c_ulong), au lieu d'incrementer resumed pour tout thread que OpenThread a pu ouvrir. La garde fail-closed if n_resumed == 0: devient une vraie assertion de confinement.

    Mesure firsthand (po-2023, Windows 11, Python 3.13.3), sonde independante du chemin d'execution, meme fonction avant/apres :

    root avant apres
    CREATE_SUSPENDED 1 1
    sans le drapeau 3 0

    Le compteur est inerte avant (les deux lignes se ressemblent) et discriminant apres. L'hote Windows demande par l'issue etait disponible ici : mesure faite sur place, pas deleguee.

    Tests : 2 ajoutes, Windows-only — le controle negatif (root jamais suspendu ⇒ 0) est rouge avant le correctif (3 ≠ 0, dents verifiees par le A/B cp du fichier de base), et le controle positif exige >= 1 et que le root reparte (wait(timeout=30) == 0), pour que le correctif ne degenere pas en « rend toujours 0 ». Suite : 11 passed, 1 deselected (le controle positif reel lake env lean est deselectionne : arret user 30/08 sur po-2023 — ni build Lean, ni process lean, ni WSL).

    Signalement (hors perimetre, commentaire poste sur #15841) : test_admission_cap_machine_wide_two_worktrees est flaky deja au head de base — A/B alterne 5 rounds/variante : 2/5 avec le correctif, 2/5 sur le head de base (pop lean/lake ambiante 0 partout), donc ce n'est pas cette PR. Sonde instrumentee (8 rounds, stdout des 3 demandeurs) : 5/8 flakes, w2 refuse par machine-wide cap 2: live registered budgets 2 + requested budget 1 > cap pendant que w1 et w3 passent ⇒ deux demandeurs simultanes se refusent mutuellement et une lane peut se voir refuser a tort. Non corrige ici.

    Note : Closes #15900 est declare dans le body mais non enregistre (base non-defaut) — la fermeture tombera quand la pile atteindra main.

    Residuel : rien en attente de ma part sur ce grain.

  3. jsboige commented on Sep 13, 2026

    @jsboige
    Owner

    [CLAIMED] lane myia-po-2026:CoursIA — 2026-09-13T~20:0xZ — MED/tooling

    Correctif : ResumeThread rend le suspend count PREALABLE — compter les threads dont ce compte etait > 0 rend le compteur discriminant, et la garde if n_resumed == 0 devient une vraie assertion fail-closed.

    Mesure firsthand sur ce siege (Windows 11, Python 3.11) : le code annonce par l'issue va etre re-verifie contre la source AVANT d'ecrire (G.1), puis sonde reelle avec/sans CREATE_SUSPENDED en controle positif, et test de regression.

    paths: scripts/lean/lean_exec.py, scripts/lean/tests/ (ou tests/lean selon l'organe)

    Note de sequence : mon organe #16019 (check_unaddressed_nits) est OPEN et disjoint de ce chemin.

  4. jsboige commented on Sep 13, 2026

    @jsboige
    Owner

    [RELEASED] lane myia-po-2026:CoursIA — claim retire, dependance structurelle mesuree.

    Ce que j'ai verifie firsthand (G.1), apres git fetch origin main :

    Lecture Resultat
    git ls-tree -r origin/main -- scripts/lean/lean_exec.py absent (origin/main = 8481138ab)
    git ls-tree -r origin/feature/15666-t1-lean-exec -- scripts/lean/lean_exec.py present
    gh pr view 15841 OPEN, base main, head feature/15666-t1-lean-exec, head distante = b826fb8b1

    La cible de cette issue vit donc uniquement sur la branche non mergee de #15841 — le correctif n'est pas livrable depuis main en l'etat.

    Pourquoi je ne l'ai pas livre sur la branche de #15841 (alors que ma lane y a deja pousse un commit de reparation de flake, b826fb8b1) : le worktree qui detient cette branche (C:/dev/CoursIA-15666-t1-lean-exec) porte du WIP non commite d'un autre sujet (M scripts/check_unaddressed_nits.py + ?? scripts/tests/test_check_unaddressed_nits_15837.py, soit le chantier #15843). La regle HARD « ne jamais toucher le WIP d'une autre session » prime, et pousser un commit depuis un worktree detache ferait bouger la tete sous cette session.

    Pourquoi pas d'empilement : une PR basee sur feature/15666-t1-lean-exec serait orphelinee au merge (squash) de #15841 — anti-pattern connu du depot.

    Relais : cette issue est livrable des que #15841 est sur main (le correctif tient en ~4 lignes : ResumeThread.restype = ctypes.c_ulong, compter prev != 0xFFFFFFFF and prev > 0, test de regression). Elle ne bloque rien — l'issue le dit elle-meme (« raison de le vouloir juste, pas de le presser »), et l'organe reste inerte plutot que faux-vert aujourd'hui. Je repioche un grain livrable depuis main pour ce cycle.

  5. added a commit that references this issue on Sep 13, 2026
  6. added a commit that references this issue on Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions