Skip to content

Commit d5d6096

Browse files
authored
Fix #805: Prevent CHANNEL_CLOSE to be sent between Channel.isOpen and… (#813)
* Fix #805: Prevent CHANNEL_CLOSE to be sent between Channel.isOpen and a Transport.write call Otherwise, a disconnect with a "packet referred to nonexistent channel" message can occur. This particularly happens when the transport.Reader thread passes an eof from the server to the ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server. * Add integration test for #805
1 parent 2551f8e commit d5d6096

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.hierynomus.sshj
17+
18+
import net.schmizz.sshj.SSHClient
19+
import net.schmizz.sshj.common.IOUtils
20+
import net.schmizz.sshj.connection.channel.direct.Session
21+
import spock.lang.Specification
22+
23+
import java.util.concurrent.*
24+
25+
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
26+
27+
class ManyChannelsSpec extends Specification {
28+
29+
def "should work with many channels without nonexistent channel error (GH issue #805)"() {
30+
given:
31+
SshdContainer sshd = new SshdContainer.Builder()
32+
.withSshdConfig("""${SshdContainer.Builder.DEFAULT_SSHD_CONFIG}
33+
MaxSessions 200
34+
""".stripMargin())
35+
.build()
36+
sshd.start()
37+
SSHClient client = sshd.getConnectedClient()
38+
client.authPublickey("sshj", "src/test/resources/id_rsa")
39+
40+
when:
41+
List<Future<Exception>> futures = []
42+
ExecutorService executorService = Executors.newCachedThreadPool()
43+
44+
for (int i in 0..20) {
45+
futures.add(executorService.submit((Callable<Exception>) {
46+
return execute(client)
47+
}))
48+
}
49+
executorService.shutdown()
50+
executorService.awaitTermination(1, TimeUnit.DAYS)
51+
52+
then:
53+
futures*.get().findAll { it != null }.empty
54+
55+
cleanup:
56+
client.close()
57+
}
58+
59+
60+
private static Exception execute(SSHClient sshClient) {
61+
try {
62+
for (def i in 0..100) {
63+
withCloseable (sshClient.startSession()) {sshSession ->
64+
Session.Command sshCommand = sshSession.exec("ls -la")
65+
IOUtils.readFully(sshCommand.getInputStream()).toString()
66+
sshCommand.close()
67+
}
68+
}
69+
} catch (Exception e) {
70+
return e
71+
}
72+
return null
73+
}
74+
}

src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,25 @@ public boolean isOpen() {
304304
}
305305
}
306306

307+
// Prevent CHANNEL_CLOSE to be sent between isOpen and a Transport.write call in the runnable, otherwise
308+
// a disconnect with a "packet referred to nonexistent channel" message can occur.
309+
//
310+
// This particularly happens when the transport.Reader thread passes an eof from the server to the
311+
// ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the
312+
// transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server.
313+
boolean whileOpen(TransportRunnable runnable) throws TransportException, ConnectionException {
314+
openCloseLock.lock();
315+
try {
316+
if (isOpen()) {
317+
runnable.run();
318+
return true;
319+
}
320+
} finally {
321+
openCloseLock.unlock();
322+
}
323+
return false;
324+
}
325+
307326
private void gotChannelRequest(SSHPacket buf)
308327
throws ConnectionException, TransportException {
309328
final String reqType;
@@ -427,5 +446,8 @@ public String toString() {
427446
+ rwin + " >";
428447
}
429448

449+
public interface TransportRunnable {
450+
void run() throws TransportException, ConnectionException;
451+
}
430452

431453
}

src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*/
3131
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
3232

33-
private final Channel chan;
33+
private final AbstractChannel chan;
3434
private final Transport trans;
3535
private final Window.Remote win;
3636

@@ -47,6 +47,12 @@ private final class DataBuffer {
4747

4848
private final SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA);
4949
private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer();
50+
private final AbstractChannel.TransportRunnable packetWriteRunnable = new AbstractChannel.TransportRunnable() {
51+
@Override
52+
public void run() throws TransportException {
53+
trans.write(packet);
54+
}
55+
};
5056

5157
DataBuffer() {
5258
headerOffset = packet.rpos();
@@ -99,8 +105,9 @@ boolean flush(int bufferSize, boolean canAwaitExpansion) throws TransportExcepti
99105
if (leftOverBytes > 0) {
100106
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
101107
}
102-
103-
trans.write(packet);
108+
if (!chan.whileOpen(packetWriteRunnable)) {
109+
throwStreamClosed();
110+
}
104111
win.consume(writeNow);
105112

106113
packet.rpos(headerOffset);
@@ -119,7 +126,7 @@ boolean flush(int bufferSize, boolean canAwaitExpansion) throws TransportExcepti
119126

120127
}
121128

122-
public ChannelOutputStream(Channel chan, Transport trans, Window.Remote win) {
129+
public ChannelOutputStream(AbstractChannel chan, Transport trans, Window.Remote win) {
123130
this.chan = chan;
124131
this.trans = trans;
125132
this.win = win;
@@ -157,17 +164,22 @@ private void checkClose() throws SSHException {
157164
if (error != null) {
158165
throw error;
159166
} else {
160-
throw new ConnectionException("Stream closed");
167+
throwStreamClosed();
161168
}
162169
}
163170
}
164171

165172
@Override
166173
public synchronized void close() throws IOException {
167174
// Not closed yet, and underlying channel is open to flush the data to.
168-
if (!closed.getAndSet(true) && chan.isOpen()) {
169-
buffer.flush(false);
170-
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
175+
if (!closed.getAndSet(true)) {
176+
chan.whileOpen(new AbstractChannel.TransportRunnable() {
177+
@Override
178+
public void run() throws TransportException, ConnectionException {
179+
buffer.flush(false);
180+
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
181+
}
182+
});
171183
}
172184
}
173185

@@ -188,4 +200,7 @@ public String toString() {
188200
return "< ChannelOutputStream for Channel #" + chan.getID() + " >";
189201
}
190202

203+
private static void throwStreamClosed() throws ConnectionException {
204+
throw new ConnectionException("Stream closed");
205+
}
191206
}

0 commit comments

Comments
 (0)