[openssh-commits] [openssh] 04/15: OSS-Fuzz: Add fuzzer for sftp client

git+noreply at mindrot.org git+noreply at mindrot.org
Fri Oct 9 21:38:22 AEDT 2026


This is an automated email from the git hooks/post-receive script.

dtucker pushed a commit to branch master
in repository openssh.

commit e605578c2506ba9e2552f97e1ef6938690d93501
Author: Arthur Chan <arthur.chan at adalogics.com>
AuthorDate: Wed May 27 19:06:21 2026 +0100

    OSS-Fuzz: Add fuzzer for sftp client
    
    Signed-off-by: Arthur Chan <arthur.chan at adalogics.com>
---
 regress/misc/fuzz-harness/Makefile            |   6 +-
 regress/misc/fuzz-harness/sftp_client_fuzz.cc | 210 ++++++++++++++++++++++++++
 2 files changed, 215 insertions(+), 1 deletion(-)

diff --git a/regress/misc/fuzz-harness/Makefile b/regress/misc/fuzz-harness/Makefile
index 90132311c..97f95db24 100644
--- a/regress/misc/fuzz-harness/Makefile
+++ b/regress/misc/fuzz-harness/Makefile
@@ -15,7 +15,8 @@ COMMON_DEPS=../../../libssh.a $(COMMON_OBJS)
 TARGETS=pubkey_fuzz sig_fuzz authopt_fuzz authkeys_fuzz sshsig_fuzz \
 	sshsigopt_fuzz privkey_fuzz kex_fuzz agent_fuzz \
 	mkcorpus_sntrup761 sntrup761_enc_fuzz sntrup761_dec_fuzz \
-	ssh_client_session_fuzz sftp_server_msg_fuzz authfd_reply_fuzz
+	ssh_client_session_fuzz sftp_server_msg_fuzz authfd_reply_fuzz \
+	sftp_client_fuzz
 
 all: $(TARGETS)
 
@@ -67,6 +68,9 @@ sftp_server_msg_fuzz_helper.o: sftp_server_msg_fuzz_helper.c ../../../sftp-serve
 authfd_reply_fuzz: authfd_reply_fuzz.o fuzz_arena.o $(SK_NULL_OBJS) $(COMMON_DEPS)
 	$(CXX) -o $@ authfd_reply_fuzz.o fuzz_arena.o $(SK_NULL_OBJS) -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup $(LDFLAGS) $(FUZZ_FLAGS) $(LIBS)
 
+sftp_client_fuzz: sftp_client_fuzz.o fuzz_arena.o $(SK_NULL_OBJS) $(COMMON_DEPS)
+	$(CXX) -o $@ sftp_client_fuzz.o fuzz_arena.o $(SK_NULL_OBJS) ../../../sftp-client.o ../../../sftp-common.o -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup $(LDFLAGS) $(FUZZ_FLAGS) $(LIBS) -lz
+
 sk-dummy.o: ../sk-dummy/sk-dummy.c
 	$(CC) $(CFLAGS) -c -o $@ ../sk-dummy/sk-dummy.c -DSK_DUMMY_INTEGRATE=1 $(LDFLAGS) $(FUZZ_FLAGS)
 
