[openssh-commits] [openssh] 02/15: OSS-Fuzz: Add new fuzzer for sftp server message handling

git+noreply at mindrot.org git+noreply at mindrot.org
Fri Oct 9 21:38:20 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 23ea1207d48003201071eb081cd803fa44171ac6
Author: Arthur Chan <arthur.chan at adalogics.com>
AuthorDate: Wed May 27 18:31:50 2026 +0100

    OSS-Fuzz: Add new fuzzer for sftp server message handling
    
    Signed-off-by: Arthur Chan <arthur.chan at adalogics.com>
---
 regress/misc/fuzz-harness/Makefile                 |   7 +-
 regress/misc/fuzz-harness/fuzz_arena.c             | 158 +++++++++++++++++
 regress/misc/fuzz-harness/sftp_server_msg_fuzz.cc  |  23 +++
 .../fuzz-harness/sftp_server_msg_fuzz_helper.c     | 193 +++++++++++++++++++++
 4 files changed, 380 insertions(+), 1 deletion(-)

diff --git a/regress/misc/fuzz-harness/Makefile b/regress/misc/fuzz-harness/Makefile
index 458f18f04..d8b86d4a9 100644
--- a/regress/misc/fuzz-harness/Makefile
+++ b/regress/misc/fuzz-harness/Makefile
@@ -15,7 +15,7 @@ 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
+	ssh_client_session_fuzz sftp_server_msg_fuzz
 
 all: $(TARGETS)
 
@@ -59,6 +59,11 @@ ssh_client_session_fuzz: ssh_client_session_fuzz.o ssh_client_session_fuzz_helpe
 
 ssh_client_session_fuzz_helper.o: ssh_client_session_fuzz_helper.c ../../../sshconnect2.c
 
+sftp_server_msg_fuzz: sftp_server_msg_fuzz.o sftp_server_msg_fuzz_helper.o fuzz_arena.o $(SK_NULL_OBJS) $(COMMON_DEPS)
+	$(CXX) -o $@ sftp_server_msg_fuzz.o sftp_server_msg_fuzz_helper.o fuzz_arena.o $(SK_NULL_OBJS) ../../../sftp-common.o -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup $(LDFLAGS) $(FUZZ_FLAGS) $(LIBS) -lz
+
+sftp_server_msg_fuzz_helper.o: sftp_server_msg_fuzz_helper.c ../../../sftp-server.c
+
 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/fuzz_arena.c b/regress/misc/fuzz-harness/fuzz_arena.c
