OSS-Fuzz: Add fuzzer for scp functions

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
This commit is contained in:
Arthur Chan
2026-02-24 10:44:49 +00:00
committed by Jakub Jelen
parent 9d36b9dd81
commit 4dfcdd96b8
20 changed files with 556 additions and 0 deletions

View File

@@ -1,8 +1,19 @@
project(fuzzing CXX)
# Build SSH server mock helper as object library
add_library(ssh_server_mock_obj OBJECT ssh_server_mock.c)
target_link_libraries(ssh_server_mock_obj PRIVATE ${TORTURE_LINK_LIBRARIES})
macro(fuzzer name)
add_executable(${name} ${name}.c)
target_link_libraries(${name} PRIVATE ${TORTURE_LINK_LIBRARIES})
# Add ssh_server_mock dependency for scp and sftp fuzzers
if (${name} STREQUAL "ssh_scp_fuzzer" OR ${name} STREQUAL "ssh_sftp_fuzzer")
target_sources(${name} PRIVATE $<TARGET_OBJECTS:ssh_server_mock_obj>)
target_link_libraries(${name} PRIVATE pthread)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set_target_properties(${name}
PROPERTIES
@@ -36,6 +47,7 @@ fuzzer(ssh_pubkey_fuzzer)
fuzzer(ssh_sftp_attr_fuzzer)
fuzzer(ssh_sshsig_fuzzer)
if (WITH_SERVER)
fuzzer(ssh_scp_fuzzer)
fuzzer(ssh_server_fuzzer)
fuzzer(ssh_bind_config_fuzzer)
endif (WITH_SERVER)

206
tests/fuzz/ssh_scp_fuzzer.c Normal file
View File

@@ -0,0 +1,206 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define LIBSSH_STATIC 1
#include <libssh/libssh.h>
#include <libssh/scp.h>
#include "nallocinc.c"
#include "ssh_server_mock.h"
static void _fuzz_finalize(void)
{
ssh_finalize();
}
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
(void)argc;
nalloc_init(*argv[0]);
ssh_init();
atexit(_fuzz_finalize);
ssh_mock_write_hostkey(SSH_MOCK_HOSTKEY_PATH);
return 0;
}
/* Helper function to test one cipher/HMAC combination */
static int test_scp_with_cipher(const uint8_t *data,
size_t size,
const char *cipher,
const char *hmac)
{
int socket_fds[2] = {-1, -1};
ssh_session client_session = NULL;
ssh_scp scp = NULL, scp_recursive = NULL;
char buf[256] = {0};
pthread_t srv_thread;
/* Configure mock SSH server with fuzzer data */
struct ssh_mock_server_config server_config = {
.protocol_data = data,
.protocol_data_size = size,
.exec_callback = ssh_mock_send_raw_data,
.subsystem_callback = NULL,
.callback_userdata = NULL,
.cipher = cipher,
.hmac = hmac,
.server_socket = -1,
.client_socket = -1,
.server_ready = false,
.server_error = false,
};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fds) != 0) {
goto cleanup;
}
server_config.server_socket = socket_fds[0];
server_config.client_socket = socket_fds[1];
if (ssh_mock_server_start(&server_config, &srv_thread) != 0) {
goto cleanup;
}
client_session = ssh_new();
if (client_session == NULL) {
goto cleanup_thread;
}
/* Configure client with specified cipher/HMAC */
ssh_options_set(client_session, SSH_OPTIONS_FD, &socket_fds[1]);
ssh_options_set(client_session, SSH_OPTIONS_HOST, "localhost");
ssh_options_set(client_session, SSH_OPTIONS_USER, "fuzz");
ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_C_S, cipher);
ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_S_C, cipher);
ssh_options_set(client_session, SSH_OPTIONS_HMAC_C_S, hmac);
ssh_options_set(client_session, SSH_OPTIONS_HMAC_S_C, hmac);
/* Set timeout for operations (1 second) */
long timeout = 1;
ssh_options_set(client_session, SSH_OPTIONS_TIMEOUT, &timeout);
if (ssh_connect(client_session) != SSH_OK) {
goto cleanup_thread;
}
if (ssh_userauth_none(client_session, NULL) != SSH_AUTH_SUCCESS) {
goto cleanup_thread;
}
scp = ssh_scp_new(client_session, SSH_SCP_READ, "/tmp/fuzz");
if (scp == NULL) {
goto cleanup_thread;
}
if (ssh_scp_init(scp) != SSH_OK) {
goto cleanup_thread;
}
if (size > 0) {
size_t copy_size = size < sizeof(buf) ? size : sizeof(buf);
memcpy(buf, data, copy_size);
}
/* Fuzz all SCP API functions in read mode */
ssh_scp_pull_request(scp);
ssh_scp_request_get_filename(scp);
ssh_scp_request_get_permissions(scp);
ssh_scp_request_get_size64(scp);
ssh_scp_request_get_size(scp);
ssh_scp_request_get_warning(scp);
ssh_scp_accept_request(scp);
ssh_scp_deny_request(scp, "Denied by fuzzer");
ssh_scp_read(scp, buf, sizeof(buf));
/* Final fuzz of scp pull request after all the calls */
ssh_scp_pull_request(scp);
/* Fuzz SCP in write/upload + recursive directory mode. */
scp_recursive = ssh_scp_new(client_session,
SSH_SCP_WRITE | SSH_SCP_RECURSIVE,
"/tmp/fuzz-recursive");
if (scp_recursive != NULL) {
if (ssh_scp_init(scp_recursive) == SSH_OK) {
ssh_scp_push_directory(scp_recursive, "fuzz-dir", 0755);
ssh_scp_push_file(scp_recursive, "fuzz-file", sizeof(buf), 0644);
ssh_scp_write(scp_recursive, buf, sizeof(buf));
ssh_scp_leave_directory(scp_recursive);
}
}
cleanup_thread:
pthread_join(srv_thread, NULL);
cleanup:
if (scp_recursive != NULL) {
ssh_scp_close(scp_recursive);
ssh_scp_free(scp_recursive);
}
if (scp) {
ssh_scp_close(scp);
ssh_scp_free(scp);
}
if (client_session) {
ssh_disconnect(client_session);
ssh_free(client_session);
}
if (socket_fds[0] >= 0)
close(socket_fds[0]);
if (socket_fds[1] >= 0)
close(socket_fds[1]);
return 0;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
assert(nalloc_start(data, size) > 0);
/* Test all cipher/HMAC combinations exhaustively */
const char *ciphers[] = {
"none",
"aes128-ctr",
"aes256-ctr",
"aes128-cbc",
};
const char *hmacs[] = {
"none",
"hmac-sha1",
"hmac-sha2-256",
};
int num_ciphers = sizeof(ciphers) / sizeof(ciphers[0]);
int num_hmacs = sizeof(hmacs) / sizeof(hmacs[0]);
for (int i = 0; i < num_ciphers; i++) {
for (int j = 0; j < num_hmacs; j++) {
test_scp_with_cipher(data, size, ciphers[i], hmacs[j]);
}
}
nalloc_end();
return 0;
}

