Repository: alexcrichton/ssh2-rs (language: rust)

Issue:
Error on keyboard interactive auth
System: M1 Mac @ macOS 14.5

Hi, I recently try login my company sftp server with keyboard interactive auth, But I got: 

```
thread 'sftp_test::sync_test::test_sync' panicked at library/core/src/panicking.rs:156:5:
unsafe precondition(s) violated: slice::from_raw_parts requires the pointer to be aligned and non-null, and the total size of the slice not to exceed `isize::MAX`
stack backtrace:
   0: _rust_begin_unwind
   1: core::panicking::panic_nounwind_fmt
   2: core::panicking::panic_nounwind
   3: core::slice::raw::from_raw_parts::precondition_check
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/core/src/intrinsics.rs:2799:21
   4: core::slice::raw::from_raw_parts
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/core/src/slice/raw.rs:98:9
   5: ssh2::session::Session::userauth_keyboard_interactive::prompt::{{closure}}
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ssh2-0.9.4/src/session.rs:391:30
   6: core::ops::function::FnOnce::call_once
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/core/src/ops/function.rs:250:5
   7: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/core/src/panic/unwind_safe.rs:272:9
   8: std::panicking::try::do_call
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/std/src/panicking.rs:552:40
   9: ___rust_try
  10: std::panicking::try
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/std/src/panicking.rs:516:19
  11: std::panic::catch_unwind
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/std/src/panic.rs:146:14
  12: ssh2::session::Session::userauth_keyboard_interactive::prompt
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ssh2-0.9.4/src/session.rs:387:21
  13: userauth_keyboard_interactive
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libssh2-sys-0.3.0/libssh2/src/userauth.c:2022:13
  14: libssh2_userauth_keyboard_interactive_ex
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libssh2-sys-0.3.0/libssh2/src/userauth.c:2162:5
  15: ssh2::session::Session::userauth_keyboard_interactive::{{closure}}
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ssh2-0.9.4/src/session.rs:458:26
  16: ssh2::session::with_abstract
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ssh2-0.9.4/src/session.rs:91:15
  17: ssh2::session::Session::userauth_keyboard_interactive
             at /Users/geniusv/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ssh2-0.9.4/src/session.rs:457:13
  18: crate_test::sftp_test::sync_test::test_sync
             at ./src/sftp_test/sync_test.rs:80:11
  19: crate_test::sftp_test::sync_test::test_sync::{{closure}}
             at ./src/sftp_test/sync_test.rs:19:19
  20: core::ops::function::FnOnce::call_once
             at /private/tmp/rust-20240518-7986-rmyaj5/rustc-1.78.0-src/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
thread caused non-unwinding panic. aborting.
error: test failed, to rerun pass `--lib`

Caused by:
  process didn't exit successfully: `/Users/geniusv/Projects/rust/rust-test/target/debug/deps/crate_test-e8c1a50874cb9730 'sftp_test::sync_test::test_sync' --format=json --exact -Z unstable-options --show-output` (signal: 6, SIGABRT: process abort signal)
error: 1 target failed:
    `--lib`
```

My test code: 

``` rust
#[test]
fn test_sync() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to the SFTP server
    let tcp = TcpStream::connect("ftp.comany.com:22")?;
    let mut session = Session::new().unwrap();
    session.set_tcp_stream(tcp);
    session.handshake()?;
    session.host_key().unwrap();
    let methods = session.auth_methods("dla_sftp").unwrap();
    assert!(
        methods.contains("keyboard-interactive"),
        "test server () must support `ChallengeResponseAuthentication yes`"
    );
    assert!(!session.authenticated());


    // Perform interactive authentication
    struct Prompter {
        some_data: usize,
    }

    impl KeyboardInteractivePrompt for Prompter {
        fn prompt<'a>(
            &mut self,
            username: &str,
            instructions: &str,
            prompts: &[Prompt<'a>],
        ) -> Vec<String> {
            assert_eq!(self.some_data, 42);

            eprintln!("username: {}", username);
            eprintln!("instructions: {}", instructions);
            eprintln!("prompts: {:?}", prompts);

            if prompts.len() == 1 {
                assert_eq!(prompts.len(), 1);
                // Might be "Password: " or "Password:" or other variations
                assert!(prompts[0].text.contains("sword"));
                assert_eq!(prompts[0].echo, false);
            } else {
                // maybe there's some PAM configuration that results
                // in multiple prompts. We can't make any real assertions
                // in this case, other than that there has to be at least
                // one prompt.
                assert!(!prompts.is_empty());
            }

            prompts.iter().map(|_| "bogus".to_string()).collect()
        }
    }

    let mut p = Prompter { some_data: 42 };

    match session.userauth_keyboard_interactive("dla_sftp", &mut p) {
        Ok(_) => eprintln!("auth succeeded somehow(!)"),
        Err(err) => eprintln!("auth failed as expected: {}", err),
    };

    Ok(())
}
```











