From cd6ddfe00afd38ee7cd8700d74e03cd742d87f4a Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 15:21:45 -0400 Subject: [PATCH] fix(device): route developer input through SDL console --- src/sendkeys/sendkeys.c | 89 +++++++++++++++++++++++++++---- test/shell/sendkeys_contract.bats | 22 ++++++++ test/test_ci_scope.sh | 5 ++ tools/ci-scope.sh | 7 ++- 4 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/sendkeys/sendkeys.c b/src/sendkeys/sendkeys.c index 9da725330..1157103bc 100644 --- a/src/sendkeys/sendkeys.c +++ b/src/sendkeys/sendkeys.c @@ -3,32 +3,99 @@ #include #include #include +#include +#include #include #include "utils/log.h" +static void usage(void) +{ + printf("Usage: sendkeys [--sdl] [[CODE] [VALUE], ...]\n" + "Values: 0 - released, 1 - pressed, 2 - repeating\n"); +} + +static int parse_integer(const char *text, int minimum, int maximum, int *value) +{ + if (text == NULL || *text == '\0' || value == NULL) + return -1; + char *end = NULL; + long parsed = strtol(text, &end, 10); + if (*end != '\0' || parsed < minimum || parsed > maximum) + return -1; + *value = (int)parsed; + return 0; +} + +static int developer_mode_enabled(void) +{ + struct stat metadata; + return lstat("/mnt/SDCARD/.bloom-dev", &metadata) == 0 && + S_ISREG(metadata.st_mode) && !S_ISLNK(metadata.st_mode); +} + +static int inject_sdl_console(const struct input_event *events, int num_events) +{ + if (!developer_mode_enabled()) + return 1; + struct stat metadata; + if (lstat("/dev/tty2", &metadata) != 0 || !S_ISCHR(metadata.st_mode) || + S_ISLNK(metadata.st_mode)) + return 1; + int input_fd = + open("/dev/tty2", O_WRONLY | O_NOCTTY | O_CLOEXEC | O_NOFOLLOW); + if (input_fd < 0) + return 1; + for (int index = 0; index < num_events; ++index) { + if (events[index].code > 127 || events[index].value > 1) { + close(input_fd); + return 1; + } + unsigned char scan_code = (unsigned char)events[index].code; + if (events[index].value == 0) + scan_code |= 0x80; + if (ioctl(input_fd, TIOCSTI, &scan_code) != 0) { + close(input_fd); + return 1; + } + } + return close(input_fd) == 0 ? 0 : 1; +} + int main(int argc, char *argv[]) { struct input_event events[100]; - - if (argc < 3 || argc % 2 == 0 || argc > 201) { - printf("Usage: sendkeys [[CODE] [VALUE], ...]\nValues: 0 - released, 1 " - "- pressed, 2 - repeating\n"); + int first_argument = 1; + int sdl_console = argc > 1 && strcmp(argv[1], "--sdl") == 0; + if (sdl_console) + first_argument++; + int argument_count = argc - first_argument; + if (argument_count < 2 || argument_count % 2 != 0 || argument_count > 200) { + usage(); return 1; } - int num_events = (argc - 1) / 2; + int num_events = argument_count / 2; memset(events, 0, sizeof(events)); - for (int i = 1; i < argc; i += 2) { - int ev_idx = (i - 1) / 2; + for (int i = first_argument; i < argc; i += 2) { + int ev_idx = (i - first_argument) / 2; events[ev_idx].type = EV_KEY; - events[ev_idx].code = atoi(argv[i]); - events[ev_idx].value = atoi(argv[i + 1]); + int code = 0; + int value = 0; + if (parse_integer(argv[i], 0, sdl_console ? 127 : KEY_MAX, &code) != 0 || + parse_integer(argv[i + 1], 0, sdl_console ? 1 : 2, &value) != 0) { + usage(); + return 1; + } + events[ev_idx].code = (__u16)code; + events[ev_idx].value = value; } - int input_fd; - input_fd = open("/dev/input/event0", O_WRONLY); + if (sdl_console) + return inject_sdl_console(events, num_events); + + int input_fd = open("/dev/input/event0", O_WRONLY | O_CLOEXEC); if (input_fd < 0) return 1; diff --git a/test/shell/sendkeys_contract.bats b/test/shell/sendkeys_contract.bats index 08bebbaef..41998155f 100644 --- a/test/shell/sendkeys_contract.bats +++ b/test/shell/sendkeys_contract.bats @@ -8,3 +8,25 @@ grep -F 'write(input_fd, &sync_event, sizeof(sync_event)) != sizeof(sync_event)' "$source_file" grep -F 'if (input_fd < 0)' "$source_file" } + +@test "developer SDL injection is explicit bounded and console-scoped" { + source_file=/workspace/src/sendkeys/sendkeys.c + + grep -F 'strcmp(argv[1], "--sdl") == 0' "$source_file" + grep -F 'lstat("/mnt/SDCARD/.bloom-dev", &metadata)' "$source_file" + grep -F 'S_ISREG(metadata.st_mode) && !S_ISLNK(metadata.st_mode)' "$source_file" + grep -F 'lstat("/dev/tty2", &metadata) != 0 || !S_ISCHR(metadata.st_mode)' "$source_file" + grep -F 'open("/dev/tty2", O_WRONLY | O_NOCTTY | O_CLOEXEC | O_NOFOLLOW)' "$source_file" + grep -F 'events[index].code > 127 || events[index].value > 1' "$source_file" + grep -F 'scan_code |= 0x80;' "$source_file" + grep -F 'ioctl(input_fd, TIOCSTI, &scan_code) != 0' "$source_file" +} + +@test "hardware input helper rejects nonnumeric and out of range values" { + source_file=/workspace/src/sendkeys/sendkeys.c + + grep -F 'strtol(text, &end, 10)' "$source_file" + grep -F "*end != '\\0' || parsed < minimum || parsed > maximum" "$source_file" + grep -F 'parse_integer(argv[i], 0, sdl_console ? 127 : KEY_MAX, &code)' "$source_file" + grep -F 'parse_integer(argv[i + 1], 0, sdl_console ? 1 : 2, &value)' "$source_file" +} diff --git a/test/test_ci_scope.sh b/test/test_ci_scope.sh index 22afd6a86..c6532c853 100755 --- a/test/test_ci_scope.sh +++ b/test/test_ci_scope.sh @@ -35,6 +35,8 @@ expect "src/common/utils/sdl_direct_fb.h src/bootScreen src/gameSwitcher src/inf expect "src/common/theme src/bootScreen src/gameSwitcher src/infoPanel src/keymon src/mainUiBatPerc src/prompt src/themeSwitcher src/tweaks" static-analysis "src/common/theme/config.h" expect "src/bootScreen src/chargingState" static-analysis "src/bootScreen/bootScreen.c src/chargingState/chargingState.c" +expect "sendkeys" build "src/sendkeys/sendkeys.c" +expect "src/sendkeys" static-analysis "src/sendkeys/sendkeys.c" expect "full" build "src/common/system/display.h" expect "full" static-analysis "src/common/system/display.h" expect "bloomShell gameSwitcher" build "src/bloomShell/bloom_shell_settings.h" @@ -151,6 +153,9 @@ expect "2" risk "build/libretro-artwork-policy.json tools/validate_libretro_artwork_policy.py" expect "full" native-tests "src/common/system/display.h" expect "none" native-tests "docs/ARCHITECTURE.md" +expect "2" risk "src/sendkeys/sendkeys.c" +expect "none" native-tests "src/sendkeys/sendkeys.c" +expect "/workspace/test/shell/sendkeys_contract.bats" shell-tests "src/sendkeys/sendkeys.c" expect "BloomUiRenderer*" visual-tests "src/bloomUi/bloom_ui_renderer.c" expect "BloomUi*" native-tests "test/test_bloom_ui_components.cpp" expect "BloomUiRenderer*" visual-tests "test/test_bloom_ui_components.cpp" diff --git a/tools/ci-scope.sh b/tools/ci-scope.sh index 73470ea20..d86668af8 100755 --- a/tools/ci-scope.sh +++ b/tools/ci-scope.sh @@ -260,7 +260,7 @@ while IFS= read -r path; do fi ;; src/bootScreen/*|src/chargingState/*|src/installUI/*|src/mainUiBatPerc/*|src/prompt/*|\ - src/themeSwitcher/*|src/tweaks/*) + src/sendkeys/*|src/themeSwitcher/*|src/tweaks/*) component=$(printf '%s\n' "$path" | cut -d/ -f2) if [ "$mode" = build ]; then add_target "$component" @@ -537,6 +537,11 @@ while IFS= read -r path; do add_native_filter 'GameSwitcher*' add_native_filter 'test_infoPanel*' ;; + src/sendkeys/*) + raise_risk 2 + native_none=1 + add_shell_target /workspace/test/shell/sendkeys_contract.bats + ;; tools/bloom-device|tools/dev-deploy-manifest.py) raise_risk 2 add_shell_target /workspace/test/shell/bloom_device.bats