Skip to content

Commit f87a88b

Browse files
CopilotdazzaXx
andauthored
Reviewing codebase for stability and critical bugs (#195)
* fix: reject invalid switchNumber(-1), mutex init guard, ABS BUTTON_TEST/SYSTEM guard, CMD_SET_COMMS_MODE batch fix + 3 new tests Agent-Logs-Url: https://github.com/dazzaXx/ModernJVS/sessions/7c312b9a-49b0-456f-ba5a-d9b806de9e20 Co-authored-by: dazzaXx <48131734+dazzaXx@users.noreply.github.com> * Fix missing continue for unmapped devices (!autoDetect); fix misleading test comment Agent-Logs-Url: https://github.com/dazzaXx/ModernJVS/sessions/67639733-14aa-44fb-b17d-4fa43fab2445 Co-authored-by: dazzaXx <48131734+dazzaXx@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dazzaXx <48131734+dazzaXx@users.noreply.github.com>
1 parent 5195675 commit f87a88b

5 files changed

Lines changed: 180 additions & 11 deletions

File tree

src/controller/input.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,19 @@ static void *deviceThread(void *_args)
574574
if (args->inputs.abs[event.code].secondaryIO && args->jvsIO->chainedIO != NULL)
575575
io = args->jvsIO->chainedIO;
576576

577+
/* Mirror the EV_KEY BUTTON_TEST guard: BUTTON_TEST and BUTTON_3
578+
* share the same numeric value (0x80); if either is mapped to the
579+
* SYSTEM player on a HAT axis, route through the test-button latch
580+
* rather than calling setSwitch directly to keep behaviour consistent
581+
* with the key-based test button path. */
582+
if (args->inputs.abs[event.code].output == BUTTON_TEST &&
583+
args->inputs.abs[event.code].jvsPlayer == SYSTEM)
584+
{
585+
if (event.value != 0 && __atomic_load_n(&args->jvsIO->deviceID, __ATOMIC_ACQUIRE) != -1)
586+
__sync_fetch_and_xor(&testButtonActive, 1);
587+
continue;
588+
}
589+
577590
if (event.value == args->inputs.absMin[event.code])
578591
{
579592
/* Activate primary direction and clear secondary to prevent
@@ -618,6 +631,18 @@ static void *deviceThread(void *_args)
618631
}
619632
continue;
620633
}
634+
635+
/* Mirror the EV_KEY BUTTON_TEST guard (BUTTON_TEST == BUTTON_3 == 0x80):
636+
* route through the latch rather than calling setSwitch directly. */
637+
if (args->inputs.key[event.code].output == BUTTON_TEST &&
638+
args->inputs.key[event.code].jvsPlayer == SYSTEM)
639+
{
640+
if (event.value != args->inputs.absMin[event.code] &&
641+
__atomic_load_n(&args->jvsIO->deviceID, __ATOMIC_ACQUIRE) != -1)
642+
__sync_fetch_and_xor(&testButtonActive, 1);
643+
continue;
644+
}
645+
621646
else if (event.value == args->inputs.absMin[event.code])
622647
{
623648
setSwitch(io, args->inputs.key[event.code].jvsPlayer, args->inputs.key[event.code].output, 0);
@@ -1378,6 +1403,11 @@ JVSInputStatus initInputs(char *outputMappingPath, char *configPath, char *secon
13781403
continue;
13791404
}
13801405
}
1406+
/* When autoDetect is off (or no generic map matched), skip any
1407+
* device that still has no mappings rather than starting a
1408+
* pointless thread that never produces JVS output. */
1409+
if (inputMappings.length == 0)
1410+
continue;
13811411
}
13821412

13831413
EVInputs evInputs = {0};

src/jvs/io.c

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,13 @@ int initIO(JVSIO *io)
122122
/* Destroy any previous mutex before re-initialising. POSIX states that
123123
* calling pthread_mutex_init on an already-initialised mutex without a
124124
* preceding pthread_mutex_destroy is undefined behaviour and leaks the
125-
* kernel resource on Linux. The destroy is a no-op if the mutex was
126-
* never initialised (the struct is zero-initialised in main). */
127-
pthread_mutex_destroy(&io->state_mutex);
125+
* kernel resource on Linux. Guard with mutexInitialized == 1 so that
126+
* the first call (where the field is zero or garbage) skips the destroy
127+
* and avoids UB on a non-zero-initialised struct. */
128+
if (io->mutexInitialized == 1)
129+
pthread_mutex_destroy(&io->state_mutex);
128130
pthread_mutex_init(&io->state_mutex, NULL);
131+
io->mutexInitialized = 1;
129132

130133
return 1;
131134
}
@@ -139,6 +142,15 @@ int setSwitch(JVSIO *io, JVSPlayer player, JVSInput switchNumber, int value)
139142
return 0;
140143
}
141144