Discussion / hints:
Same thing happening over here ✋ M3 Mac
Same here on an M1 Mac. 

The same crash does *not* occur on Linux with x86-64 (AMD 7950X) with the same code, so I suspect it's either MacOS related or just ARM in general
It seems my company server will not return username, instruction, prompts, responses in some cases and the code did not expect that. I have quickly adjusted the code and it works but not good enough to PR. Maybe someday I will...

Fix patch:
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 4fdb76e31..ccf9efcea 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-20.04]
+        os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
         rust_toolchain: [stable, beta]
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
index 0c5ebb9f9..520390033 100644
--- a/.github/workflows/macos.yml
+++ b/.github/workflows/macos.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [macos-12, macos-11]
+        os: [macos-12, macos-13, macos-14]
     runs-on: ${{ matrix.os }}
     steps:
     - uses: actions/checkout@v1
diff --git a/src/session.rs b/src/session.rs
index 7b9fe0218..5bea88a85 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -386,16 +386,29 @@ impl Session {
             // There's not much to be done with them though because the
             // signature of the callback doesn't allow reporting an error.
             let _ = catch_unwind(AssertUnwindSafe(|| {
+                if prompts.is_null() || responses.is_null() || num_prompts < 0 {
+                    return;
+                }
+
                 let prompter = unsafe { &mut **(abstrakt as *mut *mut P) };
 
-                let username =
-                    unsafe { slice::from_raw_parts(username as *const u8, username_len as usize) };
-                let username = String::from_utf8_lossy(username);
+                let username = if !username.is_null() && username_len >= 0 {
+                    let username = unsafe {
+                        slice::from_raw_parts(username as *const u8, username_len as usize)
+                    };
+                    String::from_utf8_lossy(username)
+                } else {
+                    Cow::Borrowed("")
+                };
 
-                let instruction = unsafe {
-                    slice::from_raw_parts(instruction as *const u8, instruction_len as usize)
+                let instruction = if !instruction.is_null() && instruction_len >= 0 {
+                    let instruction = unsafe {
+                        slice::from_raw_parts(instruction as *const u8, instruction_len as usize)
+                    };
+                    String::from_utf8_lossy(instruction)
+                } else {
+                    Cow::Borrowed("")
                 };
-                let instruction = String::from_utf8_lossy(instruction);
 
                 let prompts = unsafe { slice::from_raw_parts(prompts, num_prompts as usize) };
                 let responses =


Test patch:
diff --git a/systest/Cargo.toml b/systest/Cargo.toml
index dd7815553..09e06f5fc 100644
--- a/systest/Cargo.toml
+++ b/systest/Cargo.toml
@@ -9,4 +9,4 @@ libssh2-sys = { path = "../libssh2-sys" }
 libc = "0.2"
 
 [build-dependencies]
-ctest = "0.2"
+ctest2 = "0.4"
diff --git a/systest/build.rs b/systest/build.rs
index 0d5773fc0..6fbdf1661 100644
--- a/systest/build.rs
+++ b/systest/build.rs
@@ -1,9 +1,9 @@
-extern crate ctest;
+extern crate ctest2;
 
 use std::env;
 
 fn main() {
-    let mut cfg = ctest::TestGenerator::new();
+    let mut cfg = ctest2::TestGenerator::new();
     cfg.header("libssh2.h")
         .header("libssh2_publickey.h")
         .header("libssh2_sftp.h")
diff --git a/tests/run_integration_tests.sh b/tests/run_integration_tests.sh
index 521c30dfe..cf467f102 100755
--- a/tests/run_integration_tests.sh
+++ b/tests/run_integration_tests.sh
@@ -7,6 +7,7 @@ set -x
 
 # Tell the tests to use the port number we're using to spawn this server
 export RUST_SSH2_FIXTURE_PORT=8022
+export RUST_BACKTRACE=1
 
 cleanup() {
   # Stop the ssh server and local ssh agent