Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/1.0_TEST_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ shoulder button.
| Network services | Desired/runtime separation; carrier without IPv4 remains acquiring/not-ready; canonical DHCP restart and bounded post-address listener reconciliation; SSH shell and SFTP independent key-only states; canonical transfer roots and modes; Saves read-only; private roots denied; traversal/symlink/path bounds; same-filesystem atomic publication; authorized-key import; quiesce/health; stale FTP/Telnet/FileBrowser/inherited-Samba/WPS/hotspot flags cannot activate listeners; process-aware listener audit; Samba hidden until a materially different source-build candidate passes feasibility |
| Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and `bloomctl network profile upsert ID`; requested/document ID binding; explicit `network forget ID`; inactive-ID deletion with active-profile refusal; active forget only after a separately confirmed fallback connection; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; explicit local credential-free UI list with one active marker; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation |
| Wi-Fi scan API | Asynchronous start; redacted status; explicit ID-scoped local results; cancel and terminal discard; one active worker; five-second rate limit; 15-second runtime; 64-KiB raw ceiling; 32 unique printable SSIDs; eight-operation history; open/WPA2-PSK/unsupported classification; duplicate signal reconciliation; backend failure, timeout, oversize, unsafe-path, stale/live-lock, and unsupported-hardware handling |
| Wi-Fi UI | Original Mini hidden; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; selectable Scan/Cancel row and read-only discovered networks; active-only 100-ms SDL wake; deferred GameSwitcher until cancellation discard; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; controller-owned start/status/results/discard and cancel-during-start sequencing; 250-ms status opportunities; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; terminal history discarded; host-only open/WPA2 profile form with shared keyboard, masked secret, fixed profile-upsert argv, escaped stdin JSON, strict ID/SSID/PSK validation, explicit secret clearing, nonblocking submit/poll, 4-KiB acknowledgement cap, exact upsert response, timeout process-group cleanup, exact saved-ID reuse, 104-bit no-follow random new handles, and collision/security/full-store rejection; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence |
| Wi-Fi UI | Original Mini hidden; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; selectable Scan/Cancel row and read-only discovered networks; active-only 100-ms SDL wake; deferred GameSwitcher until cancellation discard; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; controller-owned start/status/results/discard and cancel-during-start sequencing; 250-ms status opportunities; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; terminal history discarded; host-only open/WPA2 profile form with shared keyboard, masked secret, fixed profile-upsert argv, escaped stdin JSON, strict ID/SSID/PSK validation, explicit secret clearing, nonblocking submit/poll, 4-KiB acknowledgement cap, exact upsert response, timeout process-group cleanup, exact saved-ID reuse, 104-bit no-follow random new handles, collision/security/full-store rejection, and one editing/submitting controller with 100-ms poll opportunities, exact cancellation, terminal clearing, and bounded focus return; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence |
| GameSwitcher | MENU open/toggle; Left/Right select; L1/R1 page; A resume; X actions; START Quick Settings; B Home; no inherited brightness/view/delete behavior |
| Apps/Tweaks | Every app disposition recorded; normal configuration requires no Tweaks; supervised launch remains bounded |
| Persistent state | Versioned atomic publication; exact restoration across Details/game/GameSwitcher/Quick Settings; malformed/oversized/symlink input defaults without Safe Mode |
Expand Down
8 changes: 8 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,14 @@ retried within a fixed bound. Exhausted collision retries, a full profile store,
unsupported security class, missing snapshot, or saved/scan security mismatch
fails closed before form state is created.

One event-loop-independent profile controller owns the mapped form and its
single-flight upsert request. It admits input only while editing, polls only at
100-ms monotonic opportunities while submitting, clears the complete form at
every terminal result, and kills and reaps the exact request process group on
cancel or destroy. The controller retains only the bounded non-secret list row
needed to return focus after completion, failure, or cancellation. It remains
unreachable from the rendered Shell in this host-only slice.

Profile association reuses the one guarded network-transaction authority rather
than adding a second recovery mechanism. `begin` copies the exact current
supplicant file into a mode-0700 JFFS2-private transaction root, records only
Expand Down
9 changes: 9 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,15 @@ bounded SDL wake; only an exact schema-1 upsert acknowledgement is success. The
request client exists for host validation but remains unreachable until the
rendered form/controller slice explicitly owns cancellation and focus return.

Use `BloomShellWifiProfileController` for that lifecycle rather than retaining a
form and request separately. It owns exactly one editing or submitting state,
accepts input only while editing, exposes 100-ms monotonic poll opportunities,
clears form and secret bytes on every terminal path, and kills and reaps the
request process group on cancellation or teardown. Its `return_item` is a
bounded non-secret row index for deterministic focus restoration; it is not a
profile, SSID, or association authority. The controller remains host-only until
the rendered integration slice adds explicit input and cancellation ownership.

The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`:

```sh
Expand Down
8 changes: 8 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,12 @@ retry a collision within a fixed bound. Exhaustion, an unavailable profile
snapshot, and a full store fail closed, so the future scan UI cannot overwrite
an unrelated credential document.

The host-only profile controller is the sole lifecycle owner for an edited
credential and its upsert child. Input is refused outside the editing state;
submission clears the password before returning; every terminal, cancellation,
and teardown path clears the complete form; and cancellation kills and reaps the
exact child process group. Only a bounded non-secret focus row survives a
terminal transition. This controller has no rendered or association consumer in
its initial slice.

Security-sensitive migrations and repair operations must be bounded, logged, reversible where possible, and validated before activation.
10 changes: 7 additions & 3 deletions docs/program/BloomOS_Work_Program_v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -887,12 +887,14 @@
548,
549,
550,
551
551,
552,
553
],
"risk_tier": 1,
"deployment_tier": "hot",
"device_gate_required": true,
"last_audited_main": "49f0a8096bff9a9157e4ce176201d2f532bb74dd",
"last_audited_main": "7c67002caf89e767d318d97f751a0b92e93d827e",
"evidence": [
"issue #544 tracks NET-05 on current main after merged NET-04A-D and UI-05; the audit found that Bloom Shell still treats Wi-Fi as an inline toggle whose A and left/right handlers synchronously call bloom-network enable or disable instead of opening the bounded profile and scan UI",
"the first host-safe slice adds an explicit local credential-free projection of at most 16 validated profile IDs, printable SSIDs, security classes, hidden flags, and one active marker; the private service never returns a PSK and the Shell model rejects unknown or credential-bearing fields",
Expand All @@ -910,7 +912,9 @@
"GitHub PR #550 passed format in 23 seconds, the complete 58-test affected shell selector in 46 seconds, native tests in 1 minute 11 seconds, AddressSanitizer in 1 minute 17 seconds, the affected Bloom Shell ARM build in 1 minute 23 seconds, and scoped static analysis in 2 minutes 22 seconds; it merged by squash as 0ef8e110c0afd197b3c63a400c6e02afd0fec8f9",
"GitHub PR #551 evolves profile submission into a bounded nonblocking request: start writes stdin and clears the password before returning, poll accepts one exact schema-1 upsert acknowledgement, and malformed, oversized, failed, or timed-out children are reaped through the exact process group; no rendered consumer, association, restart, forget, or device mutation is added",
"GitHub PR #551 passed format in 14 seconds, the complete 58-test affected shell selector in 50 seconds, AddressSanitizer in 1 minute 15 seconds, native tests in 1 minute 20 seconds, the affected Bloom Shell ARM build in 1 minute 23 seconds, and scoped static analysis in 2 minutes 18 seconds; it merged by squash as 49f0a8096bff9a9157e4ce176201d2f532bb74dd",
"GitHub PR #552 adds a host-only selection boundary that requires a current validated profile snapshot, reuses an exact saved SSID only when its security matches, creates a 104-bit high-entropy no-follow random handle for a new network, and rejects malformed state, unsupported security, full stores, security mismatch, unavailable state, and bounded collision exhaustion before any form or request can be created"
"GitHub PR #552 adds a host-only selection boundary that requires a current validated profile snapshot, reuses an exact saved SSID only when its security matches, creates a 104-bit high-entropy no-follow random handle for a new network, and rejects malformed state, unsupported security, full stores, security mismatch, unavailable state, and bounded collision exhaustion before any form or request can be created",
"GitHub PR #552 passed formatting in 13 seconds, the complete 58-test affected shell selector in 45 seconds, the affected Bloom Shell ARM build in 1 minute 12 seconds, native tests in 1 minute 15 seconds, AddressSanitizer in 1 minute 22 seconds, and scoped static analysis in 1 minute 45 seconds; it merged by squash as 7c67002caf89e767d318d97f751a0b92e93d827e",
"GitHub PR #553 gives the credential form one host-only lifecycle owner with editing and submitting states, 100-ms monotonic poll opportunities, input rejection outside editing, exact request cancellation and teardown, terminal form clearing, and a bounded non-secret return row for deterministic focus restoration; it still has no rendered, association, radio, or device consumer"
]
},
{
Expand Down
134 changes: 134 additions & 0 deletions src/bloomShell/bloom_shell_wifi_ui.c
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,140 @@ int bloom_shell_wifi_profile_request_poll(BloomShellWifiRequest *request,
return profile_upsert_ack_parse(request->output) == 0 ? 1 : -1;
}

void bloom_shell_wifi_profile_controller_init(BloomShellWifiProfileController *controller)
{
if (controller == NULL)
return;
memset(controller, 0, sizeof(*controller));
bloom_shell_wifi_request_init(&controller->request);
}

int bloom_shell_wifi_profile_controller_begin(BloomShellWifiProfileController *controller,
const BloomShellWifiUi *wifi,
size_t network_index)
{
if (controller == NULL ||
controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING ||
controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING)
return -1;
bloom_shell_wifi_request_destroy(&controller->request);
bloom_shell_wifi_profile_form_clear(&controller->form);
controller->next_poll_ms = 0;
controller->return_item = 0;
if (bloom_shell_wifi_profile_form_from_network(
wifi, network_index, &controller->form) != 0) {
controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED;
return -1;
}
controller->return_item = network_index + 1;
controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING;
return 0;
}

int bloom_shell_wifi_profile_controller_move(BloomShellWifiProfileController *controller,
int horizontal, int vertical)
{
if (controller == NULL ||
controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING ||
controller->form.security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK)
return -1;
return bloom_shell_wifi_profile_form_move(&controller->form, horizontal, vertical);
}

void bloom_shell_wifi_profile_controller_cycle_mode(
BloomShellWifiProfileController *controller)
{
if (controller != NULL &&
controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING &&
controller->form.security == BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK)
bloom_shell_wifi_profile_form_cycle_mode(&controller->form);
}

int bloom_shell_wifi_profile_controller_append(BloomShellWifiProfileController *controller)
{
if (controller == NULL ||
controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING)
return -1;
return bloom_shell_wifi_profile_form_append(&controller->form);
}

int bloom_shell_wifi_profile_controller_backspace(BloomShellWifiProfileController *controller)
{
if (controller == NULL ||
controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING)
return -1;
return bloom_shell_wifi_profile_form_backspace(&controller->form);
}

int bloom_shell_wifi_profile_controller_submit(BloomShellWifiProfileController *controller,
const char *bloomctl_path,
uint64_t now_ms)
{
if (controller == NULL ||
controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING)
return -1;
if (bloom_shell_wifi_profile_request_start(
&controller->request, bloomctl_path, &controller->form, now_ms) != 0) {
bloom_shell_wifi_profile_form_clear(&controller->form);
controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED;
return -1;
}
controller->next_poll_ms = now_ms + BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS;
controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING;
return 0;
}

int bloom_shell_wifi_profile_controller_poll(BloomShellWifiProfileController *controller,
uint64_t now_ms)
{
if (controller == NULL ||
controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING)
return -1;
if (now_ms < controller->next_poll_ms)
return 0;
int result = bloom_shell_wifi_profile_request_poll(&controller->request, now_ms);
if (result == 0) {
controller->next_poll_ms = now_ms + BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS;
return 0;
}
controller->next_poll_ms = 0;
bloom_shell_wifi_profile_form_clear(&controller->form);
controller->state = result > 0 ? BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_COMPLETE
: BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED;
return result;
}

int bloom_shell_wifi_profile_controller_cancel(BloomShellWifiProfileController *controller)
{
if (controller == NULL ||
(controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING &&
controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING))
return -1;
bloom_shell_wifi_request_destroy(&controller->request);
bloom_shell_wifi_profile_form_clear(&controller->form);
controller->next_poll_ms = 0;
controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_CANCELLED;
return 0;
}

int bloom_shell_wifi_profile_controller_active(
const BloomShellWifiProfileController *controller)
{
return controller != NULL &&
(controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING ||
controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING);
}

void bloom_shell_wifi_profile_controller_destroy(BloomShellWifiProfileController *controller)
{
if (controller == NULL)
return;
bloom_shell_wifi_request_destroy(&controller->request);
bloom_shell_wifi_profile_form_clear(&controller->form);
secure_clear(controller, sizeof(*controller));
bloom_shell_wifi_request_init(&controller->request);
}

int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path,
BloomShellWifiProfileForm *form)
{
Expand Down
Loading
Loading