name: wolfSSH Paramiko SFTP Test on: push: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build_wolfssl: name: Build wolfssl runs-on: ubuntu-latest timeout-minutes: 4 steps: - name: Checking cache for wolfssl uses: actions/cache@v5 id: cache-wolfssl with: path: build-dir/ key: wolfssh-paramiko-sftp-wolfssl-ubuntu-latest lookup-only: true - name: Checkout, build, and install wolfssl if: steps.cache-wolfssl.outputs.cache-hit != 'true' uses: wolfSSL/actions-build-autotools-project@v1 with: repository: wolfssl/wolfssl ref: master path: wolfssl configure: --enable-all check: false install: true paramiko_sftp_test: needs: build_wolfssl name: Paramiko SFTP Test runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checking cache for wolfssl uses: actions/cache@v5 with: path: build-dir/ key: wolfssh-paramiko-sftp-wolfssl-ubuntu-latest fail-on-cache-miss: true - uses: actions/checkout@v6 with: path: wolfssh/ - name: autogen working-directory: ./wolfssh/ run: ./autogen.sh - name: configure working-directory: ./wolfssh/ run: | ./configure --enable-all LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI" - name: make working-directory: ./wolfssh/ run: make - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y python3-pip openssh-client python3 -m pip install paramiko - name: Create test directories run: | mkdir -p /tmp/sftp_upload mkdir -p /tmp/sftp_download - name: Create 20MB test file for upload run: | dd if=/dev/urandom of=/tmp/sftp_upload/test_upload.dat bs=1M count=20 echo "Created 20MB test file at /tmp/sftp_upload/test_upload.dat" md5sum /tmp/sftp_upload/test_upload.dat - name: Configure wolfSSHd working-directory: ./wolfssh/ run: | # Create a minimal sshd_config file cat > sshd_config.txt << EOF Port 22222 HostKey ./keys/server-key.pem PasswordAuthentication yes Subsystem sftp internal-sftp EOF # Set proper permissions for keys. wolfSSHd loads the host key # through the secure gate, which refuses a key not owned by root or # the daemon's user, or one that is group/world readable or writable. # The daemon is launched with sudo (euid 0) while the checkout key is # owned by the runner, so make it root-owned and 0600. chmod 600 ./keys/server-key.pem sudo chown 0:0 ./keys/server-key.pem # Print debug info echo "Contents of sshd_config.txt:" cat sshd_config.txt - name: Start wolfSSHd working-directory: ./wolfssh/ run: | # Create a test user with known password echo "Creating test user..." sudo useradd -m testuser echo "testuser:testpassword" | sudo chpasswd # Start wolfSSHd with debug output echo "Starting wolfSSHd..." sudo ./apps/wolfsshd/wolfsshd -f sshd_config.txt -h ./keys/server-key.pem -p 22222 -d & echo "Started wolfSSHd on port 22222" sleep 5 # Give the server time to start # Check if server is running if ! nc -z 127.0.0.1 22222; then echo "Error: wolfSSHd failed to start" exit 1 fi # Print debug info echo "wolfSSHd process info:" ps aux | grep wolfsshd - name: Create Paramiko SFTP test script run: | cat > /tmp/paramiko_sftp_test.py << 'EOF' import paramiko import os import time import sys import hashlib import signal # Timeout handler for detecting hangs class TimeoutError(Exception): pass def timeout_handler(signum, frame): raise TimeoutError("SFTP operation timed out - possible hang detected!") def get_file_hash(filepath): """Calculate MD5 hash of a file.""" hash_md5 = hashlib.md5() with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): hash_md5.update(chunk) return hash_md5.hexdigest() def run_sftp_test(): # Create SSH client ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # Connect to server using password authentication with testuser print("Connecting to wolfSSHd server...") try: ssh.connect('127.0.0.1', port=22222, username='testuser', password='testpassword') except Exception as e: print(f"Connection error: {e}") raise # Open SFTP session print("Opening SFTP session...") sftp = ssh.open_sftp() remote_path = 'test_upload.dat' # Upload test print("Uploading 20MB test file...") start_time = time.time() sftp.put('/tmp/sftp_upload/test_upload.dat', remote_path) upload_time = time.time() - start_time print(f"Upload completed in {upload_time:.2f} seconds") # Download test print("Downloading 20MB test file...") start_time = time.time() sftp.get(remote_path, '/tmp/sftp_download/test_download.dat') download_time = time.time() - start_time print(f"Download completed in {download_time:.2f} seconds") # Stress test: Repeated GET operations with prefetch # This tests the WS_WANT_WRITE handling during repeated transfers # which was the original bug trigger (SFTP hang on non-blocking sockets) print("\n=== Starting repeated GET stress test (prefetch enabled) ===") num_iterations = 10 timeout_seconds = 30 # Per-operation timeout to detect hangs orig_hash = get_file_hash('/tmp/sftp_upload/test_upload.dat') for i in range(num_iterations): signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout_seconds) try: download_path = f'/tmp/sftp_download/stress_test_{i}.dat' start_time = time.time() # Paramiko uses prefetch by default for get() sftp.get(remote_path, download_path) elapsed = time.time() - start_time # Verify integrity down_hash = get_file_hash(download_path) if orig_hash != down_hash: print(f" Iteration {i+1}: FAILED - hash mismatch!") return False print(f" Iteration {i+1}/{num_iterations}: OK ({elapsed:.2f}s)") os.remove(download_path) # Cleanup except TimeoutError as e: print(f" Iteration {i+1}: FAILED - {e}") print(" This may indicate the WS_WANT_WRITE hang bug!") return False finally: signal.alarm(0) # Always cancel alarm print(f"=== Stress test completed: {num_iterations} iterations OK ===\n") # Close connections sftp.close() ssh.close() print("SFTP session closed") return True if __name__ == "__main__": try: success = run_sftp_test() sys.exit(0 if success else 1) except Exception as e: print(f"Error: {e}") sys.exit(1) EOF - name: Run Paramiko SFTP test run: | python3 /tmp/paramiko_sftp_test.py - name: Verify file integrity run: | echo "Verifying file integrity..." if cmp -s /tmp/sftp_upload/test_upload.dat /tmp/sftp_download/test_download.dat; then echo "SFTP Test PASSED: Files match" else echo "SFTP Test FAILED: Files do not match" exit 1 fi - name: Stop wolfSSHd run: | sudo pkill wolfsshd || true