// SPDX-License-Identifier: Apache-2.0 // libc calls made by an app instance (fstat, termios, poll, printf, exit), routed by this module's // libc wraps (../source/stdio_wrap.cpp) to the app instance's own fds and lifecycle. #include "doctest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern ServiceManifest app_internal_loader_service_manifest; namespace { // See manager_test.cpp's own copy of this helper for why this checks the registry directly // rather than a per-translation-unit static bool. void ensure_memory_loader_registered() { if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) { service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true); } } bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) { uint32_t waited = 0; while (waited < timeout_ms) { if (app_manager_get_state(id) == target) { return true; } delay_millis(10); waited += 10; } return app_manager_get_state(id) == target; } bool wait_for_flag(std::atomic& flag, uint32_t timeout_ms) { uint32_t waited = 0; while (waited < timeout_ms) { if (flag.load(std::memory_order_acquire)) { return true; } delay_millis(10); waited += 10; } return flag.load(std::memory_order_acquire); } int32_t location_app_main(int argc, char* argv[]) { if (argc == 1) { return static_cast(strtol(argv[0], nullptr, 10)); } TaskEventGroup event_group {}; task_event_group_construct(&event_group); AppEventSubscription sub {}; app_event_subscribe(&sub, &event_group); while (true) { if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) { break; // safety net so a bug here can't hang the test suite } bool done = false; AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { if (event.type == APP_EVENT_CLOSE) { done = true; break; } } if (done) { break; } } app_event_unsubscribe(&sub); task_event_group_destruct(&event_group); return 0; } int32_t printf_stream_writer_app_main(int, char*[]) { printf("loc"); return 7; } std::atomic g_posix_stdin_is_char_device { false }; std::atomic g_posix_tcgetattr_result { -2 }; std::atomic g_posix_stdin_is_raw { false }; std::atomic g_posix_tcsetattr_result { -2 }; std::atomic g_posix_poll_before_write { -2 }; std::atomic g_posix_poll_after_write { -2 }; std::atomic g_posix_poll_first_done { false }; std::atomic g_posix_null_buffers_efault { false }; int32_t posix_calls_app_main(int, char*[]) { struct stat st {}; g_posix_stdin_is_char_device.store(fstat(STDIN_FILENO, &st) == 0 && S_ISCHR(st.st_mode), std::memory_order_release); struct termios t {}; g_posix_tcgetattr_result.store(tcgetattr(STDIN_FILENO, &t), std::memory_order_release); g_posix_stdin_is_raw.store((t.c_lflag & (ICANON | ECHO)) == 0, std::memory_order_release); g_posix_tcsetattr_result.store(tcsetattr(STDIN_FILENO, TCSANOW, &t), std::memory_order_release); struct stat* volatile null_stat = nullptr; struct termios* volatile null_termios = nullptr; const bool fstat_efault = fstat(STDIN_FILENO, null_stat) == -1 && errno == EFAULT; const bool tcgetattr_efault = tcgetattr(STDIN_FILENO, null_termios) == -1 && errno == EFAULT; g_posix_null_buffers_efault.store(fstat_efault && tcgetattr_efault, std::memory_order_release); struct pollfd fds { STDIN_FILENO, POLLIN, 0 }; g_posix_poll_before_write.store(poll(&fds, 1, 50), std::memory_order_release); g_posix_poll_first_done.store(true, std::memory_order_release); g_posix_poll_after_write.store(poll(&fds, 1, 1000), std::memory_order_release); return 0; } std::atomic g_closed_fstat_ebadf { false }; std::atomic g_closed_tcgetattr_ebadf { false }; std::atomic g_closed_tcsetattr_ebadf { false }; std::atomic g_closed_ioctl_ebadf { false }; std::atomic g_closed_poll_result { -2 }; std::atomic g_closed_poll_revents { -2 }; // Must never reach the process's real fd 0 (the host terminal on the simulator). int32_t closed_stdin_posix_calls_app_main(int, char*[]) { app_io_close(STDIN_FILENO); struct stat st {}; g_closed_fstat_ebadf.store(fstat(STDIN_FILENO, &st) == -1 && errno == EBADF, std::memory_order_release); struct termios t {}; g_closed_tcgetattr_ebadf.store(tcgetattr(STDIN_FILENO, &t) == -1 && errno == EBADF, std::memory_order_release); g_closed_tcsetattr_ebadf.store(tcsetattr(STDIN_FILENO, TCSANOW, &t) == -1 && errno == EBADF, std::memory_order_release); struct winsize ws {}; g_closed_ioctl_ebadf.store(ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1 && errno == EBADF, std::memory_order_release); struct pollfd fds { STDIN_FILENO, POLLIN, 0 }; g_closed_poll_result.store(poll(&fds, 1, 1000), std::memory_order_release); g_closed_poll_revents.store(fds.revents, std::memory_order_release); return 0; } std::atomic g_exit_before { false }; std::atomic g_exit_after { false }; int32_t exit_app_main(int, char*[]) { g_exit_before.store(true, std::memory_order_release); exit(42); g_exit_after.store(true, std::memory_order_release); return 0; } void test_signal_handler(int) { } std::atomic g_signal_first_default { false }; std::atomic g_signal_returns_previous { false }; std::atomic g_signal_sigkill_rejected { false }; std::atomic g_kill_enosys { false }; int32_t signal_app_main(int, char*[]) { g_signal_first_default.store(signal(SIGWINCH, test_signal_handler) == SIG_DFL, std::memory_order_release); g_signal_returns_previous.store(signal(SIGWINCH, SIG_IGN) == test_signal_handler, std::memory_order_release); g_signal_sigkill_rejected.store(signal(SIGKILL, test_signal_handler) == SIG_ERR && errno == EINVAL, std::memory_order_release); // Would stop the whole test process if it reached the real kill() g_kill_enosys.store(kill(0, SIGSTOP) == -1 && errno == ENOSYS, std::memory_order_release); return 0; } std::atomic g_icrnl_default_on { false }; std::atomic g_icrnl_first_read { -2 }; std::atomic g_icrnl_first_done { false }; std::atomic g_icrnl_second_read { -2 }; int32_t icrnl_app_main(int, char*[]) { struct termios t {}; tcgetattr(STDIN_FILENO, &t); g_icrnl_default_on.store((t.c_iflag & ICRNL) != 0, std::memory_order_release); char c = 0; g_icrnl_first_read.store(read(STDIN_FILENO, &c, 1) == 1 ? c : -1, std::memory_order_release); t.c_iflag &= ~ICRNL; tcsetattr(STDIN_FILENO, TCSANOW, &t); g_icrnl_first_done.store(true, std::memory_order_release); g_icrnl_second_read.store(read(STDIN_FILENO, &c, 1) == 1 ? c : -1, std::memory_order_release); return 0; } } // namespace TEST_CASE("ICRNL is on by default and makes stdin read the Enter key's \\r as \\n, until an app clears it") { ensure_memory_loader_registered(); g_icrnl_default_on.store(false, std::memory_order_relaxed); g_icrnl_first_read.store(-2, std::memory_order_relaxed); g_icrnl_first_done.store(false, std::memory_order_relaxed); g_icrnl_second_read.store(-2, std::memory_order_relaxed); AppManifest manifest { "test.libc.icrnl", "Icrnl", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(icrnl_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); TaskEventGroup event_group {}; task_event_group_construct(&event_group); uint8_t storage[16]; AppStream child_stdin {}; AppStreamBinding binding { STDIN_FILENO, &child_stdin, storage, sizeof(storage), &event_group, {}, -1 }; AppInstanceId child_id = 0; AppStartContext context; REQUIRE_EQ(app_start_context_from_id("test.libc.icrnl", &context), ERROR_NONE); app_start_context_set_streams(&context, &binding, 1); REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); app_stream_write(&child_stdin, "\r", 1); REQUIRE(wait_for_flag(g_icrnl_first_done, 1000)); app_stream_write(&child_stdin, "\r", 1); REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); CHECK(g_icrnl_default_on.load(std::memory_order_acquire)); CHECK_EQ(g_icrnl_first_read.load(std::memory_order_acquire), '\n'); CHECK_EQ(g_icrnl_second_read.load(std::memory_order_acquire), '\r'); app_stream_unsubscribe(&child_stdin); task_event_group_destruct(&event_group); app_manager_remove("test.libc.icrnl"); } TEST_CASE("signal() in an app records handlers per app instance, and kill() never reaches the process") { ensure_memory_loader_registered(); g_signal_first_default.store(false, std::memory_order_relaxed); g_signal_returns_previous.store(false, std::memory_order_relaxed); g_signal_sigkill_rejected.store(false, std::memory_order_relaxed); g_kill_enosys.store(false, std::memory_order_relaxed); AppManifest manifest { "test.libc.signal", "Signal", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(signal_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; AppStartContext context; REQUIRE_EQ(app_start_context_from_id("test.libc.signal", &context), ERROR_NONE); REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); CHECK(g_signal_first_default.load(std::memory_order_acquire)); CHECK(g_signal_returns_previous.load(std::memory_order_acquire)); CHECK(g_signal_sigkill_rejected.load(std::memory_order_acquire)); CHECK(g_kill_enosys.load(std::memory_order_acquire)); app_manager_remove("test.libc.signal"); } TEST_CASE("exit() in an app ends only that app instance") { ensure_memory_loader_registered(); g_exit_before.store(false, std::memory_order_relaxed); g_exit_after.store(false, std::memory_order_relaxed); AppManifest manifest { "test.io.exit", "Exit", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(exit_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; AppStartContext context; REQUIRE_EQ(app_start_context_from_id("test.io.exit", &context), ERROR_NONE); REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); CHECK(g_exit_before.load(std::memory_order_acquire)); CHECK_FALSE(g_exit_after.load(std::memory_order_acquire)); app_manager_remove("test.io.exit"); } TEST_CASE("fstat, ioctl, termios and poll on a closed app stdin fail with EBADF/POLLNVAL instead of reaching the real fd") { ensure_memory_loader_registered(); g_closed_fstat_ebadf.store(false, std::memory_order_relaxed); g_closed_tcgetattr_ebadf.store(false, std::memory_order_relaxed); g_closed_tcsetattr_ebadf.store(false, std::memory_order_relaxed); g_closed_ioctl_ebadf.store(false, std::memory_order_relaxed); g_closed_poll_result.store(-2, std::memory_order_relaxed); g_closed_poll_revents.store(-2, std::memory_order_relaxed); AppManifest manifest { "test.io.closed_posix_calls", "ClosedPosixCalls", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(closed_stdin_posix_calls_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; AppStartContext context; REQUIRE_EQ(app_start_context_from_id("test.io.closed_posix_calls", &context), ERROR_NONE); REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); CHECK(g_closed_fstat_ebadf.load(std::memory_order_acquire)); CHECK(g_closed_tcgetattr_ebadf.load(std::memory_order_acquire)); CHECK(g_closed_tcsetattr_ebadf.load(std::memory_order_acquire)); CHECK(g_closed_ioctl_ebadf.load(std::memory_order_acquire)); CHECK_EQ(g_closed_poll_result.load(std::memory_order_acquire), 1); CHECK_EQ(g_closed_poll_revents.load(std::memory_order_acquire), POLLNVAL); app_manager_remove("test.io.closed_posix_calls"); } TEST_CASE("fstat, termios and poll on a bound app stdin report a raw character device that becomes readable") { ensure_memory_loader_registered(); g_posix_stdin_is_char_device.store(false, std::memory_order_relaxed); g_posix_tcgetattr_result.store(-2, std::memory_order_relaxed); g_posix_stdin_is_raw.store(false, std::memory_order_relaxed); g_posix_tcsetattr_result.store(-2, std::memory_order_relaxed); g_posix_poll_before_write.store(-2, std::memory_order_relaxed); g_posix_poll_after_write.store(-2, std::memory_order_relaxed); g_posix_poll_first_done.store(false, std::memory_order_relaxed); g_posix_null_buffers_efault.store(false, std::memory_order_relaxed); AppManifest manifest { "test.io.posix_calls", "PosixCalls", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(posix_calls_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); TaskEventGroup event_group {}; task_event_group_construct(&event_group); uint8_t storage[16]; AppStream child_stdin {}; AppStreamBinding binding { STDIN_FILENO, &child_stdin, storage, sizeof(storage), &event_group, {}, -1 }; AppInstanceId child_id = 0; AppStartContext context; REQUIRE_EQ(app_start_context_from_id("test.io.posix_calls", &context), ERROR_NONE); app_start_context_set_streams(&context, &binding, 1); REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); REQUIRE(wait_for_flag(g_posix_poll_first_done, 1000)); app_stream_write(&child_stdin, "x", 1); REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); CHECK(g_posix_stdin_is_char_device.load(std::memory_order_acquire)); CHECK_EQ(g_posix_tcgetattr_result.load(std::memory_order_acquire), 0); CHECK(g_posix_stdin_is_raw.load(std::memory_order_acquire)); CHECK_EQ(g_posix_tcsetattr_result.load(std::memory_order_acquire), 0); CHECK(g_posix_null_buffers_efault.load(std::memory_order_acquire)); CHECK_EQ(g_posix_poll_before_write.load(std::memory_order_acquire), 0); CHECK_EQ(g_posix_poll_after_write.load(std::memory_order_acquire), 1); app_stream_unsubscribe(&child_stdin); task_event_group_destruct(&event_group); app_manager_remove("test.io.posix_calls"); } TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() calls too") { ensure_memory_loader_registered(); AppManifest parent_manifest { "test.app.execute.printf_parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) } }; REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE); uint32_t parent_id = 0; AppStartContext parent_context; REQUIRE_EQ(app_start_context_from_id("test.app.execute.printf_parent", &parent_context), ERROR_NONE); REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE); CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); TaskEventGroup parent_event_group {}; task_event_group_construct(&parent_event_group); AppEventSubscription parent_sub {}; REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); uint8_t storage[64]; AppStream child_stdout {}; AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &parent_event_group, {}, -1 }; AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(printf_stream_writer_app_main) }; uint32_t child_id = 0; AppStartContext context = app_start_context_for_location(location); app_start_context_set_streams(&context, &binding, 1); app_start_context_set_parent(&context, parent_id); REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); std::vector received; while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { uint8_t chunk[16]; size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk)); if (n == 0) { break; // EOF } received.insert(received.end(), chunk, chunk + n); } REQUIRE_EQ(received.size(), 3u); CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0); REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); AppEvent event {}; REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE); CHECK_EQ(event.type, APP_EVENT_RESULT); CHECK_EQ(event.result.launch_id, child_id); CHECK_EQ(event.result.result, 7); app_stream_unsubscribe(&child_stdout); app_event_unsubscribe(&parent_sub); task_event_group_destruct(&parent_event_group); app_manager_stop(child_id); app_manager_stop(parent_id); app_manager_remove("test.app.execute.printf_parent"); }