From 22aa1d499544dea4f52a41902a5527ddce017439 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 09:54:34 -0700 Subject: [PATCH 1/6] echoserver: answer session requests in callbacks With -A the echoserver drives its own channels: accept() returns at userauth and the callbacks below start the shell, SFTP or SCP session. Off by default. The two modes are exclusive, since the callbacks answer the session requests the accept state machine otherwise answers itself. - wsShellStartCb() forks the pty, so it is registered in either mode, and claims the channel only once there is a shell behind it; a second request is refused rather than forking over the running shell - wsExecStartCb() takes an "scp " command as a transfer and any other command as a session, and is registered in either mode, since the legacy path has always started a shell for an exec request too - wsSubsysStartCb() is registered only with -A, as accept() serves sftp itself, and guards a NULL command, which a truncated request leaves behind - ssh_worker() drives the session through shellCtx.appFd, claims the channel itself when no callback did, and leaves an SFTP or SCP handoff through its cleanup so the pty master still closes - open the agent channel from the select loop, since the peer's auth-agent-req lands after accept() has returned, and read the listener from the context each pass because it appears mid-loop - resume a subsystem accept that returns a want, waiting on the socket between attempts rather than spinning - close the accepted socket again, clear fwdFd on EOF or reset, and stay in the loop on WS_REKEYING, which the read arm already handles - key ChildRunning's sig_atomic_t on WOLFSSH_SHELL, the only build with the SIGCHLD handler that writes it, so a target whose libc has no signal.h still compiles - ask for echo mode in the keyboard-interactive test, which has no account on the host for the shell callback to fork a shell for --- examples/echoserver/echoserver.c | 549 ++++++++++++++++++++++--------- tests/api.c | 3 + 2 files changed, 391 insertions(+), 161 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 435cec473..96ea7242d 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -139,6 +139,15 @@ static int quit = 0; wolfSSL_Mutex doneLock; #define MAX_PASSWD_RETRY 3 static int passwdRetry = MAX_PASSWD_RETRY; +/* ssh_worker() reads ChildRunning whether or not a shell is compiled in. + * With a shell, ChildSig() writes it from a SIGCHLD handler, so it has to + * be sig_atomic_t; without one there is no handler, and no signal.h to + * declare that type. Zephyr's libc has neither. */ +#ifdef WOLFSSH_SHELL +static volatile sig_atomic_t ChildRunning = 0; +#else +static volatile int ChildRunning = 0; +#endif #ifndef EXAMPLE_HIGHWATER_MARK @@ -197,7 +206,7 @@ typedef struct WS_FwdCbActionCtx { typedef struct { WOLFSSH* ssh; WS_SOCKET_T fd; - word32 id; + word32 tid; int echo; char nonBlock; #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) @@ -212,6 +221,12 @@ typedef struct { WS_FwdCbActionCtx fwdCbCtx; #endif WS_AppCtx shellCtx; +#ifdef WOLFSSH_SFTP + int doSftp; +#endif +#ifdef WOLFSSH_SCP + int doScp; +#endif byte channelBuffer[EXAMPLE_BUFFER_SZ]; /* The EOF drain holds an unsent tail across worker passes, * so it cannot share channelBuffer with the read path. */ @@ -250,7 +265,7 @@ static int dump_stats(thread_ctx_t* ctx) "Statistics for Thread #%u:\r\n" " txCount = %u\r\n rxCount = %u\r\n" " seq = %u\r\n peerSeq = %u\r\n", - ctx->id, txCount, rxCount, seq, peerSeq); + ctx->tid, txCount, rxCount, seq, peerSeq); statsSz = (word32)WSTRLEN(ctx->statsBuffer); fprintf(stderr, "%s", ctx->statsBuffer); @@ -659,8 +674,9 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, else if (action == WOLFSSH_FWD_CHANNEL_ID) { appCtx->channelId = port; } - else + else { ret = WS_FWD_INVALID_ACTION; + } return ret; } @@ -668,6 +684,222 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, #endif /* WOLFSSH_FWD */ +#ifdef WOLFSSH_SHELL +static void ChildSig(int sig) +{ + (void)sig; + ChildRunning = 0; +} + + +#ifdef SHELL_DEBUG +static int termios_show(int fd) +{ + struct termios tios; + int i; + int rc; + + WMEMSET((void *) &tios, 0, sizeof(tios)); + rc = tcgetattr(fd, &tios); + printf("tcgetattr returns=%x\n", rc); + + printf("iflag/oflag/cflag/lflag = %x/%x/%x/%x\n", + (unsigned int)tios.c_iflag, (unsigned int)tios.c_oflag, + (unsigned int)tios.c_cflag, (unsigned int)tios.c_lflag); + printf("c_ispeed/c_ospeed = %x/%x\n", + (unsigned int)tios.c_ispeed, (unsigned int)tios.c_ospeed); + for (i = 0; i < NCCS; i++) { + printf("c_cc[%d] = %hhx\n", i, tios.c_cc[i]); + } + return 0; +} +#endif +#endif /* WOLFSSH_SHELL */ + + +/* Registered in every build, in both modes: with no shell the echoserver + * still has to take the channel to mark it connected, so ssh_worker() will + * echo on it. Returns WS_SUCCESS to accept the request, 1 to reject it. */ +static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + thread_ctx_t* threadCtx = (thread_ctx_t*)ctx; + word32 channelId = 0; + + if (threadCtx == NULL) { + return 1; + } + + /* One session per connection. Nothing stops a peer asking a second + * time, and taking it would fork a second shell over the first and + * lose the fd of the one already running. */ + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + || threadCtx->shellCtx.appFd >= 0) { + return 1; + } + + /* Our own id: it is what wolfSSH_worker() reports and what the read, + * send, and find calls below take. */ + if (wolfSSH_ChannelGetId(channel, &channelId, WS_CHANNEL_ID_SELF) + != WS_SUCCESS) { + return 1; + } + +#ifdef WOLFSSH_SHELL + /* Echo mode has no shell to start, ssh_worker() echoes the channel data + * back through the SSH stream. */ + if (!threadCtx->echo) { + WOLFSSH* ssh; + const char *userName; + struct passwd *p_passwd; + struct termios tios; + pid_t childPid; + int rc; + + ssh = threadCtx->ssh; + userName = wolfSSH_GetUsername(ssh); + p_passwd = getpwnam((const char *)userName); + if (p_passwd == NULL) { + /* Not actually a user on the system. */ + #ifdef SHELL_DEBUG + fprintf(stderr, "user %s does not exist\n", userName); + #endif + return 1; + } + + childPid = forkpty(&threadCtx->shellCtx.appFd, NULL, NULL, NULL); + + if (childPid < 0) { + /* forkpty failed, so return */ + ChildRunning = 0; + return 1; + } + else if (childPid == 0) { + /* Child process */ + const char *args[] = {"-sh", NULL}; + + signal(SIGINT, SIG_DFL); + + #ifdef SHELL_DEBUG + printf("userName is %s\n", userName); + system("env"); + #endif + + setenv("HOME", p_passwd->pw_dir, 1); + setenv("LOGNAME", p_passwd->pw_name, 1); + rc = chdir(p_passwd->pw_dir); + if (rc != 0) { + /* Never return: the child would run on inside the library + * and write to the parent's socket. */ + _exit(EXIT_FAILURE); + } + + execv("/bin/sh", (char **)args); + _exit(EXIT_FAILURE); + } + #ifdef SHELL_DEBUG + printf("In childPid > 0; getpid=%d\n", (int)getpid()); + #endif + signal(SIGCHLD, ChildSig); + + rc = tcgetattr(threadCtx->shellCtx.appFd, &tios); + if (rc != 0) { + printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); + return 1; + } + rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios); + if (rc != 0) { + printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); + return 1; + } + + #ifdef SHELL_DEBUG + termios_show(threadCtx->shellCtx.appFd); + #endif + + /* set initial size of terminal based on saved size */ + #if !defined(NO_TERMIOS) && defined(WOLFSSH_TERM) + #if defined(HAVE_SYS_IOCTL_H) + wolfSSH_DoModes(ssh->modes, ssh->modesSz, threadCtx->shellCtx.appFd); + { + struct winsize s = {0}; + + s.ws_col = ssh->widthChar; + s.ws_row = ssh->heightRows; + s.ws_xpixel = ssh->widthPixels; + s.ws_ypixel = ssh->heightPixels; + + ioctl(threadCtx->shellCtx.appFd, TIOCSWINSZ, &s); + } + #endif /* HAVE_SYS_IOCTL_H */ + + wolfSSH_SetTerminalResizeCtx(ssh, (void*)&threadCtx->shellCtx.appFd); + #endif /* !NO_TERMIOS && WOLFSSH_TERM */ + } +#endif /* WOLFSSH_SHELL */ + + /* Claim the channel only once it can be served. Claiming it up front + * would leave the worker driving a connected shell that never started. */ + threadCtx->shellCtx.channelId = channelId; + threadCtx->shellCtx.state = APP_STATE_CONNECTED; + + return WS_SUCCESS; +} + + +#ifdef WOLFSSH_SFTP +static int wsSubsysStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) +{ + int rej = 1; + + if (vCtx && channel) { + thread_ctx_t* threadCtx; + const char* cmd; + WS_SessionType type; + + threadCtx = (thread_ctx_t*)vCtx; + cmd = wolfSSH_ChannelGetSessionCommand(channel); + type = wolfSSH_ChannelGetSessionType(channel); + + /* A truncated subsystem string leaves the command NULL, and this + * runs before anything else has looked at it. */ + if (type == WOLFSSH_SESSION_SUBSYSTEM && cmd != NULL + && WSTRCMP(cmd, "sftp") == 0) { + threadCtx->doSftp = 1; + rej = WS_SUCCESS; + } + } + + return rej; +} +#endif /* WOLFSSH_SFTP */ + + +/* An "scp ..." command starts a transfer, anything else runs as a session, + * the same as a shell request: the echoserver never runs the command. */ +static int wsExecStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) +{ + int rej = 1; + + if (vCtx && channel) { + const char* cmd = wolfSSH_ChannelGetSessionCommand(channel); + +#ifdef WOLFSSH_SCP + if (cmd != NULL && WSTRNCMP(cmd, "scp ", 4) == 0) { + ((thread_ctx_t*)vCtx)->doScp = 1; + rej = WS_SUCCESS; + } + else +#endif /* WOLFSSH_SCP */ + { + rej = wsShellStartCb(channel, vCtx); + } + (void)cmd; + } + + return rej; +} + + #ifdef SHELL_DEBUG static void display_ascii(char *p_buf, @@ -709,30 +941,6 @@ static void buf_dump(unsigned char *buf, int len) return; } - -#ifdef WOLFSSH_SHELL -static int termios_show(int fd) -{ - struct termios tios; - int i; - int rc; - - WMEMSET((void *) &tios, 0, sizeof(tios)); - rc = tcgetattr(fd, &tios); - printf("tcgetattr returns=%x\n", rc); - - printf("iflag/oflag/cflag/lflag = %x/%x/%x/%x\n", - (unsigned int)tios.c_iflag, (unsigned int)tios.c_oflag, - (unsigned int)tios.c_cflag, (unsigned int)tios.c_lflag); - printf("c_ispeed/c_ospeed = %x/%x\n", - (unsigned int)tios.c_ispeed, (unsigned int)tios.c_ospeed); - for (i = 0; i < NCCS; i++) { - printf("c_cc[%d] = %hhx\n", i, tios.c_cc[i]); - } - return 0; -} -#endif /* WOLFSSH_SHELL */ - #endif /* SHELL_DEBUG */ @@ -817,21 +1025,14 @@ static int termios_show(int fd) #endif -int ChildRunning = 0; - -#ifdef WOLFSSH_SHELL -static void ChildSig(int sig) -{ - (void)sig; - ChildRunning = 0; -} -#endif - static int ssh_worker(thread_ctx_t* threadCtx) { WOLFSSH* ssh; WS_SOCKET_T sshFd; int rc = 0; + /* What the loop hands back, so a transfer taking over the session + * still leaves through the cleanup below it. */ + int workerRet = 0; int eofAnswered = 0; /* Held across passes with 0 <= eofOff <= eofRead. */ int eofRead = 0; @@ -839,11 +1040,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) /* Without a shell there is no child to outlive the peer's EOF, and the * read path echoes unconditionally. */ int echoOnly = 1; -#ifdef WOLFSSH_SHELL - const char *userName; - struct passwd *p_passwd; - WS_SOCKET_T childFd = 0; - pid_t childPid; +#ifdef WOLFSSH_AGENT + int agentOpened = 0; #endif #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) pthread_t globalReq_th; @@ -862,6 +1060,20 @@ static int ssh_worker(thread_ctx_t* threadCtx) sshFd = wolfSSH_get_fd(ssh); + if (threadCtx->shellCtx.state != APP_STATE_CONNECTED) { + /* The legacy path: wolfSSH_accept() answered the session request + * itself, so no channel-request callback ran to claim the channel. + * Claim it here, on the session accept() established. */ + WOLFSSH_CHANNEL* sessionChannel; + + sessionChannel = wolfSSH_ChannelNext(ssh, NULL); + if (sessionChannel != NULL) { + threadCtx->shellCtx.state = APP_STATE_CONNECTED; + wolfSSH_ChannelGetId(sessionChannel, + &threadCtx->shellCtx.channelId, WS_CHANNEL_ID_SELF); + } + } + #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) /* submit Global Request for keep-alive */ rc = pthread_create(&globalReq_th, NULL, global_req, threadCtx); @@ -869,57 +1081,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) printf("pthread_create() failed.\n"); #endif -#ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - - userName = wolfSSH_GetUsername(ssh); - p_passwd = getpwnam((const char *)userName); - if (p_passwd == NULL) { - /* Not actually a user on the system. */ - #ifdef SHELL_DEBUG - fprintf(stderr, "user %s does not exist\n", userName); - #endif - return WS_FATAL_ERROR; - } - - ChildRunning = 1; - childPid = forkpty(&childFd, NULL, NULL, NULL); - - if (childPid < 0) { - /* forkpty failed, so return */ - ChildRunning = 0; - return WS_FATAL_ERROR; - } - else if (childPid == 0) { - /* Child process */ - const char *args[] = {"-sh", NULL}; - - signal(SIGINT, SIG_DFL); - - #ifdef SHELL_DEBUG - printf("userName is %s\n", userName); - system("env"); - #endif - - setenv("HOME", p_passwd->pw_dir, 1); - setenv("LOGNAME", p_passwd->pw_name, 1); - rc = chdir(p_passwd->pw_dir); - if (rc != 0) { - return WS_FATAL_ERROR; - } - - execv("/bin/sh", (char **)args); - } - } -#endif { /* Parent process */ -#ifdef WOLFSSH_SHELL - struct termios tios; -#endif #ifdef WOLFSSH_AGENT WS_SOCKET_T agentFd = -1; - WS_SOCKET_T agentListenFd = threadCtx->agentCtx.listenFd; word32 agentChannelId = -1; #endif #ifdef WOLFSSH_FWD @@ -927,52 +1092,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) word32 fwdBufferIdx = 0; #endif -#ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - #ifdef SHELL_DEBUG - printf("In childPid > 0; getpid=%d\n", (int)getpid()); - #endif - signal(SIGCHLD, ChildSig); - - rc = tcgetattr(childFd, &tios); - if (rc != 0) { - printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); - return WS_FATAL_ERROR; - } - rc = tcsetattr(childFd, TCSAFLUSH, &tios); - if (rc != 0) { - printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); - return WS_FATAL_ERROR; - } - - #ifdef SHELL_DEBUG - termios_show(childFd); - #endif - } - else - ChildRunning = 1; -#else ChildRunning = 1; -#endif - -#if !defined(NO_TERMIOS) && defined(WOLFSSH_TERM) && defined(WOLFSSH_SHELL) -#if defined(HAVE_SYS_IOCTL_H) - /* if not echoing, set initial size of terminal based on saved size */ - if (!threadCtx->echo) { - struct winsize s = {0,0,0,0}; - - wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd); - s.ws_col = ssh->widthChar; - s.ws_row = ssh->heightRows; - s.ws_xpixel = ssh->widthPixels; - s.ws_ypixel = ssh->heightPixels; - - ioctl(childFd, TIOCSWINSZ, &s); - - wolfSSH_SetTerminalResizeCtx(ssh, (void*)&childFd); - } -#endif /* HAVE_SYS_IOCTL_H */ -#endif /* !NO_TERMIOS && WOLFSSH_TERM && WOLFSSH_SHELL */ while (ChildRunning) { fd_set readFds; @@ -984,18 +1104,33 @@ static int ssh_worker(thread_ctx_t* threadCtx) FD_SET(sshFd, &readFds); maxFd = sshFd; + #ifdef WOLFSSH_AGENT + /* The peer's auth-agent-req lands after wolfSSH_accept() has + * already returned in application-driven mode, so the channel + * answering it is opened here rather than inside accept(). The + * call reports WS_BAD_ARGUMENT until the request arrives. */ + if (!agentOpened + && wolfSSH_AGENT_ChannelOpen(ssh) == WS_SUCCESS) { + agentOpened = 1; + } + #endif + #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - FD_SET(childFd, &readFds); - if (childFd > maxFd) - maxFd = childFd; + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + && threadCtx->shellCtx.appFd >= 0) { + FD_SET(threadCtx->shellCtx.appFd, &readFds); + if (threadCtx->shellCtx.appFd > maxFd) + maxFd = threadCtx->shellCtx.appFd; } #endif /* WOLFSSH_SHELL */ #ifdef WOLFSSH_AGENT - if (threadCtx->agentCtx.state == APP_STATE_LISTEN) { - FD_SET(agentListenFd, &readFds); - if (agentListenFd > maxFd) - maxFd = agentListenFd; + /* The poll above creates this listener mid-loop; re-read it + * each pass rather than caching it. */ + if (threadCtx->agentCtx.state == APP_STATE_LISTEN + && threadCtx->agentCtx.listenFd >= 0) { + FD_SET(threadCtx->agentCtx.listenFd, &readFds); + if (threadCtx->agentCtx.listenFd > maxFd) + maxFd = threadCtx->agentCtx.listenFd; } if (agentFd >= 0 && threadCtx->agentCtx.state == APP_STATE_CONNECTED) { @@ -1020,6 +1155,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) maxFd = fwdFd; } #endif /* WOLFSSH_FWD */ + rc = select((int)maxFd + 1, &readFds, NULL, NULL, NULL); if (rc == -1) { break; @@ -1035,6 +1171,18 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); + #ifdef WOLFSSH_SFTP + if (threadCtx->doSftp) { + workerRet = WS_SFTP_COMPLETE; + break; + } + #endif + #ifdef WOLFSSH_SCP + if (threadCtx->doScp) { + workerRet = WS_SCP_INIT; + break; + } + #endif /* Take the worker's status before the drain below: its * reads and sends latch their own into ssh->error. */ rc = wolfSSH_get_error(ssh); @@ -1113,7 +1261,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) * wolfSSH_ChannelIdRead() has no isKeying gate; the window * credit it owes is parked until the rekey finishes. */ if (rc == WS_CHAN_RXD || rc == WS_REKEYING) { - if (lastChannel == threadCtx->shellCtx.channelId) { + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED && + lastChannel == threadCtx->shellCtx.channelId) { cnt_r = wolfSSH_ChannelIdRead(ssh, threadCtx->shellCtx.channelId, threadCtx->channelBuffer, @@ -1130,7 +1279,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif #ifdef WOLFSSH_SHELL if (!threadCtx->echo) { - cnt_w = (int)write(childFd, + cnt_w = (int)write( + threadCtx->shellCtx.appFd, threadCtx->channelBuffer, cnt_r); } else { @@ -1243,7 +1393,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } - else if (rc != WS_WANT_READ) { + else if (rc != WS_WANT_READ && rc != WS_REKEYING) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", cnt_r, errno); @@ -1252,11 +1402,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) } } } - #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - if (FD_ISSET(childFd, &readFds)) { - cnt_r = (int)read(childFd, + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + && threadCtx->shellCtx.appFd >= 0) { + if (FD_ISSET(threadCtx->shellCtx.appFd, &readFds)) { + cnt_r = (int)read(threadCtx->shellCtx.appFd, threadCtx->shellCtx.buffer, sizeof threadCtx->shellCtx.buffer); /* This read will return 0 on EOF */ @@ -1335,12 +1485,13 @@ static int ssh_worker(thread_ctx_t* threadCtx) } } } - if (threadCtx->agentCtx.state == APP_STATE_LISTEN) { - if (FD_ISSET(agentListenFd, &readFds)) { + if (threadCtx->agentCtx.state == APP_STATE_LISTEN + && threadCtx->agentCtx.listenFd >= 0) { + if (FD_ISSET(threadCtx->agentCtx.listenFd, &readFds)) { #ifdef SHELL_DEBUG printf("accepting agent connection\n"); #endif - agentFd = accept(agentListenFd, NULL, NULL); + agentFd = accept(threadCtx->agentCtx.listenFd, NULL, NULL); if (agentFd == -1) { rc = errno; if (rc != SOCKET_EWOULDBLOCK) { @@ -1371,8 +1522,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) fwdFd = -1; threadCtx->fwdCtx.appFd = -1; if (threadCtx->fwdCbCtx.hostName != NULL) { - WFREE(threadCtx->fwdCbCtx.hostName, - NULL, 0); + WFREE(threadCtx->fwdCbCtx.hostName, NULL, 0); threadCtx->fwdCbCtx.hostName = NULL; } threadCtx->fwdCtx.state = APP_STATE_LISTEN; @@ -1497,8 +1647,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif /* WOLFSSH_FWD */ } #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) - WCLOSESOCKET(childFd); + if (threadCtx->shellCtx.appFd >= 0) { + WCLOSESOCKET(threadCtx->shellCtx.appFd); + threadCtx->shellCtx.appFd = -1; + } #endif } @@ -1506,10 +1658,13 @@ static int ssh_worker(thread_ctx_t* threadCtx) pthread_join(globalReq_th, NULL); #endif - return 0; + return workerRet; } +/* Seconds to wait on the socket between subsystem-accept attempts. */ +#define ES_ACCEPT_TIMEOUT 1 + #ifdef WOLFSSH_SFTP #define TEST_SFTP_TIMEOUT_SHORT 0 @@ -1756,8 +1911,10 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) else { ret = NonBlockSSH_accept(threadCtx->ssh); } + #ifdef WOLFSSH_SCP - /* finish off SCP operation */ + /* The legacy path: accept() reports the scp command and does the + * transfer on re-entry. */ if (ret == WS_SCP_INIT) { if (!threadCtx->nonBlock) ret = wolfSSH_accept(threadCtx->ssh); @@ -1773,6 +1930,8 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) break; #ifdef WOLFSSH_SFTP + /* The legacy path: wolfSSH_accept() ran the subsystem request + * itself and handed back a session ready to serve. */ case WS_SFTP_COMPLETE: ret = sftp_worker(threadCtx); break; @@ -1780,6 +1939,48 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) case WS_SUCCESS: ret = ssh_worker(threadCtx); + #ifdef WOLFSSH_SCP + if (ret == WS_SCP_INIT) { + /* On a non-blocking socket the transfer comes back part + * done; resume it rather than tearing the session down + * mid-file. */ + do { + ret = wolfSSH_SCP_accept(threadCtx->ssh); + error = wolfSSH_get_error(threadCtx->ssh); + if (ret != WS_SCP_COMPLETE + && (error == WS_WANT_READ + || error == WS_WANT_WRITE)) { + tcp_select(wolfSSH_get_fd(threadCtx->ssh), + ES_ACCEPT_TIMEOUT); + } + } while (ret != WS_SCP_COMPLETE + && (error == WS_WANT_READ || error == WS_WANT_WRITE)); + if (ret == WS_SCP_COMPLETE) { + printf("scp file transfer completed\n"); + ret = 0; + } + } + #endif + #ifdef WOLFSSH_SFTP + if (ret == WS_SFTP_COMPLETE) { + do { + ret = wolfSSH_SFTP_accept(threadCtx->ssh); + error = wolfSSH_get_error(threadCtx->ssh); + /* Wait on the socket between attempts; without this the + * gap before the client's SFTP INIT is a busy spin. */ + if (ret != WS_SFTP_COMPLETE + && (error == WS_WANT_READ + || error == WS_WANT_WRITE)) { + tcp_select(wolfSSH_get_fd(threadCtx->ssh), + ES_ACCEPT_TIMEOUT); + } + } while (ret != WS_SFTP_COMPLETE + && (error == WS_WANT_READ || error == WS_WANT_WRITE)); + } + if (ret == WS_SFTP_COMPLETE) { + ret = sftp_worker(threadCtx); + } + #endif break; } @@ -3139,6 +3340,7 @@ static void ShowUsage(void) #ifdef WOLFSSH_SHELL printf(" -f echo input\n"); #endif + printf(" -A drive channels from the application callbacks\n"); printf(" -p port to connect on, default %d\n", wolfSshPort); printf(" -N use non-blocking sockets\n"); #ifdef WOLFSSH_SFTP @@ -3192,7 +3394,7 @@ static void ShowUsage(void) } -#define ECHOSERVER_OPTLIST "?1a:d:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:" +#define ECHOSERVER_OPTLIST "?1a:Ad:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:" #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Detects whether argv or the environment requests a host key from the @@ -3323,6 +3525,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) int userEcc = 0; int peerEcc = 0; int echo = 0; + int appChannels = 0; int ch; word16 port = wolfSshPort; char* readyFile = NULL; @@ -3384,6 +3587,10 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif break; + case 'A': + appChannels = 1; + break; + case 'p': if (myoptarg == NULL) { ES_ERROR("NULL port value\n"); @@ -3625,6 +3832,25 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #ifdef WOLFSSH_FWD wolfSSH_CTX_SetFwdCb(ctx, wolfSSH_FwdDefaultActions, NULL); #endif + /* With -A the echoserver drives its own channels: accept() stops at + * userauth and these callbacks start the shell, subsystem or transfer. + * Off by default, so the path this example has always taken keeps an + * in-tree demo. The two are exclusive: the callbacks answer the session + * requests the accept state machine would otherwise answer itself. */ + /* The shell callback is the only place the pty is forked, and an exec + * request that is not a transfer runs as a session, so both are + * registered in both modes. accept() honours a registered callback with + * application-driven channels off, so the legacy path keeps the shell it + * has always started for either request. The subsystem callback is not + * registered there: accept() serves sftp itself. */ + wolfSSH_CTX_SetChannelReqShellCb(ctx, wsShellStartCb); + wolfSSH_CTX_SetChannelReqExecCb(ctx, wsExecStartCb); + if (appChannels) { + wolfSSH_CTX_SetAppChannels(ctx, 1); +#ifdef WOLFSSH_SFTP + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, wsSubsysStartCb); +#endif + } #ifndef NO_FILESYSTEM if (sshPubKeyList) { @@ -4044,6 +4270,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif wolfSSH_SetUserAuthCtx(ssh, &pwMapList); wolfSSH_SetKeyingCompletionCbCtx(ssh, (void*)ssh); + wolfSSH_SetChannelReqCtx(ssh, (void*)threadCtx); /* Use the session object for its own highwater callback ctx */ if (defaultHighwater > 0) { @@ -4103,13 +4330,13 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) tcp_set_nonblocking(&clientFd); wolfSSH_set_fd(ssh, (int)clientFd); + threadCtx->fd = clientFd; #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) threadCtx->ctx = ctx; #endif threadCtx->ssh = ssh; - threadCtx->fd = clientFd; - threadCtx->id = threadCount++; + threadCtx->tid = threadCount++; threadCtx->nonBlock = nonBlock; threadCtx->echo = echo; threadCtx->shellCtx.privateData = NULL; diff --git a/tests/api.c b/tests/api.c index 55009434c..9826c2a76 100644 --- a/tests/api.c +++ b/tests/api.c @@ -8004,6 +8004,9 @@ static void test_wolfSSH_KeyboardInteractive(void) argsCount = 0; args[argsCount++] = "."; args[argsCount++] = "-1"; + /* Echo mode: "test" is not an account on the host, so the echoserver's + * shell callback would refuse the shell request this client sends. */ + args[argsCount++] = "-f"; args[argsCount++] = "-i"; args[argsCount++] = "test:test"; args[argsCount++] = "-p"; From 0810b1208a2c84389a7f31543c85b1e00b25b378 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Tue, 8 Sep 2026 08:14:56 -0700 Subject: [PATCH 2/6] tests: cover the application-driven SCP start The echoserver's -A mode runs an accepted scp command through wolfSSH_SCP_accept(). Reaching that call's want retry path takes a non-blocking server, which -N supplies. - copy to and from an app-driven server in scp.test - check the entry point's null-session argument --- scripts/scp.test | 41 +++++++++++++++++++++++++++++++++++++++++ tests/api.c | 2 ++ 2 files changed, 43 insertions(+) diff --git a/scripts/scp.test b/scripts/scp.test index 20f9737eb..16064333f 100755 --- a/scripts/scp.test +++ b/scripts/scp.test @@ -137,6 +137,47 @@ else exit 1 fi +# With -A the echoserver binds the scp command itself and runs the transfer +# through wolfSSH_SCP_accept(); the cases above take the accept() re-entry +# instead. -N reaches that call's want-read/want-write retry path, which a +# blocking server completes in one call. The client stays blocking, which the +# server does not care about. +echo "Test basic copy from server to local, app-driven server" +./examples/echoserver/echoserver -A -N -1 -R $ready_file & +server_pid=$! +create_port +$run_client ./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test +RESULT=$? +remove_ready_file +stop_server +check_timeout $RESULT "basic copy from server to local, app-driven server" + +if test -e $PWD/scp.test; then + rm $PWD/scp.test +else + echo -e "\n\nfailed to get file from app-driven server" + do_cleanup + exit 1 +fi + +echo "Test basic copy from local to server, app-driven server" +./examples/echoserver/echoserver -A -N -1 -R $ready_file & +server_pid=$! +create_port +$run_client ./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/scripts/scp.test:$PWD/scp.test +RESULT=$? +remove_ready_file +stop_server +check_timeout $RESULT "basic copy from local to server, app-driven server" + +if test -e $PWD/scp.test; then + rm $PWD/scp.test +else + echo -e "\n\nfailed to send file to app-driven server" + do_cleanup + exit 1 +fi + echo "Test of getting empty file" touch $PWD/scripts/empty ./examples/echoserver/echoserver -1 -R $ready_file & diff --git a/tests/api.c b/tests/api.c index 9826c2a76..1d309a8f3 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2611,6 +2611,8 @@ static void test_wolfSSH_SCP_CB(void) AssertIntEQ(wolfSSH_SetScpErrorMsg(NULL, err), WS_BAD_ARGUMENT); AssertIntEQ(wolfSSH_SetScpErrorMsg(ssh, NULL), WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_SCP_accept(NULL), WS_BAD_ARGUMENT); + wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } From 0e3aa9490eb7463ac1e83c5e46e446a92140b82f Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 11 Sep 2026 13:58:24 -0700 Subject: [PATCH 3/6] echoserver: claim only a granted, usable session ssh_worker() takes the session channel accept() established only when the request was granted and there is somewhere to put the data. The type and command stay set on a refusal, so they do not say what was granted, and a refused request leaves nothing running. - a shell build serves the channel through the pty, so with no shell started only echo mode can take it - wsShellStartCb() closes the pty and reaps the child when the terminal setup fails, installs ChildSig() only once the session will run, and leaves ChildRunning alone when forkpty() fails - the shell, exec and subsystem callbacks share SessionInUse(), so a second program start on the connection is refused - the EOF drain answers a half-close only on a claimed session, rather than on whatever channel id 0 finds --- examples/echoserver/echoserver.c | 94 ++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 16 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 96ea7242d..f5f9c661d 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -86,6 +86,7 @@ #endif #ifndef USE_WINDOWS_API #include + #include #endif #include #if defined(__QNX__) || defined(__QNXNTO__) @@ -692,6 +693,26 @@ static void ChildSig(int sig) } +/* Undo a forkpty() whose session request is about to be refused: the shell + * would run on behind a pty nothing reads, and its exit would clear + * ChildRunning out from under the worker loop. */ +static void ShellChildCleanup(thread_ctx_t* threadCtx, pid_t childPid) +{ + void (*prevSig)(int); + + if (threadCtx->shellCtx.appFd >= 0) { + WCLOSESOCKET(threadCtx->shellCtx.appFd); + threadCtx->shellCtx.appFd = -1; + } + + /* This exit is ours, not the session's. */ + prevSig = signal(SIGCHLD, SIG_DFL); + kill(childPid, SIGKILL); + waitpid(childPid, NULL, 0); + signal(SIGCHLD, prevSig); +} + + #ifdef SHELL_DEBUG static int termios_show(int fd) { @@ -717,6 +738,28 @@ static int termios_show(int fd) #endif /* WOLFSSH_SHELL */ +/* One program start per connection, as RFC 4254 section 6.5 allows. A + * second start would fork a shell over the running one, or hand the + * session to sftp or scp, whose divert closes the pty. */ +static int SessionInUse(const thread_ctx_t* threadCtx) +{ + int inUse; + + /* WS_SOCKET_T is unsigned on Windows, so the unset fd is -1 rather + * than anything below zero. */ + inUse = threadCtx->shellCtx.state == APP_STATE_CONNECTED + || threadCtx->shellCtx.appFd != (WS_SOCKET_T)-1; +#ifdef WOLFSSH_SFTP + inUse = inUse || threadCtx->doSftp; +#endif +#ifdef WOLFSSH_SCP + inUse = inUse || threadCtx->doScp; +#endif + + return inUse; +} + + /* Registered in every build, in both modes: with no shell the echoserver * still has to take the channel to mark it connected, so ssh_worker() will * echo on it. Returns WS_SUCCESS to accept the request, 1 to reject it. */ @@ -729,11 +772,7 @@ static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx) return 1; } - /* One session per connection. Nothing stops a peer asking a second - * time, and taking it would fork a second shell over the first and - * lose the fd of the one already running. */ - if (threadCtx->shellCtx.state == APP_STATE_CONNECTED - || threadCtx->shellCtx.appFd >= 0) { + if (SessionInUse(threadCtx)) { return 1; } @@ -769,8 +808,7 @@ static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx) childPid = forkpty(&threadCtx->shellCtx.appFd, NULL, NULL, NULL); if (childPid < 0) { - /* forkpty failed, so return */ - ChildRunning = 0; + /* Refuse the request; the connection carries on without it. */ return 1; } else if (childPid == 0) { @@ -799,19 +837,22 @@ static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx) #ifdef SHELL_DEBUG printf("In childPid > 0; getpid=%d\n", (int)getpid()); #endif - signal(SIGCHLD, ChildSig); - rc = tcgetattr(threadCtx->shellCtx.appFd, &tios); if (rc != 0) { printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); + ShellChildCleanup(threadCtx, childPid); return 1; } rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios); if (rc != 0) { printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); + ShellChildCleanup(threadCtx, childPid); return 1; } + /* Installed only now: the refusals above reap their own child. */ + signal(SIGCHLD, ChildSig); + #ifdef SHELL_DEBUG termios_show(threadCtx->shellCtx.appFd); #endif @@ -857,6 +898,11 @@ static int wsSubsysStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) WS_SessionType type; threadCtx = (thread_ctx_t*)vCtx; + + if (SessionInUse(threadCtx)) { + return 1; + } + cmd = wolfSSH_ChannelGetSessionCommand(channel); type = wolfSSH_ChannelGetSessionType(channel); @@ -883,6 +929,10 @@ static int wsExecStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) if (vCtx && channel) { const char* cmd = wolfSSH_ChannelGetSessionCommand(channel); + if (SessionInUse((thread_ctx_t*)vCtx)) { + return 1; + } + #ifdef WOLFSSH_SCP if (cmd != NULL && WSTRNCMP(cmd, "scp ", 4) == 0) { ((thread_ctx_t*)vCtx)->doScp = 1; @@ -1061,13 +1111,22 @@ static int ssh_worker(thread_ctx_t* threadCtx) sshFd = wolfSSH_get_fd(ssh); if (threadCtx->shellCtx.state != APP_STATE_CONNECTED) { - /* The legacy path: wolfSSH_accept() answered the session request - * itself, so no channel-request callback ran to claim the channel. - * Claim it here, on the session accept() established. */ + /* The legacy path: accept() answered the session request itself, + * with no callback registered to claim the channel. Take it when + * it was granted and there is somewhere to put the data: nothing + * started a shell, so a shell build serves it only in echo mode. + * The grant is read from internal.h; the library has no public + * accessor for it yet. */ WOLFSSH_CHANNEL* sessionChannel; + int canServe = 1; + +#ifdef WOLFSSH_SHELL + canServe = echoOnly || threadCtx->shellCtx.appFd >= 0; +#endif sessionChannel = wolfSSH_ChannelNext(ssh, NULL); - if (sessionChannel != NULL) { + if (canServe && sessionChannel != NULL + && sessionChannel->sessionGranted) { threadCtx->shellCtx.state = APP_STATE_CONNECTED; wolfSSH_ChannelGetId(sessionChannel, &threadCtx->shellCtx.channelId, WS_CHANNEL_ID_SELF); @@ -1193,8 +1252,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) * us. Off the channel's own state, not the WS_EOF status: the * flush inside wolfSSH_worker() can supersede that, and it is * raised once. Echo mode only; a shell child on a pty is - * still producing, so its EOF waits for the child to exit. */ - if (!eofAnswered && echoOnly) { + * still producing, so its EOF waits for the child to exit. + * A claimed session only: unclaimed, shellCtx.channelId is + * still 0, which is the first channel the peer is given. */ + if (!eofAnswered && echoOnly + && threadCtx->shellCtx.state == APP_STATE_CONNECTED) { WOLFSSH_CHANNEL* eofChannel; eofChannel = wolfSSH_ChannelFind(ssh, @@ -1662,7 +1724,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) } -/* Seconds to wait on the socket between subsystem-accept attempts. */ +/* Seconds to wait on the socket between sftp and scp accept attempts. */ #define ES_ACCEPT_TIMEOUT 1 #ifdef WOLFSSH_SFTP From deafb1ea89f95618f05542ce7b891700b9a4a8b9 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 11 Sep 2026 13:58:24 -0700 Subject: [PATCH 4/6] echoserver: bind sftp and scp as the library does The subsystem and exec callbacks pick out the same commands the accept state machine does: sftp matched whole, by length and bytes, and scp on the three-byte prefix ChannelCommandIsScp() takes. - sftp with an embedded NUL is refused, rather than granted and then dropped by wolfSSH_SFTP_accept() - drop an unreachable WS_REKEYING test from the worker error chain --- examples/echoserver/echoserver.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index f5f9c661d..7a2bb768c 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -907,8 +907,13 @@ static int wsSubsysStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) type = wolfSSH_ChannelGetSessionType(channel); /* A truncated subsystem string leaves the command NULL, and this - * runs before anything else has looked at it. */ + * runs before anything else has looked at it. The name matches + * whole, length and bytes, as wolfSSH_SFTP_accept() asks: granting + * sftp with an embedded NUL answers success on a session it then + * refuses. */ if (type == WOLFSSH_SESSION_SUBSYSTEM && cmd != NULL + && wolfSSH_ChannelGetSessionCommandSz(channel) + == (word32)WSTRLEN("sftp") && WSTRCMP(cmd, "sftp") == 0) { threadCtx->doSftp = 1; rej = WS_SUCCESS; @@ -934,7 +939,8 @@ static int wsExecStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) } #ifdef WOLFSSH_SCP - if (cmd != NULL && WSTRNCMP(cmd, "scp ", 4) == 0) { + /* The prefix ChannelCommandIsScp() matches, so both modes agree. */ + if (cmd != NULL && WSTRNCMP(cmd, "scp", 3) == 0) { ((thread_ctx_t*)vCtx)->doScp = 1; rej = WS_SUCCESS; } @@ -1455,7 +1461,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } - else if (rc != WS_WANT_READ && rc != WS_REKEYING) { + else if (rc != WS_WANT_READ) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", cnt_r, errno); From 8908d9612ebd14aeb2f2777f5222820446281d60 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 11 Sep 2026 14:38:05 -0700 Subject: [PATCH 5/6] echoserver: retry a send the socket did not take ssh_worker() reads a WS_WANT_WRITE from wolfSSH_worker() as a send the socket has not taken yet: it waits for the socket to accept one and runs the worker again, rather than leaving the loop. Application-driven mode answers session requests here, and the peer waits on that reply, so a blocked one ended a session the legacy accept() carried through. - the worker runs on a writable socket as well as a readable one, or the owed send is never retried - the ssh socket joins the select write set only while a want-write is outstanding --- examples/echoserver/echoserver.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 7a2bb768c..732d4e8b4 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1148,6 +1148,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) { /* Parent process */ + int wantWrite = 0; #ifdef WOLFSSH_AGENT WS_SOCKET_T agentFd = -1; word32 agentChannelId = -1; @@ -1161,6 +1162,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) while (ChildRunning) { fd_set readFds; + fd_set writeFds; + int writable; WS_SOCKET_T maxFd; int cnt_r; int cnt_w; @@ -1169,6 +1172,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) FD_SET(sshFd, &readFds); maxFd = sshFd; + FD_ZERO(&writeFds); + if (wantWrite) + FD_SET(sshFd, &writeFds); + #ifdef WOLFSSH_AGENT /* The peer's auth-agent-req lands after wolfSSH_accept() has * already returned in application-driven mode, so the channel @@ -1221,12 +1228,15 @@ static int ssh_worker(thread_ctx_t* threadCtx) } #endif /* WOLFSSH_FWD */ - rc = select((int)maxFd + 1, &readFds, NULL, NULL, NULL); + rc = select((int)maxFd + 1, &readFds, + wantWrite ? &writeFds : NULL, NULL, NULL); if (rc == -1) { break; } + writable = wantWrite && FD_ISSET(sshFd, &writeFds); + wantWrite = 0; - if (FD_ISSET(sshFd, &readFds)) { + if (FD_ISSET(sshFd, &readFds) || writable) { word32 lastChannel = 0; /* The following tries to read from the first channel inside @@ -1461,6 +1471,14 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } + else if (rc == WS_WANT_WRITE) { + /* The send is owed, not lost: wait for the socket to + * take it. Application-driven mode answers session + * requests here, so a blocked reply would otherwise + * end a session accept() used to carry through. */ + wantWrite = 1; + continue; + } else if (rc != WS_WANT_READ) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", From 46a006b58d5b96d7af0bc6590129a28514733125 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 11 Sep 2026 14:38:06 -0700 Subject: [PATCH 6/6] tests: cover the app-driven sftp and shell paths The -A cases in scp.test reach the exec callback and the scp handoff. These drive the rest: the subsystem callback with the sftp accept, the shell callback in echo mode, and both accepts on a blocking server. - sftp.test connects to an -A server blocking and non-blocking - sshclient.test runs a terminal session and a command session against an -A server - scp.test copies from a blocking -A server, where wolfSSH_SCP_accept() completes in one call rather than through the retry loop --- scripts/scp.test | 20 ++++++++++++++++++++ scripts/sftp.test | 35 +++++++++++++++++++++++++++++++++++ scripts/sshclient.test | 22 +++++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/scripts/scp.test b/scripts/scp.test index 16064333f..cfa69739c 100755 --- a/scripts/scp.test +++ b/scripts/scp.test @@ -178,6 +178,26 @@ else exit 1 fi +# The same handoff on a blocking server, where wolfSSH_SCP_accept() runs to +# completion in one call rather than through the retry loop above. +echo "Test basic copy from server to local, blocking app-driven server" +./examples/echoserver/echoserver -A -1 -R $ready_file & +server_pid=$! +create_port +$run_client ./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test +RESULT=$? +remove_ready_file +stop_server +check_timeout $RESULT "basic copy from server to local, blocking app-driven server" + +if test -e $PWD/scp.test; then + rm $PWD/scp.test +else + echo -e "\n\nfailed to get file from blocking app-driven server" + do_cleanup + exit 1 +fi + echo "Test of getting empty file" touch $PWD/scripts/empty ./examples/echoserver/echoserver -1 -R $ready_file & diff --git a/scripts/sftp.test b/scripts/sftp.test index 3d5304cfe..9e551df15 100755 --- a/scripts/sftp.test +++ b/scripts/sftp.test @@ -112,6 +112,41 @@ if [ $RESULT -ne 0 ]; then exit 1 fi +# With -A the echoserver answers the subsystem request in its own callback +# and serves the session through wolfSSH_SFTP_accept(); the cases above take +# the accept() re-entry instead. +if [ "$nonblockingOnly" = 0 ]; then + echo "Test connection to an app-driven server" + ./examples/echoserver/echoserver -A -1 -R "$ready_file" & + server_pid=$! + create_port + echo "exit" | ./examples/sftpclient/wolfsftp -u jill -P upthehill -p "$port" + RESULT=$? + remove_ready_file + if [ $RESULT -ne 0 ]; then + echo + echo "failed to connect to app-driven server" + do_cleanup + exit 1 + fi +fi + +# The same handoff on a non-blocking server, which reaches the accept call's +# want-read and want-write retries. +echo "Test non blocking connection to an app-driven server" +./examples/echoserver/echoserver -A -N -1 -R "$ready_file" & +server_pid=$! +create_port +echo "exit" | ./examples/sftpclient/wolfsftp -N -u jill -P upthehill -p "$port" +RESULT=$? +remove_ready_file +if [ $RESULT -ne 0 ]; then + echo + echo "failed to connect to non blocking app-driven server" + do_cleanup + exit 1 +fi + # Test want write return from highwater callback if [ "$nonblockingOnly" = 0 ]; then echo "Test want write return from highwater callback" diff --git a/scripts/sshclient.test b/scripts/sshclient.test index af145f968..762f457fe 100755 --- a/scripts/sshclient.test +++ b/scripts/sshclient.test @@ -83,6 +83,7 @@ trap do_trap INT TERM # -f keeps the server in echo mode. Without it a build with shell support # tries to fork a login shell for the user, which fails since jill isn't a # real account, and the session ends before anything crosses the channel. +# Any arguments are passed on to the echoserver. start_server() { # The -1 server exits after its connection, but a client run that failed # before connecting leaves one listening. Reap it, server_pid is about @@ -95,7 +96,7 @@ start_server() { fi rm -f "$ready_file" - ./examples/echoserver/echoserver -1 -f -R "$ready_file" \ + ./examples/echoserver/echoserver -1 -f "$@" -R "$ready_file" \ > "$work_dir/server.log" 2>&1 & server_pid=$! @@ -282,6 +283,25 @@ if [ -s "$work_dir/terminal.log" ]; then && fail "the client sent a command it wasn't given" fi +# -A hands the session requests to the echoserver's own callbacks: the shell +# callback claims the channel the worker echoes on, and an exec request that +# is not a transfer runs as a session through the same callback. +echo "Test terminal session, app-driven server" +start_server -A +run_client "" -E "$work_dir/appterm.log" -p $port jill@127.0.0.1 +[ $? -ne 0 ] && fail "failed to open the terminal session on an app-driven server" + +grep -q "hello" "$client_out" \ + || fail "the app-driven echoserver's reply didn't make it back" + +echo "Test a session given a command, app-driven server" +start_server -A +run_client "" -E "$work_dir/appcommand.log" -p $port jill@127.0.0.1 "echo three" +[ $? -ne 0 ] && fail "failed to open the session on an app-driven server" + +grep -q "hello" "$client_out" \ + || fail "the app-driven echoserver's reply didn't make it back" + echo "Test the usage message" ./apps/wolfssh/wolfssh -Z 2>&1 | grep -q "usage:" \ || fail "no usage message for a bad option"