diff --git a/src/lib/testsuite/test_ctrlstatus.c b/src/lib/testsuite/test_ctrlstatus.c index c8c1390..ba4d1b0 100644 --- a/src/lib/testsuite/test_ctrlstatus.c +++ b/src/lib/testsuite/test_ctrlstatus.c @@ -446,10 +446,46 @@ static void test_handle_reply_ctrlstatus_does_not_free_foreign_slot(void) printf("test_handle_reply_ctrlstatus_does_not_free_foreign_slot: PASS\n"); } +/* bug-15xx (job peek 501 "nil reply"): a fire-and-forget nop/heartbeat reply + * must NOT wake the shared single-slot wbuf — xce_handle_reply returning 0 here + * is what keeps vtask_entry from calling xce_wakeup, so the stale nop status can + * never be consumed by the next blocking request as its own reply. Also proves a + * foreign (poisoned) union slot is left untouched, mirroring the bug-1077 + * ctrlstatus assertion. */ +static void test_handle_reply_nop_does_not_wake(void) +{ + xce_ctx_t ctx; + memset(&ctx, 0, sizeof(ctx)); + + /* A foreign occupant in the shared reply union must survive a nop reply + * untouched (no free, no deref) — same discipline as bug-1077. */ + xce_fetchfile_reply_t* sentinel = (xce_fetchfile_reply_t*)calloc(1, sizeof(*sentinel)); + TEST_ASSERT_NOT_NULL(sentinel); + ctx.reply.u.fetchfilereply = sentinel; /* same 8 bytes as any other slot */ + + xce_reply_t reply; + memset(&reply, 0, sizeof(reply)); + reply.opcode = OPCode_nop; + reply.status = 0; + reply.data_which = RequestReply_data_nodata; + + int rc = xce_handle_reply(&ctx, &reply); + + /* rc==0 is the contract vtask_entry relies on to skip xce_wakeup: the nop + * reply must not become a pending status in wbuf. */ + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_PTR(sentinel, ctx.reply.u.fetchfilereply); + + free(sentinel); + + printf("test_handle_reply_nop_does_not_wake: PASS\n"); +} + /* ── Suite entry point ──────────────────────────────────────────────────── */ void test_ctrlstatus(void) { + RUN_TEST(test_handle_reply_nop_does_not_wake); RUN_TEST(test_handle_reply_ctrlstatus_does_not_free_foreign_slot); RUN_TEST(test_ctrlstatus_roundtrip_all_fields); RUN_TEST(test_ctrlstatus_roundtrip_unknown_primary); diff --git a/src/lib/xcectx.c b/src/lib/xcectx.c index e750429..f8d0e47 100644 --- a/src/lib/xcectx.c +++ b/src/lib/xcectx.c @@ -69,6 +69,20 @@ int xce_handle_reply(xce_ctx_t* ctx, xce_reply_t* reply) xce_ctx_capture_redirect_hint(ctx, reply->redirect_host, reply->redirect_port); + /* bug-15xx (job peek 501 "nil reply"): a nop/heartbeat reply is fire-and- + * forget on the send side — xce_nop/xce_heartbeat sends it and never waits + * for the reply. Waking the shared single-slot wbuf here therefore leaves a + * stale status sitting in the buffer that the NEXT blocking request + * (xce_fetchfile / xce_jobsub / xce_jobctrl ...) consumes as its own reply: + * the fetchfile waiter wakes with status=0 but ctx->reply.u.fetchfilereply + * still NULL -> "nil reply" -> 501. Swallow nop replies entirely so they + * never touch wbuf; the reply itself is freed by the caller's free_Request. + * (The control-op replies -- jobctrl/queuectrl/hostctrl -- legitimately hit + * the default case below and MUST wake: their callers block on wbuf.) */ + if (reply->opcode == OPCode_nop) { + return 0; + } + if (reply->status < 0) { return reply->status; }