145+
/* Reject an invalid (out-of-range) switch number. jvsInputFromString()
146+
* returns (JVSInput)-1 when a button name is not found; using -1 in the
147+
* bitwise OR/AND below would corrupt the entire switch state word. */
148+
if ((int)switchNumber < 0)
149+
{
150+
debug(0, "Error: Invalid switch number %d — likely an unrecognised button name in a mapping file.\n", (int)switchNumber);
151+
return 0;
152+
}
153+
142154
pthread_mutex_lock(&io->state_mutex);
143155
if (value)
144156
io->state.inputSwitch[player] |= switchNumber;

src/jvs/io.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ typedef struct
112112
typedef struct JVSIO
113113
{
114114
int deviceID;
115+
/* Set to 1 by initIO after the first pthread_mutex_init call so that
116+
* subsequent initIO calls can safely call pthread_mutex_destroy first.
117+
* Checked with == 1 (not just != 0) so that a struct poisoned with
118+
* memset(0xFF) skips the destroy, which is the correct safe behaviour. */
119+
int mutexInitialized;
115120
int analogueRestBits;
116121
int gunXRestBits;
117122
int gunYRestBits;

src/jvs/jvs.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,17 +512,28 @@ JVSStatus processPacket(JVSIO *jvsIO)
512512

513513
/* CMD_SET_COMMS_MODE is a broadcast-only command (NODE NO.FF) and the JVS
514514
* spec defines its response size as 0 — no acknowledge should be sent.
515-
* Return immediately without calling writePacket so nothing is transmitted. */
515+
* If it arrives as the sole command in a packet, return immediately so
516+
* nothing is transmitted. If it appears after other commands in a batch
517+
* (non-compliant but possible with buggy arcade hardware), break out of
518+
* the loop so the responses already assembled for the prior commands are
519+
* delivered rather than silently dropped. */
516520
case CMD_SET_COMMS_MODE:
517521
{
522+
size = 2; /* command byte + mode byte */
518523
if (index + 1 >= (int)inputPacket.length - 1)
519524
{
520525
debug(0, "Error: CMD_SET_COMMS_MODE - packet too short\n");
521526
return JVS_STATUS_ERROR;
522527
}
523528
debug(1, "CMD_SET_COMMS_MODE - Mode 0x%02X (no response required)\n", inputPacket.data[index + 1]);
524-
return JVS_STATUS_SUCCESS;
529+
/* outputPacket.length == 1 means only the STATUS_SUCCESS header byte
530+
* was written, i.e. no prior commands produced any output — safe to
531+
* return without sending anything. */
532+
if (outputPacket.length <= 1)
533+
return JVS_STATUS_SUCCESS;
534+
/* Otherwise fall through to writePacket via the break. */
525535
}
536+
break;
526537

527538
/* Ask for the name of the IO board */
528539
case CMD_REQUEST_ID:

src/testsuite/test_modernjvs.c

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,73 @@ static void test_jvsInputFromString_unknown(void)
747747

748748
/* ── New tests for PR bug-fixes ──────────────────────────────────────────── */
749749

750+
/*
751+
* setSwitch must reject a negative switchNumber.
752+
* jvsInputFromString("BAD_NAME") returns (JVSInput)-1; before the fix that
753+
* value was used directly in a bitwise OR, setting ALL bits of inputSwitch
754+
* and making every button appear simultaneously pressed on the JVS bus.
755+
*/
756+
static void test_setSwitch_invalid_switch_number(void)
757+
{
758+
TEST_BEGIN(test_setSwitch_invalid_switch_number);
759+
JVSIO io = make_test_io();
760+
761+
/* Ensure the state starts clean */
762+
ASSERT_EQ_INT(io.state.inputSwitch[1], 0, "P1 switch state starts at 0");
763+
764+
/* Pass the -1 sentinel returned by jvsInputFromString on lookup failure */
765+
int r = setSwitch(&io, PLAYER_1, (JVSInput)-1, 1);
766+
ASSERT_EQ_INT(r, 0, "setSwitch with switchNumber -1 must return 0");
767+
768+
/* The switch state must not be corrupted (must still be 0, not 0xFFFF) */
769+
ASSERT_EQ_INT(io.state.inputSwitch[1], 0,
770+
"switch state must not be corrupted by invalid switchNumber");
771+
772+
/* Also verify the release path doesn't clear everything */
773+
setSwitch(&io, PLAYER_1, BUTTON_START, 1);
774+
setSwitch(&io, PLAYER_1, (JVSInput)-1, 0);
775+
ASSERT(io.state.inputSwitch[1] & BUTTON_START,
776+
"valid BUTTON_START bit must survive invalid-switch release");
777+
778+
TEST_PASS();
779+
}
780+
781+
/*
782+
* Calling initIO() twice on the same JVSIO must not produce undefined behaviour.
783+
* The first call initialises the mutex; the second call must destroy then
784+
* re-initialise it safely, confirmed by the mutexInitialized guard introduced
785+
* to prevent pthread_mutex_destroy on a garbage-initialised struct.
786+
*/
787+
static void test_initIO_reinit_mutex_safety(void)
788+
{
789+
TEST_BEGIN(test_initIO_reinit_mutex_safety);
790+
791+
JVSIO io;
792+
memset(&io, 0, sizeof(io));
793+
io.capabilities.players = 2;
794+
io.capabilities.analogueInChannels = 2;
795+
io.capabilities.analogueInBits = 10;
796+
io.capabilities.coins = 2;
797+
io.capabilities.rotaryChannels = 0;
798+
io.capabilities.gunChannels = 0;
799+
800+
int r1 = initIO(&io);
801+
ASSERT_EQ_INT(r1, 1, "first initIO returns 1");
802+
ASSERT_EQ_INT(io.mutexInitialized, 1, "mutexInitialized set after first init");
803+
804+
/* Set some state, then re-init; state must be zeroed again */
805+
io.state.inputSwitch[1] = 0xFFFF;
806+
int r2 = initIO(&io);
807+
ASSERT_EQ_INT(r2, 1, "second initIO returns 1");
808+
ASSERT_EQ_INT(io.state.inputSwitch[1], 0, "state zeroed by second initIO");
809+
810+
/* The mutex must still be usable after re-init */
811+
int r3 = setSwitch(&io, PLAYER_1, BUTTON_START, 1);
812+
ASSERT_EQ_INT(r3, 1, "setSwitch works after re-init");
813+
814+
TEST_PASS();
815+
}
816+
750817
/*
751818
* setSwitch must reject a negative player index.
752819
* jvsPlayerFromString("INVALID") returns (JVSPlayer)-1, which before the fix
@@ -2508,12 +2575,13 @@ static void test_processPacket_cmd_remaining_payout_two_slots(void)
25082575
ASSERT(r.valid == 1, "response valid");
25092576
ASSERT_EQ_INT(r.data[0], STATUS_SUCCESS, "STATUS_SUCCESS");
25102577
ASSERT_EQ_INT(r.data[1], REPORT_SUCCESS, "REPORT_SUCCESS");
2511-
/* 2 slots × 2 bytes each = 4 data bytes, all zero */
2512-
ASSERT_EQ_INT(r.data[2], 0x00, "slot1 high = 0");
2513-
ASSERT_EQ_INT(r.data[3], 0x00, "slot1 low = 0");
2514-
ASSERT_EQ_INT(r.data[4], 0x00, "slot2 high = 0");
2515-
ASSERT_EQ_INT(r.data[5], 0x00, "slot2 low = 0");
2516-
/* Total response data length: STATUS(1) + REPORT(1) + 2*2 = 6 */
2578+
/* Single-slot response: REPORT(1) + hopper_status(1) + remaining_hi(1)
2579+
* + remaining_mid(1) + remaining_lo(1) = 5 bytes; all zero because we
2580+
* always report 0 remaining. Total: STATUS(1) + 5 = 6. */
2581+
ASSERT_EQ_INT(r.data[2], 0x00, "hopper_status = 0");
2582+
ASSERT_EQ_INT(r.data[3], 0x00, "remaining_hi = 0");
2583+
ASSERT_EQ_INT(r.data[4], 0x00, "remaining_mid = 0");
2584+
ASSERT_EQ_INT(r.data[5], 0x00, "remaining_lo = 0");
25172585
ASSERT_EQ_INT(r.data_len, 6, "data_len = 6");
25182586

25192587
close(sv[0]); close(sv[1]); serialIO = -1;
@@ -2997,6 +3065,46 @@ static void test_processPacket_cmd_set_comms_mode(void)
29973065
TEST_PASS();
29983066
}
29993067

