Skip to content

Commit 689fec4

Browse files
ejohnstownphilljj
authored andcommitted
Drain the shell channel before closing stdin
SHELL_Subsystem() hands the child whatever the peer sent, whichever pass it arrived on, and closes the write end of its stdin only once that buffer is dry. It works off the shell channel's own inputBuffer, so data held back while the window was full is still handed over; the old read ran only on the worker's WS_CHAN_RXD and was skipped outright while windowFull. - The channel id comes from the head of the channel list at entry, the only channel open there, rather than from DEFAULT_NEXT_CHANNEL, which a build can override. - A lookup that finds nothing is not an EOF: only a channel that is present and drained closes the pipe. - Data arriving behind the peer's EOF, which RFC 4254 section 5.3 forbids, is dropped rather than written to a stdin that is already closed. The write would fail with EBADF and end the session mid-stream. - The short-write retry tests for a -1 return before reading errno, which nothing else sets.
1 parent b8b4032 commit 689fec4

1 file changed

Lines changed: 120 additions & 38 deletions

File tree

apps/wolfsshd/wolfsshd.c

Lines changed: 120 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,17 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
10331033
size_t sz = 0;
10341034
WCHAR h[MAX_PATH];
10351035
char* forcedCmd;
1036+
WOLFSSH_CHANNEL* shellChannel;
1037+
1038+
/* Off the channel list, not DEFAULT_NEXT_CHANNEL, which a build can
1039+
* override. Same as the POSIX copy; this loop routes its reads and sends
1040+
* off the same id. */
1041+
shellChannel = wolfSSH_ChannelNext(ssh, NULL);
1042+
if (shellChannel == NULL || wolfSSH_ChannelGetId(shellChannel,
1043+
&shellChannelId, WS_CHANNEL_ID_SELF) != WS_SUCCESS) {
1044+
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No session channel to service");
1045+
return WS_FATAL_ERROR;
1046+
}
10361047

10371048
forcedCmd = wolfSSHD_ConfigGetForcedCmd(usrConf);
10381049

@@ -1811,6 +1822,23 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
18111822

18121823
struct termios tios;
18131824
word32 shellChannelId = 0;
1825+
WOLFSSH_CHANNEL* shellChannel;
1826+
1827+
/* Name the session channel off the channel list rather than trusting
1828+
* DEFAULT_NEXT_CHANNEL to be 0, which a build can override. It is the
1829+
* only channel open at this point; the agent channel comes later. The
1830+
* loop below closes the child's stdin off this channel, so a wrong id
1831+
* there drops the peer's input instead of handing it over. */
1832+
shellChannel = wolfSSH_ChannelNext(ssh, NULL);
1833+
if (shellChannel == NULL || wolfSSH_ChannelGetId(shellChannel,
1834+
&shellChannelId, WS_CHANNEL_ID_SELF) != WS_SUCCESS) {
1835+
/* The shell child is already forked, and nothing below can reach the
1836+
* peer without an id to address, so take it down with us. */
1837+
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No session channel to service");
1838+
kill(childPid, SIGKILL);
1839+
return WS_FATAL_ERROR;
1840+
}
1841+
18141842
signal(SIGCHLD, ChildSig);
18151843
signal(SIGINT, SIG_DFL);
18161844

@@ -1853,6 +1881,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
18531881
WS_SOCKET_T maxFd;
18541882
int cnt_r;
18551883
int cnt_w;
1884+
WOLFSSH_CHANNEL* current;
18561885
int pending = 0;
18571886

18581887
FD_ZERO(&readFds);
@@ -1864,7 +1893,17 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
18641893
FD_SET(sshFd, &writeFds);
18651894
}
18661895

1867-
if (wolfSSH_stream_peek(ssh, tmp, 1) <= 0) {
1896+
/* Buffered shell input is a wake condition in its own right.
1897+
* wolfSSH_stream_peek() goes blind once the channel is at EOF, so
1898+
* leaning on it alone would park undrained bytes in a select() that
1899+
* nothing else can wake. */
1900+
current = wolfSSH_ChannelFind(ssh, shellChannelId, WS_CHANNEL_ID_SELF);
1901+
if (current != NULL
1902+
&& current->inputBuffer.length > current->inputBuffer.idx) {
1903+
pending = 1;
1904+
}
1905+
1906+
if (!pending && wolfSSH_stream_peek(ssh, tmp, 1) <= 0) {
18681907
/* select on stdout/stderr pipes with forced commands */
18691908
if (!ptyReq || forcedCmd) {
18701909
FD_SET(stdoutPipe[0], &readFds);
@@ -1896,42 +1935,28 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
18961935
}
18971936

18981937
if (wantWrite || windowFull || pending || FD_ISSET(sshFd, &readFds)) {
1899-
word32 lastChannel = 0;
1938+
word32 avail;
19001939

19011940
wantWrite = 0;
1902-
/* The following tries to read from the first channel inside
1903-
the stream. If the pending data in the socket is for
1904-
another channel, this will return an error with id
1905-
WS_CHAN_RXD. That means the agent has pending data in its
1906-
channel. The additional channel is only used with the
1907-
agent. */
1908-
cnt_r = wolfSSH_worker(ssh, &lastChannel);
1941+
/* The worker services the transport. What lands on the shell
1942+
channel is handed to the child by the drain below, which names
1943+
the channel itself, so the id the worker would report here is
1944+
not needed. */
1945+
cnt_r = wolfSSH_worker(ssh, NULL);
19091946
if (cnt_r < 0) {
19101947
rc = wolfSSH_get_error(ssh);
19111948
if (rc == WS_CHAN_RXD) {
1912-
if (!windowFull) { /* don't rewrite channeldBuffer if full
1913-
* of windowFull left overs */
1914-
if (lastChannel == shellChannelId) {
1915-
cnt_r = wolfSSH_ChannelIdRead(ssh, shellChannelId,
1916-
channelBuffer,
1917-
sizeof channelBuffer);
1918-
if (cnt_r <= 0)
1919-
break;
1920-
1921-
if (!ptyReq || forcedCmd) {
1922-
cnt_w = (int)write(stdinPipe[1], channelBuffer,
1923-
cnt_r);
1924-
}
1925-
else {
1926-
cnt_w = (int)write(childFd, channelBuffer,
1927-
cnt_r);
1928-
}
1929-
if (cnt_w <= 0)
1930-
break;
1931-
}
1932-
}
1949+
/* Arrival only; the drain below owns the read. */
19331950
}
19341951
else if (rc == WS_CHANNEL_CLOSED) {
1952+
/* The channel is retired, so nothing more can reach the
1953+
* child and the drain below is skipped on this pass.
1954+
* Close its stdin here or it blocks forever on input
1955+
* that cannot come. */
1956+
if (stdinPipe[1] != -1 && (!ptyReq || forcedCmd)) {
1957+
close(stdinPipe[1]);
1958+
stdinPipe[1] = -1;
1959+
}
19351960
peerConnected = 0;
19361961
continue;
19371962
}
@@ -1950,15 +1975,72 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
19501975
}
19511976
}
19521977

1953-
/* did the channel just receive an EOF? */
1954-
if (cnt_r == 0) {
1955-
int eof;
1956-
WOLFSSH_CHANNEL* current;
1978+
/* The shell channel's own buffer, looked up again because the
1979+
* worker above can retire it. */
1980+
current = wolfSSH_ChannelFind(ssh, shellChannelId,
1981+
WS_CHANNEL_ID_SELF);
1982+
avail = (current != NULL) ?
1983+
current->inputBuffer.length - current->inputBuffer.idx : 0;
1984+
1985+
/* Hand over what the peer sent, this pass or left buffered
1986+
* while the window was full; this is the only copy. Gated on
1987+
* windowFull, and not because the buffers overlap -- shellBuffer
1988+
* and channelBuffer are disjoint. While the peer will not take
1989+
* the child's output, writing to the child's stdin deadlocks it:
1990+
* it blocks on a full stdout pipe, stops reading stdin, and this
1991+
* write never returns. The backlog clears as soon as the peer
1992+
* reads. One buffer per pass, since the write can block. */
1993+
if (avail > 0 && !windowFull) {
1994+
int off = 0;
1995+
1996+
cnt_r = wolfSSH_ChannelIdRead(ssh, shellChannelId,
1997+
channelBuffer, sizeof channelBuffer);
1998+
if (cnt_r <= 0)
1999+
break;
2000+
2001+
/* Data behind the peer's EOF, RFC 4254 section 5.3. Stdin
2002+
* is gone, so drop it rather than write to fd -1 and end the
2003+
* session on an EBADF. */
2004+
if ((!ptyReq || forcedCmd) && stdinPipe[1] == -1)
2005+
off = cnt_r;
2006+
2007+
/* The read took the bytes off the channel, so this is the
2008+
* only copy: a short write has to be finished, not dropped.
2009+
* A PTY master goes short whenever the line discipline fills,
2010+
* and a signal can cut a transfer already under way. */
2011+
while (off < cnt_r) {
2012+
if (!ptyReq || forcedCmd) {
2013+
cnt_w = (int)write(stdinPipe[1], channelBuffer + off,
2014+
cnt_r - off);
2015+
}
2016+
else {
2017+
cnt_w = (int)write(childFd, channelBuffer + off,
2018+
cnt_r - off);
2019+
}
2020+
if (cnt_w <= 0) {
2021+
/* errno only speaks for a -1 return. */
2022+
if (cnt_w < 0 && errno == EINTR)
2023+
continue;
2024+
break;
2025+
}
2026+
off += cnt_w;
2027+
}
2028+
if (off < cnt_r)
2029+
break;
2030+
2031+
avail = current->inputBuffer.length - current->inputBuffer.idx;
2032+
}
19572033

1958-
current = wolfSSH_ChannelFind(ssh, lastChannel,
1959-
WS_CHANNEL_ID_SELF);
1960-
eof = wolfSSH_ChannelGetEof(current);
1961-
if (eof && (!ptyReq || forcedCmd)) {
2034+
/* Peer done sending: close the child's stdin, but only once what
2035+
* it already sent has been handed over. Closing early drops it
2036+
* and the next write lands on fd -1. A channel that is gone is
2037+
* the peer being done too -- DoChannelClose() retires it, and the
2038+
* id was validated at entry, so a miss here cannot mean a wrong
2039+
* id. Leaving the pipe open then would block the child forever on
2040+
* a stdin nothing will ever close. */
2041+
if (stdinPipe[1] != -1 && (!ptyReq || forcedCmd)) {
2042+
if (current == NULL
2043+
|| (wolfSSH_ChannelGetEof(current) && avail == 0)) {
19622044
/* SSH is done, close stdin pipe to child process */
19632045
close(stdinPipe[1]);
19642046
stdinPipe[1] = -1;

0 commit comments

Comments
 (0)