tests: cover application-driven channels

wolfSSH_SetAppChannels() changes where wolfSSH_accept() stops and what
becomes of a session request with no callback behind it, so both modes
are exercised.

- regress.c drives a server with the pivot on, one with a shell
  callback and one without, and checks accept() stops at
  ACCEPT_SERVER_USERAUTH_SENT
- regress.c pins the context setter, the session's inheritance of it,
  and that turning it on after accept() established the session still
  returns
- regress.c re-enters a parked accept() with output still queued, which
  is the one path that flushes before reading the state, and pins that
  it leaves the state on the stop
- unit.c checks DoChannelRequest() refuses a shell, exec and subsystem
  request with no callback once the pivot is on
pull/1235/head
John Safranek 2026-09-02 09:49:31 -07:00 committed by Paul Adelsbach
parent b723519312
commit 2c832010d2
2 changed files with 279 additions and 0 deletions

View File

@ -1490,6 +1490,185 @@ static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo,
}
#ifndef WOLFSSH_NO_RSA_SHA2_256
/* Counts the shell requests the application-driven server answered. */
static int appChannelsShellReqCount;
static int AppChannelsShellCb(WOLFSSH_CHANNEL* channel, void* ctx)
{
(void)channel;
(void)ctx;
appChannelsShellReqCount++;
return 0;
}
/* Drive an application-driven server: wolfSSH_accept() is expected to return
* at userauth, so the channel open and the shell request are answered by
* wolfSSH_worker() calls the application makes itself. */
static void RunAppChannelsHandshake(KexReplyHarness* harness,
KexReplyRunResult* result)
{
word32 step;
WMEMSET(result, 0, sizeof(*result));
result->clientRet = WS_FATAL_ERROR;
result->serverRet = WS_FATAL_ERROR;
for (step = 0; step < REGRESS_MAX_HANDSHAKE_STEPS; step++) {
if (!result->clientSuccess) {
result->clientRet = wolfSSH_connect(harness->client);
result->clientErr = wolfSSH_get_error(harness->client);
if (result->clientRet == WS_SUCCESS) {
result->clientSuccess = 1;
}
else if (!IsHandshakeRetryable(result->clientErr)) {
result->steps = step + 1;
return;
}
}
if (!result->serverSuccess) {
result->serverRet = wolfSSH_accept(harness->server);
result->serverErr = wolfSSH_get_error(harness->server);
if (result->serverRet == WS_SUCCESS) {
result->serverSuccess = 1;
}
else if (!IsHandshakeRetryable(result->serverErr)) {
result->steps = step + 1;
return;
}
}
else if (harness->server->clientState < CLIENT_DONE) {
result->serverRet = wolfSSH_worker(harness->server, NULL);
result->serverErr = wolfSSH_get_error(harness->server);
if (result->serverRet < WS_SUCCESS
&& result->serverErr != WS_CHAN_RXD
&& !IsHandshakeRetryable(result->serverErr)) {
result->steps = step + 1;
return;
}
}
if (result->clientSuccess && result->serverSuccess
&& harness->server->clientState >= CLIENT_DONE) {
result->steps = step + 1;
return;
}
}
result->steps = REGRESS_MAX_HANDSHAKE_STEPS;
}
/* With wolfSSH_SetAppChannels() on, accept() stops once the user is
* authenticated and the shell request lands on the callback instead. */
static void TestAppChannelsAcceptStopsAtUserAuth(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
appChannelsShellReqCount = 0;
InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH,
0, NULL);
AssertIntEQ(wolfSSH_CTX_SetChannelReqShellCb(harness.serverCtx,
AppChannelsShellCb), WS_SUCCESS);
AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS);
RunAppChannelsHandshake(&harness, &result);
AssertTrue(result.clientSuccess);
AssertTrue(result.serverSuccess);
AssertIntEQ(harness.server->acceptState, ACCEPT_SERVER_USERAUTH_SENT);
AssertIntEQ(harness.server->clientState, CLIENT_DONE);
AssertIntEQ(appChannelsShellReqCount, 1);
AssertIntEQ(harness.client->connectState,
CONNECT_SERVER_CHANNEL_REQUEST_DONE);
AssertFalse(harness.clientIo.sawDisconnect);
AssertFalse(harness.serverIo.sawDisconnect);
FreeKexReplyHarness(&harness);
}
/* Same mode, no callback registered: nothing can start the shell once
* accept() has returned, so the request is refused. The default mode
* accepts it, which AssertHandshakeSucceeds() covers. */
static void TestAppChannelsNoShellCbRejects(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH,
0, NULL);
AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS);
RunAppChannelsHandshake(&harness, &result);
/* RunAppChannelsHandshake() also leaves clientSuccess clear when it
* runs out of steps with neither side erroring, so pin the refusal
* itself: the client stopped early, and for the right reason. */
AssertFalse(result.clientSuccess);
AssertTrue(result.steps < REGRESS_MAX_HANDSHAKE_STEPS);
AssertIntEQ(result.clientErr, WS_CHANOPEN_FAILED);
AssertTrue(harness.client->connectState <
CONNECT_SERVER_CHANNEL_REQUEST_DONE);
AssertIntEQ(harness.server->acceptState, ACCEPT_SERVER_USERAUTH_SENT);
FreeKexReplyHarness(&harness);
}
/* The flag is documented as a context setting first, so pin the setter
* returns and the inheritance wolfSSH_new() does. */
static void TestAppChannelsCtxInherits(void)
{
WOLFSSH_CTX* ctx;
WOLFSSH* ssh;
AssertIntEQ(wolfSSH_CTX_SetAppChannels(NULL, 1), WS_SSH_CTX_NULL_E);
AssertIntEQ(wolfSSH_SetAppChannels(NULL, 1), WS_SSH_NULL_E);
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
AssertNotNull(ctx);
ssh = wolfSSH_new(ctx);
AssertNotNull(ssh);
AssertIntEQ(ssh->appChannels, 0);
wolfSSH_free(ssh);
AssertIntEQ(wolfSSH_CTX_SetAppChannels(ctx, 1), WS_SUCCESS);
ssh = wolfSSH_new(ctx);
AssertNotNull(ssh);
AssertIntEQ(ssh->appChannels, 1);
AssertIntEQ(wolfSSH_SetAppChannels(ssh, 0), WS_SUCCESS);
AssertIntEQ(ssh->appChannels, 0);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
}
/* Turning the mode on after accept() established the session must not leave
* the accept loop hunting for a state it has already stepped past. */
static void TestAppChannelsLateEnableReturns(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH,
0, NULL);
RunKexReplyHandshake(&harness, &result);
AssertTrue(result.serverSuccess);
AssertIntEQ(harness.server->acceptState,
ACCEPT_CLIENT_SESSION_ESTABLISHED);
AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS);
AssertIntEQ(wolfSSH_accept(harness.server), WS_SUCCESS);
AssertIntEQ(harness.server->acceptState,
ACCEPT_CLIENT_SESSION_ESTABLISHED);
FreeKexReplyHarness(&harness);
}
static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void)
{
AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH);
@ -3701,6 +3880,39 @@ static void TestChannelReqSubsysCallbackRuns(void)
WOLFSSH_SESSION_SUBSYSTEM), MSGID_CHANNEL_FAILURE);
}
/* accept() re-entered while it is already parked, with a reply still
* queued, has to flush and stay put. Stepping the state on from here
* would put the stop behind it, and the loop tests for that state
* exactly, so the session would run on to established instead. */
static void TestAppChannelsAcceptKeepsStopWithPendingOutput(void)
{
ChannelOpenHarness harness;
WOLFSSH_CHANNEL* channel;
InitChannelOpenHarness(&harness, NULL, 0);
AssertIntEQ(wolfSSH_SetAppChannels(harness.ssh, 1), WS_SUCCESS);
channel = SeedUnconfirmedChannel(&harness);
AssertIntEQ(ChannelUpdatePeer(channel, 5, 1024, 1024), WS_SUCCESS);
channel->openConfirmed = 1;
/* A blocked send leaves the channel data queued. Nothing here drives
* a channel request, so clientState stays short of CLIENT_DONE and a
* state stepped past the stop fails the accept below rather than
* spinning in it. */
harness.io.blockNext = 1;
AssertIntEQ(wolfSSH_stream_send(harness.ssh, (byte*)"x", 1), 1);
AssertTrue(harness.ssh->outputBuffer.length > 0);
AssertTrue(harness.ssh->clientState < CLIENT_DONE);
AssertIntEQ(harness.ssh->acceptState, ACCEPT_SERVER_USERAUTH_SENT);
AssertIntEQ(wolfSSH_accept(harness.ssh), WS_SUCCESS);
AssertIntEQ(harness.ssh->acceptState, ACCEPT_SERVER_USERAUTH_SENT);
AssertIntEQ(harness.ssh->outputBuffer.length, 0);
FreeChannelOpenHarness(&harness);
}
/* A username change after the first userauth request must end the session. */
static void TestUsernameChangeDisconnects(void)
{
@ -13764,6 +13976,7 @@ int main(int argc, char** argv)
TestChannelCloseCallbackReturnIgnored();
TestChannelReqExecCallbackRuns();
TestChannelReqSubsysCallbackRuns();
TestAppChannelsAcceptKeepsStopWithPendingOutput();
TestSecondSessionChannelRejected();
TestUsernameChangeDisconnects();
TestSameUserRetryAllowed();
@ -13968,6 +14181,10 @@ int main(int argc, char** argv)
#ifdef KEXDH_REPLY_REGRESS_KEX_ALGO
#ifndef WOLFSSH_NO_RSA_SHA2_256
TestAppChannelsCtxInherits();
TestAppChannelsAcceptStopsAtUserAuth();
TestAppChannelsNoShellCbRejects();
TestAppChannelsLateEnableReturns();
TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade();
#endif
#ifndef WOLFSSH_NO_RSA_SHA2_512

View File

@ -9421,6 +9421,68 @@ static int test_DoChannelRequest(void)
}
#endif /* WOLFSSH_SHELL && WOLFSSH_TERM */
/* Application-driven channels flip the no-callback default: with
* accept() already returned there is nothing left to start a shell,
* exec or subsystem, so all three are refused rather than accepted. */
{
static const byte paySubsys[] = {
0x00,0x00,0x00,0x00, /* channelId = 0 */
0x00,0x00,0x00,0x09, /* typeSz = 9 */
0x73,0x75,0x62,0x73,0x79,0x73,
0x74,0x65,0x6D, /* "subsystem" */
0x01, /* wantReply = 1 */
0x00,0x00,0x00,0x04, /* nameSz = 4 */
0x73,0x66,0x74,0x70 /* "sftp" */
};
struct {
const char* label;
const byte* payload;
word32 payloadSz;
int errBase;
} appCases[] = {
{ "shell", payShell, (word32)sizeof(payShell), -495 },
{ "exec", payExec, (word32)sizeof(payExec), -497 },
{ "subsystem", paySubsys, (word32)sizeof(paySubsys), -499 }
};
int a;
for (a = 0; a < (int)(sizeof(appCases) / sizeof(appCases[0])); a++) {
word32 idxApp = 0;
int retApp, capMsgId;
if (wolfSSH_SetAppChannels(ssh, 1) != WS_SUCCESS) {
printf("DoChannelRequest[app-%s]: set failed\n",
appCases[a].label);
result = appCases[a].errBase;
goto done;
}
s_chanReqCaptureSz = 0;
WMEMSET(s_chanReqCapture, 0, sizeof(s_chanReqCapture));
retApp = wolfSSH_TestDoChannelRequest(ssh,
(byte*)appCases[a].payload, appCases[a].payloadSz,
&idxApp);
wolfSSH_SetAppChannels(ssh, 0);
if (retApp != WS_SUCCESS) {
printf("DoChannelRequest[app-%s]: ret=%d, expected=%d\n",
appCases[a].label, retApp, WS_SUCCESS);
result = appCases[a].errBase;
goto done;
}
capMsgId = CaptureMsgId(s_chanReqCapture, s_chanReqCaptureSz);
if (capMsgId != (int)MSGID_CHANNEL_FAILURE) {
printf("DoChannelRequest[app-%s]: msg_id=0x%02x, "
"expected=0x%02x\n", appCases[a].label, capMsgId,
MSGID_CHANNEL_FAILURE);
result = appCases[a].errBase - 1;
goto done;
}
}
}
done:
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);