Skip to content

fix(tools): patch.sh 2b-ter greps text its patch adds; a gate holds every guard to its patch - #279

Merged
angeloINTJ merged 1 commit into
mainfrom
fix/patch-sh-2b-ter-sentinel
Oct 16, 2026
Merged

angeloINTJ merged 1 commit into
mainfrom
fix/patch-sh-2b-ter-sentinel

Conversation

@angeloINTJ

Copy link
Copy Markdown
Owner

Step 2b-ter of patch.sh does not recognise its own patch, so the maintainer's next run stops there. This fixes the sentinel and adds a gate that holds every guard to its patch.

The defect

patch.sh guards each framework override with grep -q "<sentinel>", so that a second run skips what is applied. Step 2b-ter (#274, merged 2026-10-15) greps SIMUT override — drop a non-TLS client. wifiserversecure_drop_plaintext.patch adds SIMUT override — tell a non-TLS client from a TLS one by its FIRST BYTE. The first run applies the patch, so nothing failed. On a core that already carries it, as the maintainer's has since 2026-10-15, the grep finds nothing and patch -p1 runs on a patched file.

Reproduction (2026-10-16, patch.sh of 2d6650b)

On isolated cores: cp -al ~/.platformio, the two state files and lwipopts.h copied out of the hard links first, PLATFORMIO_CORE_DIR set. patch.sh was never run on ~/.platformio, and it was not written: the sha256 of its framework's (inode, size, mtime, path) list and of the two state files are equal before and after, and the target file keeps its inode and sha256 (patch writes a new inode in the copy, the original stays).

Run (unfixed script) Exit Steps that said "nada a fazer" DONE At 2b-ter
Maintainer's core, once, no tty 1 1 of 17 no Reversed (or previously applied) patch detected! Assume -R? [n], Apply anyway? [n], Skipping patch., 1 out of 1 hunk ignored -- saving rejects to file libraries/WiFi/src/WiFiServerSecureBearSSL.cpp.rej
2b-ter undone with patch -R, run 1 0 16 yes applies; the file's sha256 is the maintainer's
same core, run 2 1 1 of 17 no as above
Maintainer's core, on a tty, Enter at both prompts 1 1 of 17 no waits at both prompts, then as above
Maintainer's core, on a tty, y at the first 0 16 yes the override is reversed: its marker count goes 1 to 0

In words: patch prompts, rejects the hunk to a .rej file next to the target (the target is untouched) and exits 1, and set -e ends patch.sh on that line. Steps 2b-bis to 2n did not run, nor did step 3, the cache invalidation that makes the next pio run rebuild after an override merges. An override step added after 2b-ter would not have been applied by the maintainer's next run, and exit status 1 would be the only sign. On a tty, y at Assume -R? takes the override out of the core and the script finishes with exit 0.

A dry run on the maintainer's core with patch stubbed out lists the steps that would call it: one, 2b-ter. The other sixteen sentinels match their patches.

The fix

One string in tools/arduino_pico_overrides/patch.sh: the sentinel is now SIMUT override — tell a non-TLS client from a TLS one by its FIRST BYTE, the head of the line the patch adds. A comment above step 2b and arduino_pico_overrides/README.md (where it says patch.sh is idempotent) now say what a sentinel has to be.

The same runs on fresh isolated cores:

  • the maintainer's core, once: exit 0, 17 of 17 "nada a fazer", nothing applied, DONE;
  • 2b-ter undone, then twice: run 1 applies only 2b-ter (same sha256 as the unfixed script's run 1 and the maintainer's file), run 2 exits 0 with 17 of 17 "nada a fazer";
  • on a tty: no prompt.

Every step's skip message ends "— nada a fazer"; the words "já aplicada" are 2b-ter's own.

The gate

tools/check_patch_sentinels.py reads patch.sh against patches/, with no framework on disk, so it runs in the gates job. Each patch step is held to four rules:

  • unguarded: a patch command that is not the else-branch of if grep -q "<sentinel>" "<file>"; then. Every patch command is read, so a step in a new shape is a finding, not a pass.
  • unreadable: a sentinel with $, a backtick, \, [, ], * or ^ (bash or grep would not read it as written), or a path from a variable the gate cannot resolve.
  • no-patch: the patch file is not there.
  • not-added: the sentinel is not in a + line the patch adds to the file the guard greps. Context lines, removed lines and other files of the same patch do not count (2f greps HTTPServer.h, 2m and 2n grep HTTPServer.cpp, in patches that touch both).

Not read, because they do the same thing every time: the backup of lwipopts.h (guarded by [ ! -f ... ]) and the copy of lwipopts.h (a plain cp). There is no sed step.

Negative control: on the patch.sh of 2d6650b the gate reports exactly one finding, 2b-ter at line 138, with the line the patch adds; on this tree, none (17 guarded steps).

tools/test_patch_sentinels.py (49 checks) pins it: the diff reader, synthetic trees for each rule and for the variables patch.sh builds its paths from, the command's exit codes, and on the real tree each of the 17 guards broken four ways (sentinel changed, marker in the patch changed, guard taken away, patch file gone), each exactly one finding at that step, plus 2b-ter with its old sentinel. 22 of 22 one-line changes to the gate are caught by it (3 by the test crashing on a lookup the removed check was guarding, not by a failed check).

What was run

  • python3 tools/test_patch_sentinels.py: 49 of 49. python3 tools/check_patch_sentinels.py: 0 findings in 17 guarded steps.
  • ci_gates_local.py --quick on this branch: 35 of 35 steps of gates and static-analysis. The native suites, the sanitizer pass and the fuzzer are skipped: they build src/, which this does not touch.
  • Not run: a firmware build, the bench. No image changes expected: on a core without the override the old and the new guard both find nothing and apply the same patch (same sha256 above). CHANGELOG.md is not touched: the last 40 commits that touched tools/ changed it once, for a firmware change.

CI exposure

The firmware jobs run patch.sh after restoring ~/.platformio from a cache (build.yml: "the script is idempotent, so a warm cache that already carries them is fine"; key pio-fw-<env>-<hash of platformio.ini>, with prefix fallbacks). platformio.ini last changed on 2026-10-13, before 2b-ter merged, and the ten latest build.yml runs, through #278, are green. My reading, not checked in a log (gh run view --job --log returned an empty log): CI still restores entries saved before 2b-ter, where the guard is never asked about an applied 2b-ter. The first entry saved after it (a change to platformio.ini, or an evicted entry recreated) would carry the override, and the next run to restore it would stop at 2b-ter in "Apply framework overrides" in every firmware job.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx

…very guard to its patch

patch.sh guards each framework override with `grep -q "<sentinel>"`, so that a second run
skips what is applied. Step 2b-ter (#274, merged 2026-10-15) greps "SIMUT override — drop a
non-TLS client"; wifiserversecure_drop_plaintext.patch adds "SIMUT override — tell a non-TLS
client from a TLS one by its FIRST BYTE". The first run applies the patch, so nothing failed.
On a core that already carries it, as the maintainer's has since 2026-10-15, the grep finds
nothing and `patch -p1` runs on a patched file.

Reproduced on isolated cores (cp -al copies of ~/.platformio, 2026-10-16), patch.sh of 2d6650b:
- the maintainer's core, once, no tty: "Reversed (or previously applied) patch detected!
  Assume -R? [n]", "Apply anyway? [n]", "Skipping patch.", "1 out of 1 hunk ignored -- saving
  rejects to file libraries/WiFi/src/WiFiServerSecureBearSSL.cpp.rej", exit 1. `set -e` ends
  the script on that line: 1 of 17 steps said "nada a fazer", no "DONE", and steps 2b-bis to
  2n and the cache invalidation of step 3 did not run. A dry run with `patch` stubbed out
  calls it from one step of 17 (2b-ter), so the other sixteen guards match their patches.
- 2b-ter undone with `patch -R`, then patch.sh twice: run 1 applies it (the file's sha256 is
  the maintainer's), run 2 stops as above.
- on a tty the same, after waiting at both prompts; "y" at the first reverses the override
  (the marker count goes 1 to 0) and the script goes on, exit 0.
~/.platformio was not written: the sha256 of its framework's (inode, size, mtime, path) list
and of the two state files, and the target's inode and sha256, are equal before and after.

Fix: the sentinel is the head of the line the patch adds. The same runs on fresh cores: the
maintainer's core once, exit 0, 17 of 17 "nada a fazer", nothing applied; 2b-ter undone, then
twice: run 1 applies only 2b-ter (same sha256), run 2 exit 0, 17 of 17 "nada a fazer"; on a
tty, no prompt.

tools/check_patch_sentinels.py reads patch.sh against patches/ and holds each `patch` step to
four rules: unguarded, unreadable, no-patch, and not-added (the sentinel must be in a `+`
line the patch adds to the file the guard greps). On the patch.sh of 2d6650b it reports
exactly one finding, 2b-ter at line 138; on this tree, none. tools/test_patch_sentinels.py
pins it in 49 checks (the diff reader, synthetic trees, and each of the 17 guards broken four
ways on the real tree, one finding each) and runs in the gates job; 22 of 22 one-line changes
to the gate are caught by it. The comment above step 2b and the overrides README now say what
a sentinel has to be.

Ran: ci_gates_local.py --quick, 35 of 35 steps (the native suites, the sanitizer pass and the
fuzzer skipped: they build src/, which this does not touch). Not run: a firmware build and the
bench. No image changes expected: on a core without the override the old and the new guard
both find nothing and apply the same patch (same sha256).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx
@github-actions github-actions Bot added documentation Improvements or additions to documentation ci Continuous integration and automation tools Build tools, scripts, and developer tooling labels Oct 16, 2026
@angeloINTJ
angeloINTJ merged commit ea4fcb1 into main Oct 16, 2026
13 checks passed
angeloINTJ pushed a commit that referenced this pull request Oct 21, 2026
Takes main from 2d6650b to 90e82d1: #279 (patch.sh's 2b-ter sentinel and its
gate, check_patch_sentinels.py, with its test) and #280 (an injected touch ends
in loopCore1( ) right after handleTouch( ); the touch sensitivity's Cancelar
takes an injection; the calibration's measuring phase ignores it).

Conflicts, one line each:
- src/DisplayManager_Touch.cpp: the sensitivity Cancelar keeps the branch's
  uiSensCancelHit( ) and actSensCancel( ) inside main's `p.z >= 50 || simNow`
  gate, with main's `_lastTouchInjected = simNow;` and its comment before
  acceptTouch(0); main's simNow hoist, its removal of the later declaration and
  its `if (simNow) return;` after the verdict buttons (uiCalVerdictHit) merged
  on their own, as did the expiry in loopCore1( ) (DisplayManager.cpp).
- .github/workflows/build.yml: both sides added steps at the same spot; all four
  kept (the branch's three, then main's patch.sh sentinel step).
- tools/flash_budget.json, docs/analysis/PLANO_STABLE.md section 3.1, README.md,
  README.pt-BR.md, README.es-ES.md: the branch's text, because sizes and counts
  are measured on the merged tree, not merged as text; the next commit does it.
- tools/README.md: the branch's counts and rows plus main's two new rows
  (check_patch_sentinels.py, test_patch_sentinels.py); counts checked in the next
  commit.

Nothing was built or run on a board for this commit; the host proof follows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx
@angeloINTJ
angeloINTJ deleted the fix/patch-sh-2b-ter-sentinel branch October 22, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and automation documentation Improvements or additions to documentation tools Build tools, scripts, and developer tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant