Pintuzoft opened a new issue, #952:
URL: https://github.com/apache/mina-sshd/issues/952

   ### Version
   
   3.0.0-M7 (same code on `dev_3.0`)
   
   ### Bug description
   
   Rarely, a client sends its first key exchange message (type 30) before its 
own `SSH_MSG_KEXINIT`.
   
   `IdentFilter.WriteHandler.send()` queues the KEXINIT behind the 
identification line. When that write completes, the listener releases the queue 
*before* it passes the KEXINIT on:
   
   ```java
   queue.addListener(f -> {
       lastWrite.compareAndSet(result, null);   // queue released
       if (f.isWritten()) {
           owner().send(cmd, message)...        // KEXINIT passed on
   ```
   
   `KexFilter.sendKexInit()` counts the KEXINIT as sent when `forward.send()` 
returns, not when it is written. So the negotiation can send 30 in that gap; it 
finds `lastWrite == null` and goes out first.
   
   Reproducer, using only sshd-core, sshd-common and slf4j-api. It prints `[-1, 
30, 20]` (-1 is the identification line):
   
   <details><summary>IdentFilterOrder.java</summary>
   
   ```java
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   import org.apache.sshd.common.PropertyResolver;
   import org.apache.sshd.common.SshConstants;
   import org.apache.sshd.common.filter.DefaultFilterChain;
   import org.apache.sshd.common.filter.InputHandler;
   import org.apache.sshd.common.filter.IoFilter;
   import org.apache.sshd.common.filter.OutputHandler;
   import org.apache.sshd.common.io.DefaultIoWriteFuture;
   import org.apache.sshd.common.session.filters.IdentFilter;
   import org.apache.sshd.common.session.filters.SshIdentHandler;
   import org.apache.sshd.common.util.buffer.Buffer;
   import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
   
   // Chain [wire, moment, IdentFilter, top]. KEXINIT is queued behind the 
identification line; when it is
   // passed on, "moment" (right below IdentFilter) lets another thread send 30 
from the top first.
   public class IdentFilterOrder {
   
       static final List<Integer> wire = Collections.synchronizedList(new 
ArrayList<>());
       static final DefaultIoWriteFuture identWritten = new 
DefaultIoWriteFuture("ident", null);
       static IoFilter top;
   
       static Buffer packet(int cmd) {
           ByteArrayBuffer buffer = new ByteArrayBuffer();
           buffer.putByte((byte) cmd);
           return buffer;
       }
   
       static IoFilter filter(OutputHandler out) {
           return new IoFilter() {
               @Override
               public InputHandler in() {
                   return null;
               }
   
               @Override
               public OutputHandler out() {
                   return out;
               }
           };
       }
   
       public static void main(String[] args) throws Exception {
           IoFilter socket = filter((cmd, message) -> {
               wire.add(cmd);
               if (cmd < 0) {
                   return identWritten; // the identification line, written 
when we say so
               }
               DefaultIoWriteFuture written = new DefaultIoWriteFuture(cmd, 
null);
               written.setValue(Boolean.TRUE);
               return written;
           });
           IoFilter[] moment = new IoFilter[1];
           moment[0] = filter((cmd, message) -> {
               if (cmd == SshConstants.SSH_MSG_KEXINIT) {
                   Thread other = new Thread(() -> {
                       try {
                           top.owner().send(SshConstants.SSH_MSG_KEX_FIRST, 
packet(SshConstants.SSH_MSG_KEX_FIRST));
                       } catch (Exception e) {
                           throw new RuntimeException(e);
                       }
                   });
                   other.start();
                   try {
                       other.join();
                   } catch (InterruptedException e) {
                       throw new java.io.InterruptedIOException();
                   }
               }
               return moment[0].owner().send(cmd, message);
           });
           IdentFilter ident = new IdentFilter();
           ident.setPropertyResolver(PropertyResolver.EMPTY);
           ident.setIdentHandler(new SshIdentHandler() {
               @Override
               public boolean isServer() {
                   return false;
               }
   
               @Override
               public List<String> readIdentification(Buffer buffer) {
                   return List.of();
               }
   
               @Override
               public List<String> provideIdentification() {
                   return List.of("SSH-2.0-IdentFilterOrder");
               }
           });
           top = filter(null);
   
           DefaultFilterChain chain = new DefaultFilterChain();
           chain.addLast(socket);
           chain.addLast(moment[0]);
           chain.addLast(ident);
           chain.addLast(top);
   
           top.owner().send(SshConstants.SSH_MSG_KEXINIT, 
packet(SshConstants.SSH_MSG_KEXINIT)); // queued
           identWritten.setValue(Boolean.TRUE);
           System.out.println(wire); // expected [-1, 20, 30]
       }
   }
   ```
   
   </details>
   
   ### Actual behavior
   
   Identification, 30, KEXINIT. OpenSSH with strict KEX answers the early 30 
with `SSH_MSG_UNIMPLEMENTED`, and the client fails:
   
   ```
   org.apache.sshd.common.SshException: SSH_MSG_UNIMPLEMENTED not allowed 
during initial key exchange in strict KEX
       at 
org.apache.sshd.common.session.filters.kex.KexFilter$KexInputHandler.handleMessage(KexFilter.java:1216)
   ```
   
   ### Expected behavior
   
   KEXINIT is the first packet after the identification line.
   
   ### Other information
   
   Peer: OpenSSH 9.2p1 (Debian bookworm). Seen once in a night of CI tests, and 
once in 6000 connections with a plain `SshClient.setUpDefaultClient()` against 
a server that sends its KEXINIT at once.
   
   Possible fix: release the queue after the message is passed on. With this, 
the reproducer prints `[-1, 20, 30]` and 2000 handshakes and logins against an 
`SshServer` work:
   
   ```diff
                queue.addListener(f -> {
   -                lastWrite.compareAndSet(result, null);
                    if (f.isWritten()) {
    ...
                    }
   +                lastWrite.compareAndSet(result, null);
                });
   ```
   
   Found and analysed with Claude (Anthropic's AI assistant, Claude Opus 5.5).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to