3068+
/*
3069+
* CMD_SET_COMMS_MODE appearing after another command in a batch must NOT
3070+
* silently discard the REPORT bytes already assembled for the prior command.
3071+
* Before the fix the handler did an unconditional `return`, dropping any
3072+
* partially-built response.
3073+
*
3074+
* Batch: CMD_COMMAND_VERSION | CMD_SET_COMMS_MODE 0x01
3075+
* Expected response: STATUS_SUCCESS + REPORT_SUCCESS + commandVersion byte
3076+
* (the SET_COMMS_MODE does not add a REPORT byte but the version reply
3077+
* must be present in the wire output).
3078+
*/
3079+
static void test_processPacket_cmd_set_comms_mode_batch_preserves_prior(void)
3080+
{
3081+
TEST_BEGIN(test_processPacket_cmd_set_comms_mode_batch_preserves_prior);
3082+
3083+
JVSIO io = make_test_io();
3084+
io.deviceID = 0x01;
3085+
int sv[2];
3086+
int afd = open_test_socket(sv);
3087+
ASSERT(afd >= 0, "socketpair");
3088+
3089+
/* Build a two-command batch: CMD_COMMAND_VERSION (no args) followed by
3090+
* CMD_SET_COMMS_MODE 0x01. */
3091+
unsigned char cmd[] = {CMD_COMMAND_VERSION, CMD_SET_COMMS_MODE, 0x01};
3092+
JVSStatus s = run_processPacket(&io, afd, 0x01, cmd, 3);
3093+
ASSERT(s == JVS_STATUS_SUCCESS, "batch with CMD_SET_COMMS_MODE SUCCESS");
3094+
3095+
/* The response for CMD_COMMAND_VERSION must be present; without the fix
3096+
* the SET_COMMS_MODE early-return would have dropped it entirely. */
3097+
JVSResponse r = jvs_read_response(afd);
3098+
ASSERT(r.valid == 1, "response checksum valid");
3099+
ASSERT_EQ_INT(r.data[0], STATUS_SUCCESS, "STATUS_SUCCESS present");
3100+
ASSERT_EQ_INT(r.data[1], REPORT_SUCCESS, "REPORT_SUCCESS for CMD_COMMAND_VERSION");
3101+
ASSERT_EQ_INT(r.data[2], io.capabilities.commandVersion,
3102+
"commandVersion byte preserved in response");
3103+
3104+
close(sv[0]); close(sv[1]); serialIO = -1;
3105+
TEST_PASS();
3106+
}
3107+
30003108
static void test_processPacket_cmd_read_keypad(void)
30013109
{
30023110
TEST_BEGIN(test_processPacket_cmd_read_keypad);
@@ -3459,6 +3567,7 @@ static const TestFn tests[] = {
34593567
test_jvsPlayerFromString_known,
34603568
test_jvsPlayerFromString_unknown,
34613569
/* New bounds-check tests (PR bug-fixes) */
3570+
test_setSwitch_invalid_switch_number,
34623571
test_setSwitch_negative_player,
34633572
test_setAnalogue_negative_channel,
34643573
test_setGun_negative_channel,
@@ -3468,6 +3577,7 @@ static const TestFn tests[] = {
34683577
test_initIO_oversized_capabilities,
34693578
test_initIO_zero_analogue_bits,
34703579
test_initIO_oversized_analogue_bits,
3580+
test_initIO_reinit_mutex_safety,
34713581
test_initJVS_right_align_bits,
34723582
test_initJVS_chained_io_rest_bits,
34733583
/* Config parsing */
@@ -3543,6 +3653,7 @@ static const TestFn tests[] = {
35433653
test_processPacket_cmd_remaining_payout_two_slots,
35443654
/* Additional command coverage */
35453655
test_processPacket_cmd_set_comms_mode,
3656+
test_processPacket_cmd_set_comms_mode_batch_preserves_prior,
35463657
test_processPacket_cmd_read_keypad,
35473658
test_processPacket_cmd_write_gpo_bit,
35483659
test_processPacket_cmd_write_analog,

0 commit comments

Comments
 (0)