From 377108b561ad4fca8af4f7d2b7352fb2cb53208f Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 01:36:44 -0400 Subject: [PATCH 1/2] feat(network): poll Wi-Fi profile submission --- docs/1.0_TEST_MATRIX.md | 2 +- docs/ARCHITECTURE.md | 7 ++ docs/DEVELOPMENT.md | 7 +- docs/SECURITY.md | 7 ++ docs/program/BloomOS_Work_Program_v3.json | 6 +- src/bloomShell/bloom_shell_wifi_ui.c | 147 ++++++++++++++++++---- src/bloomShell/bloom_shell_wifi_ui.h | 7 ++ test/test_bloom_shell_wifi_ui.cpp | 91 +++++++++++++- 8 files changed, 241 insertions(+), 33 deletions(-) diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index 0016edd29..05f5d2ecb 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -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, and explicit secret clearing; 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, and timeout process-group cleanup; 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 | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9047a1f91..0858bfacc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -582,6 +582,13 @@ stdin. The password and temporary document are explicitly cleared on every success and failure path. This model has no rendered consumer and does not connect, project, restart, or forget a profile. +Profile submission can now use the shared nonblocking request lifecycle. Start +writes the bounded document once, clears the secret immediately, and returns +with only a child handle, non-secret profile ID, and bounded output pipe. Poll +accepts one exact schema-1 upsert acknowledgement; malformed, oversized, +non-zero, or five-second-timeout responses kill and reap the exact process +group. This request is still not reachable from the render or input loop. + 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 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2f83ed0dd..ef07cc6f1 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -399,8 +399,11 @@ the password buffer. The form validates the exact store vocabulary, masks every password byte in display copy, JSON-escapes printable input, sends the profile only on stdin to fixed `network profile upsert ID` arguments, and securely clears both password and temporary document bytes on every path. Do not wire this -synchronous host-tested boundary into the SDL loop; rendered submission requires -a separate nonblocking controller before it can become reachable. +boundary directly into the SDL loop. Use +`bloom_shell_wifi_profile_request_start()` and poll the active request from a +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. The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`: diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 12011825b..2ed9d64fc 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -163,4 +163,11 @@ stderr closed to `/dev/null`; parent memory explicitly clears the password and escaped document before returning, including validation, pipe, fork, execution, and timeout failures. No runtime surface can invoke this boundary yet. +The asynchronous profile request retains no password after `start` returns. Its +remaining state is limited to the child/process-group handle, a non-secret +profile ID, monotonic deadline, and a 4-KiB-bounded acknowledgement buffer. Poll +requires the exact schema-1 `bloom-wifi-profiles` upsert response and rejects a +zero/out-of-range profile count, unknown fields, oversized output, non-zero +exit, or timeout before reporting completion. + Security-sensitive migrations and repair operations must be bounded, logged, reversible where possible, and validated before activation. diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index 599013fdf..96b4a0b4d 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -891,7 +891,7 @@ "risk_tier": 1, "deployment_tier": "hot", "device_gate_required": true, - "last_audited_main": "6c277d2300494ba8387accbf4bc28f4459640509", + "last_audited_main": "0ef8e110c0afd197b3c63a400c6e02afd0fec8f9", "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", @@ -905,7 +905,9 @@ "GitHub PR #548 passed format in 18 seconds, 58 focused shell tests in 50 seconds, the affected Bloom Shell ARM build in 1 minute 11 seconds, native tests in 1 minute 20 seconds, AddressSanitizer in 1 minute 26 seconds, and scoped static analysis in 2 minutes 8 seconds; it merged by squash as 9b3a847d99daf8baf44d953670d0ff4a18562a60", "GitHub PR #549 renders the bounded scan controller through a 100-ms SDL wake without invoking commands from draw or blocking input, adds a selectable Scan action and read-only bounded results, cancels in the background when leaving the panel, and defers GameSwitcher until the exact scan operation is cancelled, confirmed terminal, and discarded; association, credential entry, forget, and radio power remain absent", "GitHub PR #549 passed format in 40 seconds, 106 affected shell tests in 50 seconds, AddressSanitizer in 1 minute 10 seconds, the affected Bloom Shell ARM build in 1 minute 22 seconds, native tests in 1 minute 30 seconds, and scoped static analysis in 2 minutes 3 seconds; it merged by squash as 6c277d2300494ba8387accbf4bc28f4459640509", - "GitHub PR #550 adds a host-only credential-form boundary that accepts only bounded schema-valid open or WPA2 inputs, masks password copy, invokes fixed bloomctl network profile upsert arguments with escaped JSON only on stdin, and explicitly clears password and temporary document bytes on every path; it has no rendered consumer and cannot scan, associate, restart Wi-Fi, forget a profile, or mutate the device" + "GitHub PR #550 adds a host-only credential-form boundary that accepts only bounded schema-valid open or WPA2 inputs, masks password copy, invokes fixed bloomctl network profile upsert arguments with escaped JSON only on stdin, and explicitly clears password and temporary document bytes on every path; it has no rendered consumer and cannot scan, associate, restart Wi-Fi, forget a profile, or mutate the device", + "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", + "the following host-only slice 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" ] }, { diff --git a/src/bloomShell/bloom_shell_wifi_ui.c b/src/bloomShell/bloom_shell_wifi_ui.c index b0779cbf2..115d801ce 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.c +++ b/src/bloomShell/bloom_shell_wifi_ui.c @@ -599,13 +599,19 @@ static int write_all(int descriptor, const char *value, size_t length) return 0; } -int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, - BloomShellWifiProfileForm *form) +int bloom_shell_wifi_profile_request_start(BloomShellWifiRequest *request, + const char *bloomctl_path, + BloomShellWifiProfileForm *form, + uint64_t now_ms) { char document[512]; size_t document_length = 0; if (form == NULL) return -1; + if (request == NULL || request->child > 0 || request->output_fd != -1) { + secure_clear(form->psk, sizeof(form->psk)); + return -1; + } int valid = executable_regular(bloomctl_path) && profile_document(form, document, sizeof(document), &document_length) == 0; if (!valid) { @@ -613,8 +619,17 @@ int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, secure_clear(document, sizeof(document)); return -1; } - int input[2]; - if (pipe(input) != 0) { + int input[2] = {-1, -1}; + int output[2] = {-1, -1}; + if (pipe(input) != 0 || pipe(output) != 0) { + if (input[0] >= 0) { + close(input[0]); + close(input[1]); + } + if (output[0] >= 0) { + close(output[0]); + close(output[1]); + } secure_clear(form->psk, sizeof(form->psk)); secure_clear(document, sizeof(document)); return -1; @@ -623,18 +638,22 @@ int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, if (child < 0) { close(input[0]); close(input[1]); + close(output[0]); + close(output[1]); secure_clear(form->psk, sizeof(form->psk)); secure_clear(document, sizeof(document)); return -1; } if (child == 0) { close(input[1]); - if (setpgid(0, 0) != 0 || dup2(input[0], STDIN_FILENO) < 0) + close(output[0]); + if (setpgid(0, 0) != 0 || dup2(input[0], STDIN_FILENO) < 0 || + dup2(output[1], STDOUT_FILENO) < 0) _exit(127); close(input[0]); + close(output[1]); int null_descriptor = open("/dev/null", O_RDWR); - if (null_descriptor < 0 || dup2(null_descriptor, STDOUT_FILENO) < 0 || - dup2(null_descriptor, STDERR_FILENO) < 0) + if (null_descriptor < 0 || dup2(null_descriptor, STDERR_FILENO) < 0) _exit(127); if (null_descriptor > STDERR_FILENO) close(null_descriptor); @@ -644,9 +663,15 @@ int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, _exit(127); } close(input[0]); - int setup_failed = setpgid(child, child) != 0 && errno != EACCES && errno != ESRCH; + close(output[1]); + int descriptor_flags = fcntl(output[0], F_GETFL); + int setup_failed = + (setpgid(child, child) != 0 && errno != EACCES && errno != ESRCH) || + descriptor_flags < 0 || + fcntl(output[0], F_SETFL, descriptor_flags | O_NONBLOCK) < 0; if (setup_failed) { close(input[1]); + close(output[0]); kill(child, SIGKILL); while (waitpid(child, NULL, 0) < 0 && errno == EINTR) { } @@ -664,20 +689,23 @@ int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, sigaction(SIGPIPE, &previous, NULL); secure_clear(form->psk, sizeof(form->psk)); secure_clear(document, sizeof(document)); - int status = 0; - for (int attempt = 0; attempt < BLOOM_SHELL_WIFI_REQUEST_TIMEOUT_MS / 10; ++attempt) { - pid_t result = waitpid(child, &status, WNOHANG); - if (result == child) - return !failed && WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -1; - if (result < 0 && errno != EINTR) - return -1; - usleep(10000); - } - kill(-child, SIGKILL); - kill(child, SIGKILL); - while (waitpid(child, &status, 0) < 0 && errno == EINTR) { + if (failed) { + close(output[0]); + kill(-child, SIGKILL); + kill(child, SIGKILL); + while (waitpid(child, NULL, 0) < 0 && errno == EINTR) { + } + return -1; } - return -1; + request->child = child; + request->output_fd = output[0]; + request->kind = BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT; + request->started_ms = now_ms; + request->output_length = 0; + request->output[0] = '\0'; + snprintf(request->operation_id, sizeof(request->operation_id), "%s", + form->profile_id); + return 0; } int bloom_shell_wifi_request_start(BloomShellWifiRequest *request, @@ -749,13 +777,16 @@ int bloom_shell_wifi_request_start(BloomShellWifiRequest *request, static int request_read(BloomShellWifiRequest *request) { - while (request->output_length <= BLOOM_SHELL_WIFI_REQUEST_OUTPUT_LIMIT) { + size_t limit = request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT + ? BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT + : BLOOM_SHELL_WIFI_REQUEST_OUTPUT_LIMIT; + while (request->output_length <= limit) { ssize_t count = read(request->output_fd, request->output + request->output_length, - sizeof(request->output) - request->output_length - 1); + limit + 1 - request->output_length); if (count > 0) { request->output_length += (size_t)count; - if (request->output_length > BLOOM_SHELL_WIFI_REQUEST_OUTPUT_LIMIT) + if (request->output_length > limit) return -1; continue; } @@ -811,10 +842,31 @@ static int request_parse(const BloomShellWifiRequest *request, BloomShellWifiUi } } -int bloom_shell_wifi_request_poll(BloomShellWifiRequest *request, - BloomShellWifiUi *wifi, uint64_t now_ms) +static int profile_upsert_ack_parse(const char *json) +{ + cJSON *root = cJSON_ParseWithOpts(json, NULL, 1); + const cJSON *operation = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "operation"); + const cJSON *published = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "published"); + const cJSON *replaced = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "replaced"); + const cJSON *count = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "profile_count"); + uint64_t parsed_count = 0; + int valid = exact_object(root, 6) && root_identity(root, "bloom-wifi-profiles") && + bounded_string(operation, 16, 1) && + strcmp(operation->valuestring, "upsert") == 0 && + cJSON_IsTrue(published) && cJSON_IsBool(replaced) && + unsigned_integer(count, BLOOM_SHELL_WIFI_PROFILE_LIMIT, &parsed_count) && + parsed_count > 0; + cJSON_Delete(root); + return valid ? 0 : -1; +} + +static int request_finish(BloomShellWifiRequest *request, uint64_t now_ms) { - if (request == NULL || wifi == NULL || request->child <= 0 || request->output_fd < 0) + if (request == NULL || request->child <= 0 || request->output_fd < 0) return -1; int read_result = request_read(request); int status = 0; @@ -841,9 +893,50 @@ int bloom_shell_wifi_request_poll(BloomShellWifiRequest *request, if (read_result < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1; request->output[request->output_length] = '\0'; + return 1; +} + +int bloom_shell_wifi_request_poll(BloomShellWifiRequest *request, + BloomShellWifiUi *wifi, uint64_t now_ms) +{ + if (wifi == NULL || request == NULL || + request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT) + return -1; + int result = request_finish(request, now_ms); + if (result <= 0) + return result; return request_parse(request, wifi) == 0 ? 1 : -1; } +int bloom_shell_wifi_profile_request_poll(BloomShellWifiRequest *request, + uint64_t now_ms) +{ + if (request == NULL || request->kind != BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT) + return -1; + int result = request_finish(request, now_ms); + if (result <= 0) + return result; + return profile_upsert_ack_parse(request->output) == 0 ? 1 : -1; +} + +int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, + BloomShellWifiProfileForm *form) +{ + BloomShellWifiRequest request; + bloom_shell_wifi_request_init(&request); + if (bloom_shell_wifi_profile_request_start(&request, bloomctl_path, form, 0) != 0) + return -1; + for (int attempt = 0; attempt < BLOOM_SHELL_WIFI_REQUEST_TIMEOUT_MS / 10; ++attempt) { + int result = bloom_shell_wifi_profile_request_poll(&request, + (uint64_t)attempt * 10); + if (result != 0) + return result > 0 ? 0 : -1; + usleep(10000); + } + bloom_shell_wifi_request_destroy(&request); + return -1; +} + int bloom_shell_wifi_request_active(const BloomShellWifiRequest *request) { return request != NULL && request->child > 0 && request->output_fd >= 0; diff --git a/src/bloomShell/bloom_shell_wifi_ui.h b/src/bloomShell/bloom_shell_wifi_ui.h index d95b09b7e..4f9a5bfcf 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.h +++ b/src/bloomShell/bloom_shell_wifi_ui.h @@ -72,6 +72,7 @@ typedef enum { BLOOM_SHELL_WIFI_REQUEST_SCAN_RESULTS, BLOOM_SHELL_WIFI_REQUEST_SCAN_CANCEL, BLOOM_SHELL_WIFI_REQUEST_SCAN_DISCARD, + BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT, } BloomShellWifiRequestKind; typedef struct { @@ -119,6 +120,12 @@ int bloom_shell_wifi_profile_form_label(const BloomShellWifiProfileForm *form, char *label, size_t label_size); int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, BloomShellWifiProfileForm *form); +int bloom_shell_wifi_profile_request_start(BloomShellWifiRequest *request, + const char *bloomctl_path, + BloomShellWifiProfileForm *form, + uint64_t now_ms); +int bloom_shell_wifi_profile_request_poll(BloomShellWifiRequest *request, + uint64_t now_ms); void bloom_shell_wifi_profile_form_clear(BloomShellWifiProfileForm *form); int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_status_parse(const char *json, BloomShellWifiUi *wifi); diff --git a/test/test_bloom_shell_wifi_ui.cpp b/test/test_bloom_shell_wifi_ui.cpp index 659bb6c81..107bd2b38 100644 --- a/test/test_bloom_shell_wifi_ui.cpp +++ b/test/test_bloom_shell_wifi_ui.cpp @@ -113,7 +113,8 @@ TEST(BloomShellWifiUi, SubmitsFixedProfileArgumentsViaStdinAndClearsSecret) { std::ofstream output(script); output << "#!/bin/sh\nprintf '%s' \"$*\" > '" << arguments.string() - << "'\ncat > '" << input_path.string() << "'\n"; + << "'\ncat > '" << input_path.string() + << "'\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"operation\":\"upsert\",\"published\":true,\"replaced\":false,\"profile_count\":1}'\n"; } ASSERT_EQ(0, chmod(script.c_str(), 0700)); BloomShellWifiProfileForm form{}; @@ -162,6 +163,94 @@ TEST(BloomShellWifiUi, SubmitsFixedProfileArgumentsViaStdinAndClearsSecret) std::filesystem::remove_all(directory); } +TEST(BloomShellWifiUi, RunsOneBoundedNonblockingProfileSubmission) +{ + auto directory = std::filesystem::temp_directory_path() / + ("bloom-shell-wifi-profile-request-" + std::to_string(getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + auto script = directory / "bloomctl"; + auto input_path = directory / "input"; + { + std::ofstream output(script); + output << "#!/bin/sh\ncat > '" << input_path.string() + << "'\nsleep 0.05\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"operation\":\"upsert\",\"published\":true,\"replaced\":false,\"profile_count\":1}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + BloomShellWifiProfileForm form{}; + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-async", "Fixture Async", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + snprintf(form.psk, sizeof(form.psk), "%s", "fixture-password"); + BloomShellWifiRequest request; + bloom_shell_wifi_request_init(&request); + ASSERT_EQ(0, bloom_shell_wifi_profile_request_start( + &request, script.c_str(), &form, 100)); + EXPECT_TRUE(bloom_shell_wifi_request_active(&request)); + EXPECT_STREQ("", form.psk); + EXPECT_STREQ("fixture-async", request.operation_id); + int result = 0; + for (uint64_t attempt = 0; result == 0 && attempt < 100; ++attempt) { + result = bloom_shell_wifi_profile_request_poll(&request, 100 + attempt * 10); + if (result == 0) + usleep(10000); + } + EXPECT_EQ(1, result); + EXPECT_FALSE(bloom_shell_wifi_request_active(&request)); + + { + std::ofstream output(script); + output << "#!/bin/sh\ncat >/dev/null\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"operation\":\"upsert\",\"published\":true,\"replaced\":false,\"profile_count\":0}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-invalid", "Fixture Invalid", + BLOOM_SHELL_WIFI_SECURITY_OPEN, 0)); + ASSERT_EQ(0, bloom_shell_wifi_profile_request_start( + &request, script.c_str(), &form, 200)); + result = 0; + for (uint64_t attempt = 0; result == 0 && attempt < 100; ++attempt) { + result = bloom_shell_wifi_profile_request_poll(&request, 200 + attempt * 10); + if (result == 0) + usleep(10000); + } + EXPECT_EQ(-1, result); + + { + std::ofstream output(script); + output << "#!/bin/sh\ncat >/dev/null\nhead -c 4097 /dev/zero | tr '\\000' x\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-oversize", "Fixture Oversize", + BLOOM_SHELL_WIFI_SECURITY_OPEN, 0)); + ASSERT_EQ(0, bloom_shell_wifi_profile_request_start( + &request, script.c_str(), &form, 250)); + result = 0; + for (uint64_t attempt = 0; result == 0 && attempt < 100; ++attempt) { + result = bloom_shell_wifi_profile_request_poll(&request, 250 + attempt * 10); + if (result == 0) + usleep(10000); + } + EXPECT_EQ(-1, result); + + { + std::ofstream output(script); + output << "#!/bin/sh\ncat >/dev/null\nsleep 10\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-timeout", "Fixture Timeout", + BLOOM_SHELL_WIFI_SECURITY_OPEN, 0)); + ASSERT_EQ(0, bloom_shell_wifi_profile_request_start( + &request, script.c_str(), &form, 300)); + EXPECT_EQ(-1, bloom_shell_wifi_profile_request_poll( + &request, 300 + BLOOM_SHELL_WIFI_REQUEST_TIMEOUT_MS)); + EXPECT_FALSE(bloom_shell_wifi_request_active(&request)); + bloom_shell_wifi_request_destroy(&request); + std::filesystem::remove_all(directory); +} + TEST(BloomShellWifiUi, TracksOneBoundedAsynchronousScan) { BloomShellWifiUi wifi{}; From 8040250c3335bb077ce0e2ffcadcd9c27fbd3cc0 Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 01:37:26 -0400 Subject: [PATCH 2/2] docs(program): record NET-05 profile controller PR --- docs/program/BloomOS_Work_Program_v3.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index 96b4a0b4d..0fd946244 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -886,7 +886,8 @@ 547, 548, 549, - 550 + 550, + 551 ], "risk_tier": 1, "deployment_tier": "hot", @@ -907,7 +908,7 @@ "GitHub PR #549 passed format in 40 seconds, 106 affected shell tests in 50 seconds, AddressSanitizer in 1 minute 10 seconds, the affected Bloom Shell ARM build in 1 minute 22 seconds, native tests in 1 minute 30 seconds, and scoped static analysis in 2 minutes 3 seconds; it merged by squash as 6c277d2300494ba8387accbf4bc28f4459640509", "GitHub PR #550 adds a host-only credential-form boundary that accepts only bounded schema-valid open or WPA2 inputs, masks password copy, invokes fixed bloomctl network profile upsert arguments with escaped JSON only on stdin, and explicitly clears password and temporary document bytes on every path; it has no rendered consumer and cannot scan, associate, restart Wi-Fi, forget a profile, or mutate the device", "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", - "the following host-only slice 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 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" ] }, {