View File

@@ -0,0 +1 @@
C0644 50 ../../../etc/passwd

View File

@@ -0,0 +1 @@
C0644 10 dir/file.txt

View File

@@ -0,0 +1 @@
C 100 test

View File

@@ -0,0 +1 @@
C0644 10 ..

View File

@@ -0,0 +1 @@
C0755 1024 executable.sh

View File

@@ -0,0 +1 @@
C0644 999999999999 huge.dat

View File

@@ -0,0 +1 @@
T1234567890 0 1234567890 0

View File

@@ -0,0 +1 @@
C0644 100 test.txt

View File

@@ -0,0 +1 @@
Warning: Test warning

View File

@@ -0,0 +1 @@
C0644 10 .

View File

@@ -0,0 +1 @@
Error: Test error

View File

@@ -0,0 +1 @@
Xunknown command

View File

@@ -0,0 +1 @@
C0644 test

View File

@@ -0,0 +1 @@
D0755 0 mydir

View File

@@ -0,0 +1 @@
C0644 abc test

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ssh_server_mock.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LIBSSH_STATIC 1
#include <libssh/callbacks.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
/* Fixed ed25519 key for all mock servers */
const char *ssh_mock_ed25519_key_pem =
"-----BEGIN OPENSSH PRIVATE KEY-----\n"
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n"
"QyNTUxOQAAACBpFO8/JfYlIqg6+vqx1vDKWDqxJHxw4tBqnQfiOjf2zAAAAJgbsYq1G7GK\n"
"tQAAAAtzc2gtZWQyNTUxOQAAACBpFO8/JfYlIqg6+vqx1vDKWDqxJHxw4tBqnQfiOjf2zA\n"
"AAAEAkGaLvQwKMbGVRk2M8cz7gqWvpBKuHkuekJxIBQrUJl2kU7z8l9iUiqDr6+rHW8MpY\n"
"OrEkfHDi0GqdB+I6N/bMAAAAEGZ1enotZWQyNTUxOS1rZXkBAgMEBQ==\n"
"-----END OPENSSH PRIVATE KEY-----\n";
/* Internal server session data */
struct mock_session_data {
ssh_channel channel;
struct ssh_mock_server_config *config;
};
/* Auth callback - always accepts "none" auth */
static int mock_auth_none(ssh_session session, const char *user, void *userdata)
{
(void)session;
(void)user;
(void)userdata;
return SSH_AUTH_SUCCESS;
}
/* Channel open callback */
static ssh_channel mock_channel_open(ssh_session session, void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
sdata->channel = ssh_channel_new(session);
return sdata->channel;
}
/* Exec request callback - for SCP */
static int mock_channel_exec(ssh_session session,
ssh_channel channel,
const char *command,
void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
(void)session;
(void)command;
if (sdata->config->exec_callback) {
return sdata->config->exec_callback(channel,
sdata->config->protocol_data,
sdata->config->protocol_data_size,
sdata->config->callback_userdata);
}
return SSH_OK;
}
/* Subsystem request callback - for SFTP */
static int mock_channel_subsystem(ssh_session session,
ssh_channel channel,
const char *subsystem,
void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
(void)session;
(void)subsystem;
if (sdata->config->subsystem_callback) {
return sdata->config->subsystem_callback(
channel,
sdata->config->protocol_data,
sdata->config->protocol_data_size,
sdata->config->callback_userdata);
}
return SSH_OK;
}
/* Server thread implementation */
static void *server_thread_func(void *arg)
{
struct ssh_mock_server_config *config =
(struct ssh_mock_server_config *)arg;
ssh_bind sshbind = NULL;
ssh_session session = NULL;
ssh_event event = NULL;
struct mock_session_data sdata = {0};
sdata.config = config;
struct ssh_server_callbacks_struct server_cb = {
.userdata = &sdata,
.auth_none_function = mock_auth_none,
.channel_open_request_session_function = mock_channel_open,
};
struct ssh_channel_callbacks_struct channel_cb = {
.userdata = &sdata,
.channel_exec_request_function = mock_channel_exec,
.channel_subsystem_request_function = mock_channel_subsystem,
};
bool no = false;
sshbind = ssh_bind_new();
if (sshbind == NULL) {
config->server_error = true;
return NULL;
}
session = ssh_new();
if (session == NULL) {
ssh_bind_free(sshbind);
config->server_error = true;
return NULL;
}
const char *cipher = config->cipher ? config->cipher : "aes128-ctr";
const char *hmac = config->hmac ? config->hmac : "hmac-sha1";
ssh_bind_options_set(sshbind,
SSH_BIND_OPTIONS_HOSTKEY,
SSH_MOCK_HOSTKEY_PATH);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_CIPHERS_C_S, cipher);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_CIPHERS_S_C, cipher);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HMAC_C_S, hmac);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HMAC_S_C, hmac);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_PROCESS_CONFIG, &no);
ssh_set_auth_methods(session, SSH_AUTH_METHOD_NONE);
ssh_callbacks_init(&server_cb);
ssh_set_server_callbacks(session, &server_cb);
if (ssh_bind_accept_fd(sshbind, session, config->server_socket) != SSH_OK) {
ssh_free(session);
ssh_bind_free(sshbind);
config->server_error = true;
return NULL;
}
config->server_ready = true;
event = ssh_event_new();
if (event == NULL) {
ssh_disconnect(session);
ssh_free(session);
ssh_bind_free(sshbind);
return NULL;
}
if (ssh_handle_key_exchange(session) == SSH_OK) {
ssh_event_add_session(event, session);
for (int i = 0; i < 50 && !sdata.channel; i++) {
ssh_event_dopoll(event, 1);
}
if (sdata.channel) {
ssh_callbacks_init(&channel_cb);
ssh_set_channel_callbacks(sdata.channel, &channel_cb);
int max_iterations = 30;
for (int iter = 0; iter < max_iterations &&
!ssh_channel_is_closed(sdata.channel) &&
!ssh_channel_is_eof(sdata.channel);
iter++) {
if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
break;
}
}
}
}
if (event)
ssh_event_free(event);
if (session) {
ssh_disconnect(session);
ssh_free(session);
}
if (sshbind)
ssh_bind_free(sshbind);
return NULL;
}
/* Public API - start mock SSH server */
int ssh_mock_server_start(struct ssh_mock_server_config *config,
pthread_t *thread)
{
if (!config || !thread)
return -1;
config->server_ready = false;
config->server_error = false;
if (pthread_create(thread, NULL, server_thread_func, config) != 0) {
return -1;
}
for (int i = 0; i < 50 && !config->server_ready && !config->server_error;
i++) {
usleep(100);
}
return config->server_error ? -1 : 0;
}
/* Generic protocol callback - sends raw fuzzer data for any protocol */
int ssh_mock_send_raw_data(void *channel,
const void *data,
size_t size,
void *userdata)
{
(void)userdata;
ssh_channel target_channel = (ssh_channel)channel;
/* Send raw fuzzer data - let protocol parser interpret it */
if (size > 0) {
ssh_channel_write(target_channel, data, size);
}
/* Close channel to signal completion */
ssh_channel_send_eof(target_channel);
ssh_channel_close(target_channel);
return SSH_OK;
}
/* Write fixed ed25519 host key to file */
int ssh_mock_write_hostkey(const char *path)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
return -1;
size_t len = strlen(ssh_mock_ed25519_key_pem);
size_t nwritten = fwrite(ssh_mock_ed25519_key_pem, 1, len, fp);
fclose(fp);
return (nwritten == len) ? 0 : -1;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef SSH_SERVER_MOCK_H
#define SSH_SERVER_MOCK_H
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
/* Server callback type */
typedef int (*ssh_mock_callback_fn)(void *channel,
const void *data,
size_t size,
void *userdata);
/* Mock server configuration */
struct ssh_mock_server_config {
const uint8_t *protocol_data;
size_t protocol_data_size;
ssh_mock_callback_fn exec_callback;
ssh_mock_callback_fn subsystem_callback;
void *callback_userdata;
const char *cipher;
const char *hmac;
int server_socket;
int client_socket;
bool server_ready;
bool server_error;
};
/* Public API functions */
int ssh_mock_server_start(struct ssh_mock_server_config *config,
pthread_t *thread);
int ssh_mock_send_raw_data(void *channel,
const void *data,
size_t size,
void *userdata);
int ssh_mock_write_hostkey(const char *path);
/* Fixed ed25519 key constant */
extern const char *ssh_mock_ed25519_key_pem;
/* Centralized hostkey path used by all mock servers */
#define SSH_MOCK_HOSTKEY_PATH "/tmp/libssh_mock_fuzz_key"
#endif