diff --git a/regress/misc/fuzz-harness/sftp_client_fuzz.cc b/regress/misc/fuzz-harness/sftp_client_fuzz.cc
new file mode 100644
index 000000000..cd9d5d6b3
--- /dev/null
+++ b/regress/misc/fuzz-harness/sftp_client_fuzz.cc
@@ -0,0 +1,210 @@
+/* Copyright 2026 Google LLC
+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.
+*/
+
+/*
+ * Fuzzer for OpenSSH SFTP client response parser (sftp-client.c)
+ *
+ * Target: sftp-client.c
+ *
+ * Drives every public sftp-client API on a socketpair using the
+ * fuzzer input as the reply payload. A helper opens a fresh
+ * socketpair, pre-fills a valid FXP_VERSION reply so sftp_init
+ * succeeds, then writes the fuzzer body which the API call consumes
+ * as its own response. Each iteration runs all 12 APIs on the same
+ * input so no API is starved by a selector byte.
+ *
+ * Primary functions exercised:
+ * - sftp_init, sftp_stat, sftp_lstat, sftp_realpath, sftp_expand_path
+ * - sftp_readdir, sftp_statvfs, sftp_get_limits
+ * - sftp_rm, sftp_mkdir, sftp_rmdir, sftp_rename, sftp_symlink
+ * - decode_attrib via every reply path (sftp-common.c)
+ *
+ * Threat model: a malicious SFTP server returning crafted attribute
+ * lists, name lists, status responses, and statvfs payloads to a
+ * connected client (sshfs, sftp, scp to a hostile host).
+ *
+ * Failure handling: sftp-client.c calls fatal() liberally on
+ * protocol violations. A custom cleanup_exit() longjmp()s back to
+ * the helper. fuzz_arena (linker --wrap'd malloc family) frees
+ * every still-live allocation on both the success and longjmp paths
+ * so LSan sees zero per-iteration leaks.
+ */
+
+#include <setjmp.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+extern "C" {
+#include "includes.h"
+#include "sshbuf.h"
+#include "ssherr.h"
+#include "log.h"
+#include "sftp.h"
+#include "sftp-common.h"
+#include "sftp-client.h"
+
+void fuzz_arena_begin(void);
+void fuzz_arena_cleanup(void);
+}
+
+extern "C" volatile sig_atomic_t interrupted = 0;
+extern "C" int showprogress = 0;
+extern "C" void start_progress_meter(const char *file, off_t filesize,
+    off_t *ctr) { (void)file; (void)filesize; (void)ctr; }
+extern "C" void refresh_progress_meter(int force_update) { (void)force_update; }
+extern "C" void stop_progress_meter(void) { }
+
+static jmp_buf fuzz_env;
+static volatile int in_fuzz = 0;
+
+extern "C" void
+cleanup_exit(int code)
+{
+	if (in_fuzz)
+		longjmp(fuzz_env, code ? code : 1);
+	_exit(code);
+}
+
+static const uint8_t k_version_reply[] = {
+    0x00, 0x00, 0x00, 0x05,
+    0x02,
+    0x00, 0x00, 0x00, 0x03,
+};
+
+enum {
+	API_STAT,
+	API_LSTAT,
+	API_REALPATH,
+	API_EXPAND_PATH,
+	API_READDIR,
+	API_STATVFS,
+	API_GET_LIMITS,
+	API_RM,
+	API_MKDIR,
+	API_RMDIR,
+	API_RENAME,
+	API_SYMLINK,
+	API_COUNT,
+};
+
+static void
+run_one(int api, const uint8_t *body, size_t bsize)
+{
+	int sv[2] = { -1, -1 };
+	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0)
+		return;
+
+	ssize_t w = write(sv[0], k_version_reply, sizeof(k_version_reply));
+	(void)w;
+	if (bsize > 0)
+		w = write(sv[0], body, bsize);
+	(void)w;
+	shutdown(sv[0], SHUT_WR);
+
+	fuzz_arena_begin();
+	in_fuzz = 1;
+	if (setjmp(fuzz_env) == 0) {
+		struct sftp_conn *conn = sftp_init(sv[1], sv[1], 32768, 64, 0);
+		if (conn != NULL) {
+			Attrib attr;
+			attrib_clear(&attr);
+
+			switch (api) {
+			case API_STAT: {
+				Attrib a;
+				(void)sftp_stat(conn, "/fuzz", 1, &a);
+				break;
+			}
+			case API_LSTAT: {
+				Attrib a;
+				(void)sftp_lstat(conn, "/fuzz", 1, &a);
+				break;
+			}
+			case API_REALPATH: {
+				char *p = sftp_realpath(conn, "/fuzz");
+				free(p);
+				break;
+			}
+			case API_EXPAND_PATH: {
+				char *p = sftp_expand_path(conn, "~");
+				free(p);
+				break;
+			}
+			case API_READDIR: {
+				SFTP_DIRENT **dir = NULL;
+				(void)sftp_readdir(conn, "/fuzz", &dir);
+				if (dir != NULL)
+					sftp_free_dirents(dir);
+				break;
+			}
+			case API_STATVFS: {
+				struct sftp_statvfs vfs;
+				(void)sftp_statvfs(conn, "/fuzz", &vfs, 1);
+				break;
+			}
+			case API_GET_LIMITS: {
+				struct sftp_limits lim;
+				(void)sftp_get_limits(conn, &lim);
+				break;
+			}
+			case API_RM:
+				(void)sftp_rm(conn, "/fuzz");
+				break;
+			case API_MKDIR:
+				(void)sftp_mkdir(conn, "/fuzz", &attr, 0);
+				break;
+			case API_RMDIR:
+				(void)sftp_rmdir(conn, "/fuzz");
+				break;
+			case API_RENAME:
+				(void)sftp_rename(conn, "/a", "/b", 0);
+				break;
+			case API_SYMLINK:
+				(void)sftp_symlink(conn, "/a", "/b");
+				break;
+			}
+
+			sftp_free(conn);
+		}
+	}
+	in_fuzz = 0;
+	fuzz_arena_cleanup();
+
+	close(sv[0]);
+	close(sv[1]);
+}
+
+extern "C" int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+	static int initialized = 0;
+	if (!initialized) {
+		initialized = 1;
+		log_init("sftp_client_fuzz", SYSLOG_LEVEL_ERROR,
+		    SYSLOG_FACILITY_AUTH, 0);
+	}
+
+	if (size > 65536)
+		return 0;
+
+	for (int api = 0; api < API_COUNT; api++)
+		run_one(api, data, size);
+
+	return 0;
+}

-- 
To stop receiving notification emails like this one, please contact
djm at mindrot.org.


More information about the openssh-commits mailing list