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 connection holds the shell's pid, so the worker loop's exit and
  an accept() whose reply failed end the child too
- 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
pull/1254/head
John Safranek 2026-09-11 13:58:24 -07:00 committed by Paul Adelsbach
parent 6b954da7f7
commit eef5346f7f
1 changed files with 99 additions and 19 deletions

View File

@ -86,6 +86,7 @@
#endif
#ifndef USE_WINDOWS_API
#include <pwd.h>
#include <sys/wait.h>
#endif
#include <signal.h>
#if defined(__QNX__) || defined(__QNXNTO__)
@ -221,6 +222,11 @@ typedef struct {
WS_FwdCbActionCtx fwdCbCtx;
#endif
WS_AppCtx shellCtx;
#ifdef WOLFSSH_SHELL
/* The forked shell. Held here because the callback that forks is gone
* by the time the session ends. */
pid_t shellPid;
#endif
#ifdef WOLFSSH_SFTP
int doSftp;
#endif
@ -692,6 +698,29 @@ static void ChildSig(int sig)
}
/* End a forked shell and its pty. A shell left behind runs on behind a pty
* nothing reads, and its exit clears ChildRunning out from under a worker
* loop -- this connection's, or another's, since the flag is shared. */
static void ShellChildCleanup(thread_ctx_t* threadCtx)
{
if (threadCtx->shellCtx.appFd >= 0) {
WCLOSESOCKET(threadCtx->shellCtx.appFd);
threadCtx->shellCtx.appFd = -1;
}
if (threadCtx->shellPid > 0) {
void (*prevSig)(int);
/* This exit is ours, not the session's. */
prevSig = signal(SIGCHLD, SIG_DFL);
kill(threadCtx->shellPid, SIGKILL);
waitpid(threadCtx->shellPid, NULL, 0);
signal(SIGCHLD, prevSig);
threadCtx->shellPid = -1;
}
}
#ifdef SHELL_DEBUG
static int termios_show(int fd)
{
@ -717,6 +746,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 +780,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 +816,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 +845,26 @@ static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx)
#ifdef SHELL_DEBUG
printf("In childPid > 0; getpid=%d\n", (int)getpid());
#endif
signal(SIGCHLD, ChildSig);
/* The child is the connection's from here, so every exit below can
* end it. */
threadCtx->shellPid = childPid;
rc = tcgetattr(threadCtx->shellCtx.appFd, &tios);
if (rc != 0) {
printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno);
ShellChildCleanup(threadCtx);
return 1;
}
rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios);
if (rc != 0) {
printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno);
ShellChildCleanup(threadCtx);
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 +910,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 +941,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 +1123,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);
@ -1190,8 +1261,11 @@ static int ssh_worker(thread_ctx_t* threadCtx)
/* The peer is done sending: hand back the backlog and answer
* its EOF, since the library no longer answers for us. Off
* the channel's own state, not the once-only WS_EOF status.
* Echo mode only; a shell child on a pty still produces. */
if (!eofAnswered && echoOnly) {
* Echo mode only; a shell child on a pty still produces.
* 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,
@ -1649,10 +1723,7 @@ static int ssh_worker(thread_ctx_t* threadCtx)
#endif /* WOLFSSH_FWD */
}
#ifdef WOLFSSH_SHELL
if (threadCtx->shellCtx.appFd >= 0) {
WCLOSESOCKET(threadCtx->shellCtx.appFd);
threadCtx->shellCtx.appFd = -1;
}
ShellChildCleanup(threadCtx);
#endif
}
@ -1664,7 +1735,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
@ -1990,6 +2061,12 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
break;
}
#ifdef WOLFSSH_SHELL
/* The session request is answered inside wolfSSH_accept(), so a reply
* that fails leaves a forked shell with no ssh_worker() to end it. */
ShellChildCleanup(threadCtx);
#endif
if (ret == WS_FATAL_ERROR) {
const char* errorStr;
error = wolfSSH_get_error(threadCtx->ssh);
@ -4349,6 +4426,9 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args)
threadCtx->shellCtx.listenFd = -1;
threadCtx->shellCtx.appFd = -1;
threadCtx->shellCtx.state = APP_STATE_INIT;
#ifdef WOLFSSH_SHELL
threadCtx->shellPid = -1;
#endif
#ifdef WOLFSSH_AGENT
threadCtx->agentCtx.privateData = &threadCtx->agentCbCtx;
threadCtx->agentCtx.listenFd = -1;