new file mode 100644
index 000000000..8178f1659
--- /dev/null
+++ b/regress/misc/fuzz-harness/fuzz_arena.c
@@ -0,0 +1,158 @@
+/* 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.
+*/
+
+/*
+ * Per-iteration allocation arena for fuzz harnesses.
+ *
+ * Intercepts malloc / calloc / realloc / free / strdup via linker
+ * --wrap and tracks every live allocation made between
+ * fuzz_arena_begin() and the next release or cleanup call.
+ *
+ * Exit semantics:
+ * - fuzz_arena_release(): stop tracking, leave allocations in place.
+ *   Use when the code under test owns its allocations across iterations.
+ * - fuzz_arena_cleanup(): free every still-tracked allocation. Use on
+ *   longjmp recovery or when the code under test is treated as a pure
+ *   per-iteration scratch user.
+ *
+ * Tracking is disabled outside of a begin/release-or-cleanup window
+ * so libFuzzer's own bookkeeping allocations are not touched. The
+ * implementation is single-threaded.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern void *__real_malloc(size_t);
+extern void *__real_calloc(size_t, size_t);
+extern void *__real_realloc(void *, size_t);
+extern void __real_free(void *);
+extern char *__real_strdup(const char *);
+
+struct arena_node {
+	void *ptr;
+	struct arena_node *next;
+};
+
+static struct arena_node *arena_head;
+static int tracking;
+
+static inline void
+arena_track(void *p)
+{
+	struct arena_node *n;
+	if (!tracking || p == NULL)
+		return;
+	n = (struct arena_node *)__real_malloc(sizeof(*n));
+	if (n == NULL)
+		return;
+	n->ptr = p;
+	n->next = arena_head;
+	arena_head = n;
+}
+
+static inline void
+arena_untrack(void *p)
+{
+	struct arena_node **pp;
+	if (p == NULL)
+		return;
+	for (pp = &arena_head; *pp != NULL; pp = &(*pp)->next) {
+		if ((*pp)->ptr == p) {
+			struct arena_node *n = *pp;
+			*pp = n->next;
+			__real_free(n);
+			return;
+		}
+	}
+}
+
+void *
+__wrap_malloc(size_t size)
+{
+	void *p = __real_malloc(size);
+	arena_track(p);
+	return p;
+}
+
+void *
+__wrap_calloc(size_t nmemb, size_t size)
+{
+	void *p = __real_calloc(nmemb, size);
+	arena_track(p);
+	return p;
+}
+
+void *
+__wrap_realloc(void *ptr, size_t size)
+{
+	arena_untrack(ptr);
+	void *p = __real_realloc(ptr, size);
+	arena_track(p);
+	return p;
+}
+
+void
+__wrap_free(void *ptr)
+{
+	arena_untrack(ptr);
+	__real_free(ptr);
+}
+
+char *
+__wrap_strdup(const char *s)
+{
+	size_t len;
+	char *p;
+	if (s == NULL)
+		return NULL;
+	len = strlen(s) + 1;
+	p = (char *)__real_malloc(len);
+	if (p == NULL)
+		return NULL;
+	memcpy(p, s, len);
+	arena_track(p);
+	return p;
+}
+
+void
+fuzz_arena_begin(void)
+{
+	tracking = 1;
+}
+
+void
+fuzz_arena_release(void)
+{
+	struct arena_node *n;
+	tracking = 0;
+	while (arena_head != NULL) {
+		n = arena_head;
+		arena_head = n->next;
+		__real_free(n);
+	}
+}
+
+void
+fuzz_arena_cleanup(void)
+{
+	struct arena_node *n;
+	tracking = 0;
+	while (arena_head != NULL) {
+		n = arena_head;
+		arena_head = n->next;
+		__real_free(n->ptr);
+		__real_free(n);
+	}
+}
diff --git a/regress/misc/fuzz-harness/sftp_server_msg_fuzz.cc b/regress/misc/fuzz-harness/sftp_server_msg_fuzz.cc
new file mode 100644
index 000000000..b12a0c0fc
--- /dev/null
+++ b/regress/misc/fuzz-harness/sftp_server_msg_fuzz.cc
@@ -0,0 +1,23 @@
+/* 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.
+*/
+
+#include <stddef.h>
+#include <stdint.h>
+
+extern "C" void fuzz_server_one(const uint8_t *data, size_t size);
+
+extern "C" int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+	fuzz_server_one(data, size);
+	return 0;
+}
diff --git a/regress/misc/fuzz-harness/sftp_server_msg_fuzz_helper.c b/regress/misc/fuzz-harness/sftp_server_msg_fuzz_helper.c
new file mode 100644
index 000000000..6780209b2
--- /dev/null
+++ b/regress/misc/fuzz-harness/sftp_server_msg_fuzz_helper.c
@@ -0,0 +1,193 @@
+/* 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 server message dispatch (sftp-server.c)
+ *
+ * Target: sftp-server.c
+ *
+ * Follows the agent_fuzz_helper.c pattern: #include's sftp-server.c so
+ * process() and the static process_* handlers, plus the file-scope
+ * globals (iqueue, oqueue, pw, init_done, handles), are reachable from
+ * this translation unit. Each iteration bootstraps an FXP_INIT then
+ * feeds the fuzzer input as a single framed SFTP message.
+ *
+ * Primary functions exercised:
+ * - process() and the 17 base process_* handlers (open / close / read /
+ *   write / stat / lstat / fstat / setstat / fsetstat / opendir /
+ *   readdir / remove / mkdir / rmdir / realpath / rename / readlink /
+ *   symlink)
+ * - process_extended_* (11 variants including posix-rename, statvfs,
+ *   hardlink, fsync, lsetstat, limits, expand-path, copy-data,
+ *   home-directory, users-groups-by-id)
+ * - decode_attrib / encode_attrib (sftp-common.c)
+ *
+ * Failure handling: fatal() -> cleanup_exit() and
+ * sftp_server_cleanup_exit() are intercepted with overriding
+ * definitions that longjmp() back to the entry point. fuzz_arena
+ * (linker --wrap'd malloc family) frees every still-live allocation on
+ * recovery so LSan sees zero per-iteration leaks.
+ */
+
+#include <setjmp.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <sys/types.h>
+
+#include "log.h"
+
+extern void fuzz_arena_begin(void);
+extern void fuzz_arena_release(void);
+extern void fuzz_arena_cleanup(void);
+
+static jmp_buf fuzz_server_env;
+static volatile int in_fuzz_server = 0;
+
+void
+cleanup_exit(int code)
+{
+	if (in_fuzz_server)
+		longjmp(fuzz_server_env, code ? code : 1);
+	_exit(code);
+}
+
+#define sftp_server_cleanup_exit __orig_sftp_server_cleanup_exit
+#include "../../../sftp-server.c"
+#undef sftp_server_cleanup_exit
+
+void
+sftp_server_cleanup_exit(int code)
+{
+	if (in_fuzz_server)
+		longjmp(fuzz_server_env, code ? code : 1);
+	_exit(code);
+}
+
+static void (* const __force_use_renamed)(int) =
+    __orig_sftp_server_cleanup_exit;
+
+static struct passwd fuzz_pw;
+static int log_initialized = 0;
+
+static void
+cleanup_handles(void)
+{
+	u_int i;
+	if (handles == NULL)
+		return;
+	for (i = 0; i < num_handles; i++) {
+		if (handles[i].use == HANDLE_FILE && handles[i].fd >= 0)
+			close(handles[i].fd);
+		else if (handles[i].use == HANDLE_DIR && handles[i].dirp != NULL)
+			closedir(handles[i].dirp);
+		free(handles[i].name);
+	}
+	free(handles);
+	handles = NULL;
+	num_handles = 0;
+	first_unused_handle = -1;
+}
+
+static void
+reset_module_globals(void)
+{
+	pw = &fuzz_pw;
+	client_addr = NULL;
+	version = 0;
+	init_done = 0;
+	readonly = 0;
+	request_allowlist = NULL;
+	request_denylist = NULL;
+	handles = NULL;
+	num_handles = 0;
+	first_unused_handle = -1;
+	iqueue = NULL;
+	oqueue = NULL;
+}
+
+void
+fuzz_server_one(const uint8_t *data, size_t size)
+{
+	if (!log_initialized) {
+		log_initialized = 1;
+		log_init("sftp_server_msg_fuzz", SYSLOG_LEVEL_ERROR,
+		    SYSLOG_FACILITY_AUTH, 0);
+
+		/* Confine filesystem syscalls (process_open/mkdir/symlink) to a fresh tmpdir. */
+		char tmpdir[] = "/tmp/sftp_fuzz_XXXXXX";
+		if (mkdtemp(tmpdir) != NULL && chdir(tmpdir) != 0)
+			_exit(1);
+
+		memset(&fuzz_pw, 0, sizeof(fuzz_pw));
+		fuzz_pw.pw_name = (char *)"fuzz";
+		fuzz_pw.pw_uid = getuid();
+		fuzz_pw.pw_gid = getgid();
+		fuzz_pw.pw_dir = (char *)"/tmp";
+		fuzz_pw.pw_shell = (char *)"/bin/sh";
+	}
+
+	if (size == 0 || size > 65000)
+		return;
+
+	reset_module_globals();
+
+	fuzz_arena_begin();
+	in_fuzz_server = 1;
+	if (setjmp(fuzz_server_env) == 0) {
+		client_addr = strdup("127.0.0.1");
+		iqueue = sshbuf_new();
+		oqueue = sshbuf_new();
+		if (iqueue == NULL || oqueue == NULL)
+			goto out;
+
+		const uint8_t init_msg[] = {
+		    0x00, 0x00, 0x00, 0x05,
+		    SSH2_FXP_INIT,
+		    0x00, 0x00, 0x00, 0x03,
+		};
+		if (sshbuf_put(iqueue, init_msg, sizeof(init_msg)) != 0)
+			goto out;
+		process();
+
+		sshbuf_reset(iqueue);
+		sshbuf_reset(oqueue);
+
+		uint8_t hdr[4] = {
+		    (uint8_t)((size >> 24) & 0xff),
+		    (uint8_t)((size >> 16) & 0xff),
+		    (uint8_t)((size >> 8) & 0xff),
+		    (uint8_t)(size & 0xff),
+		};
+		if (sshbuf_put(iqueue, hdr, 4) != 0 ||
+		    sshbuf_put(iqueue, data, size) != 0)
+			goto out;
+		process();
+	out:
+		cleanup_handles();
+		sshbuf_free(iqueue);
+		sshbuf_free(oqueue);
+		free(client_addr);
+		iqueue = oqueue = NULL;
+		client_addr = NULL;
+		in_fuzz_server = 0;
+		fuzz_arena_release();
+		return;
+	}
+	cleanup_handles();
+	in_fuzz_server = 0;
+	fuzz_arena_cleanup();
+	iqueue = oqueue = NULL;
+	client_addr = NULL;
+}

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


More information about the openssh-commits mailing list