Skip to content

fix: four high findings of the manual review, and the reason for v=6 - #255

Merged
angeloINTJ merged 7 commits into
mainfrom
fix/high-findings-batch
Oct 5, 2026
Merged

angeloINTJ merged 7 commits into
mainfrom
fix/high-findings-batch

Conversation

@angeloINTJ

Copy link
Copy Markdown
Owner

Four of the high-severity findings that docs/analysis/PLANO_REVISAO_EXTERNA.md still listed after v2.11.0 (1, 8, 9 and 25), and the reason the Files page did not give for v=6, which v2.11.0 listed under Known. One commit per finding, then the flash record and the plan. Each finding was checked on the rig against main e5438e7 (v2.11.0 and two docs PRs), both images started from the same bench state.

What changes

main this branch
9, the forced PIN change (stricter policy, factory 1234) SAIR opens the Settings menu, PIN unchanged SAIR goes to the dashboard; Settings asks for the account again
8, permissions granted at the panel an account with Users gives Limits, Block and Maintenance to anyone, itself included it adds only bits it holds; taking one away is allowed (panelGrantAllowed, the web's V-09 rule)
1, "Adopt the probe wired here" reads GP<slot>, writes that number as the slot's pin, no reload reads and keeps the slot's GPIO, reloads the sensors; 409 for a slot not saved as an active DS18B20
25, the AP opened by the boot gesture its 15 min limit read 32 bytes nothing had written beginAP( ) gets the network's name: with one, the AP gives way at 900 s; with none, it stays
v=6 on the Files page "Upload failed (validation v=6). Cancelled." adds "The image is for another chip, or it is damaged. The Pico 2 W takes the releasetwo .bin, and the Pico W the others."

The 3 s sensor health check had finding 1's mistake too: it read GP<slot> for each DS18B20 slot. It reads the slot's GPIO now.

Tests first

  • native: test_panel_grant_adds_only_bits_the_caller_holds (finding 8). 284 of 284.
  • native_network: three cases, 39 of 39. Against main's NetworkManager, called the way main calls it, two fail:
    • test_an_ap_given_the_network_gives_way_to_it fails over a zeroed object: "the AP outlived its limit on a unit with a network";
    • test_the_ap_limit_reads_no_leftover_bytes fails over memory holding 0x01: "leftover bytes closed the AP of a unit with no network";
    • test_an_ap_with_no_network_stays_open passes on both.
  • tools/test_webui_firmware_refusal.py: the v=6 reply and the fil_fw_v6 key. Against main's page and packs it fails in 6 places; here it passes.
  • All nine native suites pass here, plainly and under ASan and UBSan, as CI runs them. So do the gates job's scripts (check_readme_numbers.py with 636 cases) and build_manual.py --check.
  • Every commit builds pico_w_test.

On the rig

For findings 9, 8 and 1, each run used pico_w_test of its own tree, from the S1 bench dump, restored and read back byte for byte before each image. The web is on :8080, the panel is driven with POST /api/touch, and the log is read from /api/logs. Finding 25 ran afterwards on the same unit, from bench builds with the touch remapped.

Finding 9. Tightening the PIN policy from 4/3/0 to 5/3/0 marks every account holding a PIN. pmgr identified with its old 5-digit PIN (code 308), Novo PIN opened on the ordered keypad, and SAIR gave:

  • main: UI mode 5, the Settings menu;
  • this branch: UI mode 0, the dashboard. CFG then opened the account picker (mode 26).

Finding 8. pmgr holds Users alone (0x0100):

from its Users menu main this branch
a new account with L and M created, 0x1400 refused
ptgt 0x0, plus L 0x0400 refused, 0x0
pkeep 0x0800, minus B (a bit pmgr lacks) 0x0 0x0
pmgr itself, plus M 0x1100 refused, 0x0100
log 450 ×1, 453 ×3 453 ×1, 458 ×3

Finding 1. Two cases:

  • Slot 1 on GP1, after a swap. sensor define told the slot it holds slot 0's probe, so it sat in quarantine. Adopt answered 200 on both. Over the next 90 s, main read 0 valid values of 15; this branch read 14 of 14, the first one at once.
  • The probe on GP1, moved to slot 7. main read GP7 and answered 422 badrom, for a probe GP1 was reading at 23.7 °C. This branch answered 200, the slot stayed on GP1, and it read again 8 s after the reload.
  • An empty slot. On this branch, Adopt on slot 12 answered 409 {"error":"notds18b20"}.

Finding 25. The image is a bench build of pico_w_test with -DTOUCH_IRQ=17 -DTOUCH_CS=21 (AGENTS.md §1). The hand held the finger down across a reset, on a unit configured for the bench network. The event log has, on both images, code 403 with context 1 (the gesture) 10 s into the boot, and code 525, "AP mode timeout, rebooting to STA", at 910 s:

  • main: the AP gave way too. The 32 bytes were not zero on that boot, so the rig did not show "never expires". The host tests pin both sides: the zero case for a unit with a network, and the leftover-bytes case for a unit with none.
  • this branch: the AP gave way, the next boot joined the network, and the web answered on :8080.

The rig did not try a unit with no network. Neither the console nor the web clears a configured network, so that takes a factory reset.

Flash

image main this branch budget
release 1,020,660 1,020,940 (+280) 1,020,732 → 1,021,012
test 1,025,244 1,025,516 (+272) holds
test_https 1,030,284 1,030,492 (+208) 1,030,284 → 1,030,492
asserts 1,022,964 1,023,236 (+272) 1,022,996 → 1,023,268
alpha 974,868 974,908 (+40), .bin unchanged holds
Air 1,018,176 1,018,224 (+48), .bin unchanged holds
Pico 2 W 996,736 997,000 (+264) 996,864 → 997,128

Budgets move by the growth, which keeps their margins. The signed release .bin has 7,171 B left under the OTA ceiling (7,451 B on v2.11.0). The test image's slack, 2,595 B, was already under one sector.

Three trims kept the growth there, from +472 B on release:

  • panelAllowed( ) gets an overload, not default arguments (−56 B);
  • beginAP( ) writes the name, instead of the constructor zeroing four buffers (−96 B);
  • _pinForced has no initializer (−16 B).

Found on the way, not fixed here

  • A stricter PIN minimum locks shorter PINs out of the panel.
    • DisplayManager::pinSubmit( ) refuses an entry shorter than the current minimum before Core 0 sees it.
    • So after 4 → 5, the 4-digit PIN of an account marked to change it cannot identify at all; on the rig it got "PIN muito curto".
    • The comment on user policy says tightening marks the PINs precisely so that this does not happen.
  • The message says "(min 4)" in the firmware and in both packs, whatever the policy's minimum is.
  • The touch-remapped bench image opened the AP on every boot. With the hand at CHARGER HIZ, the line reads low once the boot's first look is past. CHARGER ON holds it high, and AGENTS.md §1 now says so.

Docs

  • Manual chapters 6, 8, 9 and 17 describe the new behaviour; MANUAL.pt-BR.html is regenerated.
  • The three READMEs count 636 cases.
  • PLANO_REVISAO_EXTERNA.md marks the four findings, 52 remaining.
  • tools/flash_budget.json and §3.1 of PLANO_STABLE.md carry the record.
  • AGENTS.md §1 notes the trap of the touch-remapped image.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um

Ângelo Moisés Alves and others added 7 commits October 4, 2026 22:22
Finding 9 of docs/analysis/PLANO_REVISAO_EXTERNA.md. An account whose PIN
must change (a stricter policy, or the admin's factory 1234) is identified
and sent straight to Novo PIN. SAIR there opened the Settings menu, with the
session's every bit and the PIN unchanged; the change was asked again only
at the next identification.

showPinEntry( ) takes a `forced` flag, and the two forced calls in
panelIdentify( ) set it. pinCancel( ) sends a forced PIN_FOR_OWN to the
dashboard, where Settings asks for the account again; the voluntary change,
from the menu, still goes back to the menu. _pinForced has no initializer:
only pinCancel( ) reads it, on the PIN screen, and showPinEntry( ), the one
way onto that screen, writes it first. Zeroing it cost the constructor 16 B.

Tested on the rig with pico_w_test, each image started from the S1 bench
dump. Tightening the policy from 4/3/0 to 5/3/0 marks every account holding
a PIN. pmgr identified with its old 5-digit PIN (SEC_PIN_OK, 308), Novo PIN
opened on the ordered keypad, and SAIR then gave:
- main e5438e7: UI mode 5, the Settings menu (screenshot);
- this branch: UI mode 0, the dashboard; CFG then opened the account
  picker (mode 26).
The factory-PIN path is the same call with the same flag; it was not run on
the rig. The panel's screens have no native suite.

Manual chapter 8 says what SAIR does now; MANUAL.pt-BR.html regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
Finding 8 of docs/analysis/PLANO_REVISAO_EXTERNA.md. At the panel, an
account with Users (PERM_USER_MGR) gave Limits, Block and Maintenance to
any account, a new one or its own, whatever it held itself. The web has
refused that since V-09 (commitGrantAllowed).

panelGrantAllowed(before, after, caller), in SessionCheck.h, is the rule: a
change of an account's panel bits may only ADD bits the caller holds. Taking
one away is allowed, and so is leaving one the caller lacks where it was.
panelAllowed( ) gains an overload that asks it, used by EVT_USER_ADD (from
0) and EVT_USER_PERMS. A refusal is the one panelAllowed( ) already gives:
the error tone, APP_UI_PERM_DENIED (458) in the log, "Sem permissão", and
nothing written. An overload, not two default arguments: those cost each of
the other call sites 4 B, 56 B in all on pico_w_release.

Tests first: test_panel_grant_adds_only_bits_the_caller_holds, in the native
suite, which passes 284 of 284, under ASan and UBSan too.

On the rig, pmgr holding Users alone (0x0100), from its Users menu:
- a new account with L and M: main created it with 0x1400; this branch
  refuses it;
- ptgt, 0x0, plus L: main 0x0400; this branch refuses, and it stays 0x0;
- pkeep, 0x0800, minus B (a bit pmgr lacks): 0x0 on both;
- pmgr itself plus M: main 0x1100; this branch refuses, 0x0100;
- the log: main 450 once and 453 three times; this branch 458 three times
  and 453 once.

Manual chapter 8 says who grants what; MANUAL.pt-BR.html regenerated. The
READMEs count the new case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
Finding 1 of docs/analysis/PLANO_REVISAO_EXTERNA.md. "Adopt the probe wired
here" read the 1-Wire bus on the GPIO whose number is the slot's, and wrote
that number into the slot's pins[0]. A slot is not a pin. And it saved the
new ROM without reloading the running sensor, which keeps its own copy and
checks the probe against it every fifth read.

sensor_accept now reads the GPIO the slot has saved and keeps it, and saves
and reloads the sensors under quiet mode, as the calibration commit and the
console's accept do. A slot that is not saved as an active DS18B20 has no
GPIO to read: it answers 409 {"error":"notds18b20"}, and the page says
"Save this slot as an active DS18B20 first." (new key sens_accept_unsaved,
in both packs). checkAndAutoHealSensors( ), the 3 s health check, had the
same mistake: it read GP<slot> and flagged by that number. It reads the
slot's GPIO now, and logs it as the read path does.

On the rig, pico_w_test, each image from the S1 bench dump:
- slot 1 on GP1, told by `sensor define` it holds slot 0's probe, as after
  a swap, sits in quarantine. Adopt answered 200 on both. main: 0 valid
  reads of 15 in the next 90 s. This branch: 14 of 14, the first at once.
- the probe on GP1 moved to slot 7: main read GP7 and answered 422
  "badrom" for a probe GP1 was reading at 23.7 °C; this branch answered 200
  and slot 7 stayed on GP1, reading again 8 s after the reload.
- in 160 s neither logged a "missing" (106) for slot 7: the rig's empty GP7
  answers a ROM that fails its CRC, which the health check skips.

Manual chapter 6 says what Adopt does now, and the warning about the slot's
number is gone; MANUAL.pt-BR.html regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
A Pico W refuses the Pico 2 W's .bin with validation code 6: its first 256
bytes are no RP2040 boot stage. Since v2.11.0 the two .bin sit side by side
on the release page, and the Files page said only "Upload failed
(validation v=6). Cancelled." (under Known in v2.11.0). On the Pico 2 W, 6
is an image with no RP2350 image definition, which a Pico W .bin does not
reach: its env tag refuses it first, with 7.

fwStageMessage( ) explains 6: "The image is for another chip, or it is
damaged. The Pico 2 W takes the releasetwo .bin, and the Pico W the
others." New key fil_fw_v6, in both packs.

Tests first: tools/test_webui_firmware_refusal.py gains the v=6 reply, and
its pack check asks for fil_fw_v6. Against main's WebUI.h and packs it
fails in 6 places: both packs, and the reply as written and as minified.
It passes here.

Manual chapter 17 quotes the message; MANUAL.pt-BR.html regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
Finding 25 of docs/analysis/PLANO_REVISAO_EXTERNA.md. An AP on a unit with
a network gives way to it after 15 min (AP_MODE_TIMEOUT_MS) by restarting
into the station, and asks for the network by its name. Only begin( )
wrote that name, and the boot the gesture sends into AP mode never calls
begin( ): the limit read 32 bytes of a heap object nothing had written.
Zeros kept the AP of a unit with a network up for good, which is the
finding as written; anything else closed the AP of a unit with none, the
one way to give it a network.

beginAP( ) takes the network's name, and both callers pass cfg.wifiSsid.
It writes the name every time, empty for a unit with none.

Tests first, in native_network (39 of 39, under ASan and UBSan too):
- test_an_ap_given_the_network_gives_way_to_it: up at 895 s, restarted by
  905 s;
- test_an_ap_with_no_network_stays_open: no restart in 30 min, with no
  name and with an empty one;
- test_the_ap_limit_reads_no_leftover_bytes: built over memory holding
  0x01, a unit with no network keeps its AP.
Against main's NetworkManager, called the way main calls it, the first
fails over a zeroed object ("the AP outlived its limit on a unit with a
network") and the third over 0x01 ("leftover bytes closed the AP of a unit
with no network").

On the rig, a bench build of pico_w_test with -DTOUCH_IRQ=17 -DTOUCH_CS=21
(AGENTS.md section 1), the finger held with CHARGER OFF across a reset, the
unit configured for the bench network. The event log has, on both images,
code 403 with context 1 (the gesture) 10 s into the boot, and code 525,
"AP mode timeout, rebooting to STA", at 910 s:
- main e5438e7: the AP gave way. The 32 bytes were not zero on that boot,
  so the rig did not show "never expires";
- this branch: the AP gave way, and the next boot joined the network; the
  web answered on :8080.
A unit with no network was not tried on the rig: neither the console nor
the web clears a configured network, so it takes a factory reset.

Found on the way, now in AGENTS.md section 1: with the hand at CHARGER HIZ,
that image's PENIRQ line read low once the boot's first look was past
(GP3 level=L), so every one of its boots opened the AP with no finger,
including the one after the 15 min. CHARGER ON holds the line high.

Manual chapter 9 says how long the gesture's AP lasts; MANUAL.pt-BR.html
regenerated. The READMEs count the three new cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
All seven images built from this branch, on main e5438e7:
- release: 'measured' and the .bin +280 B (1,020,940 and 1,032,972);
  budget 1,020,732 -> 1,021,012;
- test: +272 B (1,025,516 and 1,037,548); its budget holds, 144 B under;
- test_https: +208 B (1,030,492); budget 1,030,284 -> 1,030,492;
- asserts: +272 B (1,023,236 and 1,035,268); budget 1,022,996 ->
  1,023,268;
- alpha: +40 B (974,908), Air: +48 B (1,018,224); both keep their .bin,
  the growth inside the 4 KiB pad before .data, and their budgets;
- pico2_w_release: +264 B (997,000 and 997,024); budget 996,864 ->
  997,128.
Each budget that moves moves by its image's growth, which keeps its
margin.

By commit, on pico_w_test: the forced PIN change +40 B, the panel's grant
rule +80 B, Adopt +80 B, the v=6 reason +56 B (the Files page's gzip), the
AP limit +16 B. Three choices kept it there, from +472 B on release at
first: panelAllowed( ) has an overload instead of two default arguments
(-56 B), beginAP( ) writes the name instead of the constructor zeroing
four buffers (-96 B), and _pinForced has no initializer (-16 B).

The OTA headroom table of docs/analysis/PLANO_STABLE.md section 3.1
follows: release 7,171 B (7,451 on v2.11.0), test 2,595 B, which was
already under one sector and warned, asserts 4,875 B, pico2_w_release
571,503 B against its slot; alpha and Air unchanged.
check_flash_budget.py passes on the seven builds, and --table passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
docs/analysis/PLANO_REVISAO_EXTERNA.md marks the four high findings #255
fixes, each with the number the bench measured against main, and counts
52 remaining. Of the high ones, 20 and 65 still need code, and 41 is the
documented limitation of plain HTTP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Testing infrastructure and test coverage display Display, TFT, touchscreen, and themes i18n Internationalization and translations tools Build tools, scripts, and developer tooling code Firmware source changes (src/, WebUI.h) labels Oct 5, 2026
@angeloINTJ
angeloINTJ merged commit 8388f32 into main Oct 5, 2026
11 checks passed
@angeloINTJ
angeloINTJ deleted the fix/high-findings-batch branch October 15, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code Firmware source changes (src/, WebUI.h) display Display, TFT, touchscreen, and themes documentation Improvements or additions to documentation i18n Internationalization and translations tests Testing infrastructure and test coverage tools Build tools, scripts, and developer tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant