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; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; 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; 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; 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
7 changes: 7 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,13 @@ versioned scan model's operation IDs, caps output at 16 KiB, and terminates and
reaps a command after five seconds. This client has no runtime consumer until
the follow-on event-loop state machine is integrated and validated.

`BloomShellWifiController` owns that event-loop-independent lifecycle. It
sequences one start, 250-ms status opportunities, one results fetch, and one
terminal discard. A cancel received before start returns an operation ID is
remembered and issued immediately afterward. Successful networks and terminal
errors remain in bounded memory for presentation while the service-side
operation ID is cleared, preventing scan-history accumulation.

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
7 changes: 7 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,13 @@ an unvalidated operation ID. Host tests must exercise timeout, oversized
output, malformed acknowledgement, symlink, and process cleanup paths before
the request object gains a runtime consumer.

Use `BloomShellWifiController`, not an ad hoc chain of request calls, for the
scan lifecycle. Its poll function is nonblocking and returns after at most one
state transition; the SDL layer supplies monotonic milliseconds and schedules
the next wake. Keep the controller alive until cancel/status/discard completes,
including when cancel is requested while start is still returning its operation
ID.

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

```sh
Expand Down
6 changes: 6 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,10 @@ deadline, and kills and reaps the child process group on every failed or
abandoned request. Cancel and discard acknowledgements must match the exact
requested operation ID.

The lifecycle controller automatically discards succeeded, failed, and
cancelled operations after fetching any successful result. It never guesses an
operation ID: cancellation requested during start remains pending until the
validated start response supplies the ID. This bounds scan history and prevents
a UI close or rapid cancel from racing an unrelated service operation.

Security-sensitive migrations and repair operations must be bounded, logged, reversible where possible, and validated before activation.
9 changes: 6 additions & 3 deletions docs/program/BloomOS_Work_Program_v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -882,20 +882,23 @@
],
"github_prs": [
545,
546
546,
547
],
"risk_tier": 1,
"deployment_tier": "hot",
"device_gate_required": true,
"last_audited_main": "7dee3fc27b5b0cd5b743af99f8e66b631c514c12",
"last_audited_main": "765f2038b7d63e61d6db8bab1a70ce750a07d088",
"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",
"the bounded Shell model accepts at most 32 unique scan results, one 18-32 byte operation ID, fixed scan phases and errors, integral timestamps and signal values, and exact schema-1 service identities; this slice adds no rendered surface, radio toggle, scan, association, profile transition, listener mutation, reboot, update, or device mutation",
"GitHub PR #545 passed format in 16 seconds, native in 1 minute 24 seconds, the affected ARM build in 1 minute 26 seconds, AddressSanitizer in 1 minute 55 seconds, the 736-test shell gate in 2 minutes 3 seconds, and static analysis in 2 minutes 4 seconds; it merged by squash as 3b0a13a30d34733f03831b0d041785c4fc6dc3ae",
"the dependent rendered-surface slice changes Wi-Fi from a toggle to a detail row, converges Settings and both Quick Settings implementations on the exact --settings-wifi route, removes the direct radio mutation helper, makes Wi-Fi left/right inert, and loads only the 4-KiB-bounded credential-free saved-profile projection outside the render path; scan, association, credential entry, forget, and recovery-qualified radio power remain pending",
"GitHub PR #546 passed format in 14 seconds, the affected ARM build in 1 minute 17 seconds, native tests and the deterministic UI gallery in 1 minute 29 seconds, AddressSanitizer in 1 minute 56 seconds, static analysis in 2 minutes 17 seconds, and the 737-test shell gate in 2 minutes 19 seconds; it merged by squash as 7dee3fc27b5b0cd5b743af99f8e66b631c514c12",
"the next host-only slice adds no runtime consumer or device action; it provides a nonblocking fixed-argv client for scan start, status, results, cancel, and discard with strict operation IDs, a 16-KiB output ceiling, a five-second command deadline, process-group kill and reap, no-follow executable checks, exact response parsing, and deterministic teardown so the later event-loop UI cannot improvise subprocess behavior"
"the next host-only slice adds no runtime consumer or device action; it provides a nonblocking fixed-argv client for scan start, status, results, cancel, and discard with strict operation IDs, a 16-KiB output ceiling, a five-second command deadline, process-group kill and reap, no-follow executable checks, exact response parsing, and deterministic teardown so the later event-loop UI cannot improvise subprocess behavior",
"GitHub PR #547 used risk-scoped CI: format passed in 23 seconds, 58 focused shell tests in 43 seconds, native tests in 1 minute 8 seconds, AddressSanitizer in 1 minute 12 seconds, the affected Bloom Shell ARM build in 1 minute 21 seconds, and scoped static analysis in 2 minutes 8 seconds; it merged by squash as 765f2038b7d63e61d6db8bab1a70ce750a07d088",
"the following host-only lifecycle controller sequences start, bounded status polling, results, and terminal discard through the single-flight request client; it preserves terminal results in memory while clearing the service operation, and a cancel received during start is deferred until the exact operation ID arrives before status confirmation and discard"
]
},
{
Expand Down
199 changes: 199 additions & 0 deletions src/bloomShell/bloom_shell_wifi_ui.c
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,202 @@ void bloom_shell_wifi_request_destroy(BloomShellWifiRequest *request)
close(request->output_fd);
bloom_shell_wifi_request_init(request);
}

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

static void scan_model_reset(BloomShellWifiUi *wifi)
{
wifi->scan_id[0] = '\0';
wifi->scan_phase = BLOOM_SHELL_WIFI_SCAN_IDLE;
wifi->scan_error[0] = '\0';
wifi->scan_result_count = 0;
wifi->scan_started_at = 0;
wifi->scan_completed_at = 0;
wifi->network_count = 0;
memset(wifi->networks, 0, sizeof(wifi->networks));
}

int bloom_shell_wifi_controller_begin(BloomShellWifiController *controller,
const char *bloomctl_path,
BloomShellWifiUi *wifi, uint64_t now_ms)
{
if (controller == NULL || wifi == NULL ||
bloom_shell_wifi_controller_active(controller) || wifi->scan_id[0] != '\0')
return -1;
scan_model_reset(wifi);
controller->terminal_phase = BLOOM_SHELL_WIFI_SCAN_IDLE;
controller->next_poll_ms = 0;
controller->cancel_pending = 0;
if (bloom_shell_wifi_request_start(
&controller->request, bloomctl_path,
BLOOM_SHELL_WIFI_REQUEST_SCAN_START, NULL, now_ms) != 0) {
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_FAILED;
return -1;
}
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_STARTING;
return 0;
}

static int controller_start(BloomShellWifiController *controller,
const char *bloomctl_path,
BloomShellWifiRequestKind kind,
const char *operation_id_value, uint64_t now_ms,
BloomShellWifiControllerState state)
{
if (bloom_shell_wifi_request_start(&controller->request, bloomctl_path, kind,
operation_id_value, now_ms) != 0) {
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_FAILED;
return -1;
}
controller->state = state;
return 0;
}

static int controller_discard(BloomShellWifiController *controller,
const char *bloomctl_path,
const BloomShellWifiUi *wifi, uint64_t now_ms)
{
controller->terminal_phase = wifi->scan_phase;
return controller_start(controller, bloomctl_path,
BLOOM_SHELL_WIFI_REQUEST_SCAN_DISCARD,
wifi->scan_id, now_ms,
BLOOM_SHELL_WIFI_CONTROLLER_DISCARDING);
}

static void controller_discarded(BloomShellWifiController *controller,
BloomShellWifiUi *wifi)
{
wifi->scan_id[0] = '\0';
controller->cancel_pending = 0;
if (controller->terminal_phase == BLOOM_SHELL_WIFI_SCAN_SUCCEEDED)
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_COMPLETE;
else if (controller->terminal_phase == BLOOM_SHELL_WIFI_SCAN_CANCELLED)
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_CANCELLED;
else
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_FAILED;
}

int bloom_shell_wifi_controller_poll(BloomShellWifiController *controller,
const char *bloomctl_path,
BloomShellWifiUi *wifi, uint64_t now_ms)
{
if (controller == NULL || wifi == NULL)
return -1;
if (!bloom_shell_wifi_request_active(&controller->request)) {
if (controller->state != BLOOM_SHELL_WIFI_CONTROLLER_POLLING ||
now_ms < controller->next_poll_ms)
return 0;
return controller_start(controller, bloomctl_path,
BLOOM_SHELL_WIFI_REQUEST_SCAN_STATUS,
wifi->scan_id, now_ms,
BLOOM_SHELL_WIFI_CONTROLLER_POLLING) == 0
? 1
: -1;
}
BloomShellWifiRequestKind completed = controller->request.kind;
int result = bloom_shell_wifi_request_poll(&controller->request, wifi, now_ms);
if (result == 0)
return 0;
if (result < 0) {
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_FAILED;
return -1;
}
switch (completed) {
case BLOOM_SHELL_WIFI_REQUEST_SCAN_START:
if (controller->cancel_pending)
return controller_start(controller, bloomctl_path,
BLOOM_SHELL_WIFI_REQUEST_SCAN_CANCEL,
wifi->scan_id, now_ms,
BLOOM_SHELL_WIFI_CONTROLLER_CANCELLING) == 0
? 1
: -1;
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_POLLING;
controller->next_poll_ms = now_ms;
return 1;
case BLOOM_SHELL_WIFI_REQUEST_SCAN_STATUS:
if (wifi->scan_phase == BLOOM_SHELL_WIFI_SCAN_QUEUED ||
wifi->scan_phase == BLOOM_SHELL_WIFI_SCAN_RUNNING) {
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_POLLING;
controller->next_poll_ms = now_ms + BLOOM_SHELL_WIFI_SCAN_POLL_INTERVAL_MS;
return 1;
}
if (wifi->scan_phase == BLOOM_SHELL_WIFI_SCAN_SUCCEEDED)
return controller_start(controller, bloomctl_path,
BLOOM_SHELL_WIFI_REQUEST_SCAN_RESULTS,
wifi->scan_id, now_ms,
BLOOM_SHELL_WIFI_CONTROLLER_FETCHING) == 0
? 1
: -1;
if (wifi->scan_phase == BLOOM_SHELL_WIFI_SCAN_FAILED ||
wifi->scan_phase == BLOOM_SHELL_WIFI_SCAN_CANCELLED)
return controller_discard(controller, bloomctl_path, wifi, now_ms) == 0
? 1
: -1;
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_FAILED;
return -1;
case BLOOM_SHELL_WIFI_REQUEST_SCAN_RESULTS:
return controller_discard(controller, bloomctl_path, wifi, now_ms) == 0
? 1
: -1;
case BLOOM_SHELL_WIFI_REQUEST_SCAN_CANCEL:
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_POLLING;
controller->next_poll_ms = now_ms;
return 1;
case BLOOM_SHELL_WIFI_REQUEST_SCAN_DISCARD:
controller_discarded(controller, wifi);
return 1;
default:
controller->state = BLOOM_SHELL_WIFI_CONTROLLER_FAILED;
return -1;
}
}

int bloom_shell_wifi_controller_cancel(BloomShellWifiController *controller,
const char *bloomctl_path,
const BloomShellWifiUi *wifi,
uint64_t now_ms)
{
if (controller == NULL || wifi == NULL)
return -1;
if (controller->state == BLOOM_SHELL_WIFI_CONTROLLER_STARTING) {
controller->cancel_pending = 1;
return 0;
}
if (controller->state == BLOOM_SHELL_WIFI_CONTROLLER_CANCELLING)
return 0;
if (wifi->scan_id[0] == '\0')
return -1;
if (bloom_shell_wifi_request_active(&controller->request))
bloom_shell_wifi_request_destroy(&controller->request);
controller->cancel_pending = 0;
return controller_start(controller, bloomctl_path,
BLOOM_SHELL_WIFI_REQUEST_SCAN_CANCEL,
wifi->scan_id, now_ms,
BLOOM_SHELL_WIFI_CONTROLLER_CANCELLING);
}

int bloom_shell_wifi_controller_active(const BloomShellWifiController *controller)
{
if (controller == NULL)
return 0;
return bloom_shell_wifi_request_active(&controller->request) ||
controller->state == BLOOM_SHELL_WIFI_CONTROLLER_STARTING ||
controller->state == BLOOM_SHELL_WIFI_CONTROLLER_POLLING ||
controller->state == BLOOM_SHELL_WIFI_CONTROLLER_FETCHING ||
controller->state == BLOOM_SHELL_WIFI_CONTROLLER_CANCELLING ||
controller->state == BLOOM_SHELL_WIFI_CONTROLLER_DISCARDING;
}

void bloom_shell_wifi_controller_destroy(BloomShellWifiController *controller)
{
if (controller == NULL)
return;
bloom_shell_wifi_request_destroy(&controller->request);
bloom_shell_wifi_controller_init(controller);
}
Loading
Loading