[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