new file mode 100644
@@ -0,0 +1,286 @@
+From 5858a988942d2e25985b34b8c40ce2792cbbe853 Mon Sep 17 00:00:00 2001
+From: Jakub Jelen <jjelen@redhat.com>
+Date: Thu, 11 Dec 2025 17:33:19 +0100
+Subject: [PATCH 4/4] CVE-2026-0965 config: Do not attempt to read non-regular
+ and too large configuration files
+
+Changes also the reading of known_hosts to use the new helper function
+
+CVE: CVE-2026-0965
+Upstream-Status: Backport [https://git.libssh.org/projects/libssh.git/commit/?id=bf390a042623e02abc8f421c4c5fadc0429a8a76]
+
+Signed-off-by: Jakub Jelen <jjelen@redhat.com>
+Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
+(cherry picked from commit a5eb30dbfd8f3526b2d04bd9f0a3803b665f5798)
+(cherry picked from commit bf390a042623e02abc8f421c4c5fadc0429a8a76)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ include/libssh/misc.h | 3 ++
+ include/libssh/priv.h | 3 ++
+ src/bind_config.c | 4 +-
+ src/config.c | 8 ++--
+ src/dh-gex.c | 4 +-
+ src/known_hosts.c | 2 +-
+ src/knownhosts.c | 2 +-
+ src/misc.c | 74 ++++++++++++++++++++++++++++++++
+ tests/unittests/torture_config.c | 20 +++++++++
+ 9 files changed, 110 insertions(+), 10 deletions(-)
+
+diff --git a/include/libssh/misc.h b/include/libssh/misc.h
+index ab726a0e..8eab94ee 100644
+--- a/include/libssh/misc.h
++++ b/include/libssh/misc.h
+@@ -36,6 +36,7 @@
+ #include <sys/types.h>
+ #include <stdbool.h>
+ #endif /* _WIN32 */
++#include <stdio.h>
+
+ #ifdef __cplusplus
+ extern "C" {
+@@ -136,6 +137,8 @@ int ssh_check_username_syntax(const char *username);
+ void ssh_proxyjumps_free(struct ssh_list *proxy_jump_list);
+ bool ssh_libssh_proxy_jumps(void);
+
++FILE *ssh_strict_fopen(const char *filename, size_t max_file_size);
++
+ #ifdef __cplusplus
+ }
+ #endif
+diff --git a/include/libssh/priv.h b/include/libssh/priv.h
+index 35fd8506..62069970 100644
+--- a/include/libssh/priv.h
++++ b/include/libssh/priv.h
+@@ -473,6 +473,9 @@ char *ssh_strerror(int err_num, char *buf, size_t buflen);
+ #define SSH_TTY_MODES_MAX_BUFSIZE (55 * 5 + 1)
+ int encode_current_tty_opts(unsigned char *buf, size_t buflen);
+
++/** The default maximum file size for a configuration file */
++#define SSH_MAX_CONFIG_FILE_SIZE 16 * 1024 * 1024
++
+ #ifdef __cplusplus
+ }
+ #endif
+diff --git a/src/bind_config.c b/src/bind_config.c
+index 9e4a7fd4..c12f1003 100644
+--- a/src/bind_config.c
++++ b/src/bind_config.c
+@@ -212,7 +212,7 @@ local_parse_file(ssh_bind bind,
+ return;
+ }
+
+- f = fopen(filename, "r");
++ f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
+ if (f == NULL) {
+ SSH_LOG(SSH_LOG_RARE, "Cannot find file %s to load",
+ filename);
+@@ -636,7 +636,7 @@ int ssh_bind_config_parse_file(ssh_bind bind, const char *filename)
+ * option to be redefined later by another file. */
+ uint8_t seen[BIND_CFG_MAX] = {0};
+
+- f = fopen(filename, "r");
++ f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
+ if (f == NULL) {
+ return 0;
+ }
+diff --git a/src/config.c b/src/config.c
+index b4171efd..1ffad537 100644
+--- a/src/config.c
++++ b/src/config.c
+@@ -223,10 +223,9 @@ local_parse_file(ssh_session session,
+ return;
+ }
+
+- f = fopen(filename, "r");
++ f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
+ if (f == NULL) {
+- SSH_LOG(SSH_LOG_RARE, "Cannot find file %s to load",
+- filename);
++ /* The underlying function logs the reasons */
+ return;
+ }
+
+@@ -1466,8 +1465,9 @@ int ssh_config_parse_file(ssh_session session, const char *filename)
+ int parsing, rv;
+ bool global = 0;
+
+- f = fopen(filename, "r");
++ f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
+ if (f == NULL) {
++ /* The underlying function logs the reasons */
+ return 0;
+ }
+
+diff --git a/src/dh-gex.c b/src/dh-gex.c
+index 46ba934e..428a5655 100644
+--- a/src/dh-gex.c
++++ b/src/dh-gex.c
+@@ -519,9 +519,9 @@ static int ssh_retrieve_dhgroup(char *moduli_file,
+ }
+
+ if (moduli_file != NULL)
+- moduli = fopen(moduli_file, "r");
++ moduli = ssh_strict_fopen(moduli_file, SSH_MAX_CONFIG_FILE_SIZE);
+ else
+- moduli = fopen(MODULI_FILE, "r");
++ moduli = ssh_strict_fopen(MODULI_FILE, SSH_MAX_CONFIG_FILE_SIZE);
+
+ if (moduli == NULL) {
+ char err_msg[SSH_ERRNO_MSG_MAX] = {0};
+diff --git a/src/known_hosts.c b/src/known_hosts.c
+index 3ef83e21..701576ce 100644
+--- a/src/known_hosts.c
++++ b/src/known_hosts.c
+@@ -83,7 +83,7 @@ static struct ssh_tokens_st *ssh_get_knownhost_line(FILE **file,
+ struct ssh_tokens_st *tokens = NULL;
+
+ if (*file == NULL) {
+- *file = fopen(filename,"r");
++ *file = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
+ if (*file == NULL) {
+ return NULL;
+ }
+diff --git a/src/knownhosts.c b/src/knownhosts.c
+index a2d08a75..3ab468de 100644
+--- a/src/knownhosts.c
++++ b/src/knownhosts.c
+@@ -232,7 +232,7 @@ static int ssh_known_hosts_read_entries(const char *match,
+ FILE *fp = NULL;
+ int rc;
+
+- fp = fopen(filename, "r");
++ fp = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
+ if (fp == NULL) {
+ char err_msg[SSH_ERRNO_MSG_MAX] = {0};
+ SSH_LOG(SSH_LOG_TRACE, "Failed to open the known_hosts file '%s': %s",
+diff --git a/src/misc.c b/src/misc.c
+index 774211fb..3968e6bc 100644
+--- a/src/misc.c
++++ b/src/misc.c
+@@ -37,6 +37,7 @@
+ #endif /* _WIN32 */
+
+ #include <errno.h>
++#include <fcntl.h>
+ #include <limits.h>
+ #include <stdio.h>
+ #include <string.h>
+@@ -2244,4 +2245,77 @@ ssh_libssh_proxy_jumps(void)
+ return !(t != NULL && t[0] == '1');
+ }
+
++/**
++ * @internal
++ *
++ * @brief Safely open a file containing some configuration.
++ *
++ * Runs checks if the file can be used as some configuration file (is regular
++ * file and is not too large). If so, returns the opened file (for reading).
++ * Otherwise logs error and returns `NULL`.
++ *
++ * @param filename The path to the file to open.
++ * @param max_file_size Maximum file size that is accepted.
++ *
++ * @returns the opened file or `NULL` on error.
++ */
++FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
++{
++ FILE *f = NULL;
++ struct stat sb;
++ char err_msg[SSH_ERRNO_MSG_MAX] = {0};
++ int r, fd;
++
++ /* open first to avoid TOCTOU */
++ fd = open(filename, O_RDONLY);
++ if (fd == -1) {
++ SSH_LOG(SSH_LOG_RARE,
++ "Failed to open a file %s for reading: %s",
++ filename,
++ ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
++ return NULL;
++ }
++
++ /* Check the file is sensible for a configuration file */
++ r = fstat(fd, &sb);
++ if (r != 0) {
++ SSH_LOG(SSH_LOG_RARE,
++ "Failed to stat %s: %s",
++ filename,
++ ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
++ close(fd);
++ return NULL;
++ }
++ if ((sb.st_mode & S_IFMT) != S_IFREG) {
++ SSH_LOG(SSH_LOG_RARE,
++ "The file %s is not a regular file: skipping",
++ filename);
++ close(fd);
++ return NULL;
++ }
++
++ if ((size_t)sb.st_size > max_file_size) {
++ SSH_LOG(SSH_LOG_RARE,
++ "The file %s is too large (%jd MB > %zu MB): skipping",
++ filename,
++ (intmax_t)sb.st_size / 1024 / 1024,
++ max_file_size / 1024 / 1024);
++ close(fd);
++ return NULL;
++ }
++
++ f = fdopen(fd, "r");
++ if (f == NULL) {
++ SSH_LOG(SSH_LOG_RARE,
++ "Failed to open a file %s for reading: %s",
++ filename,
++ ssh_strerror(r, err_msg, SSH_ERRNO_MSG_MAX));
++ close(fd);
++ return NULL;
++ }
++
++ /* the flcose() will close also the underlying fd */
++ return f;
++}
++
+ /** @} */
+diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c
+index fcfe8fbc..0cb31a76 100644
+--- a/tests/unittests/torture_config.c
++++ b/tests/unittests/torture_config.c
+@@ -2675,6 +2675,23 @@ static void torture_config_match_complex(void **state)
+ ssh_string_free_char(v);
+ }
+
++/* Invalid configuration files
++ */
++static void torture_config_invalid(void **state)
++{
++ ssh_session session = *state;
++
++ ssh_options_set(session, SSH_OPTIONS_HOST, "Bar");
++
++ /* non-regular file -- ignored (or missing on non-unix) so OK */
++ _parse_config(session, "/dev/random", NULL, SSH_OK);
++
++#ifndef _WIN32
++ /* huge file -- ignored (or missing on non-unix) so OK */
++ _parse_config(session, "/proc/kcore", NULL, SSH_OK);
++#endif
++}
++
+ int torture_run_tests(void)
+ {
+ int rc;
+@@ -2771,6 +2788,9 @@ int torture_run_tests(void)
+ setup, teardown),
+ cmocka_unit_test_setup_teardown(torture_config_match_complex,
+ setup, teardown),
++ cmocka_unit_test_setup_teardown(torture_config_invalid,
++ setup,
++ teardown),
+ };
+
+
+--
+2.51.0
+
@@ -14,6 +14,7 @@ SRC_URI = "git://git.libssh.org/projects/libssh.git;protocol=https;branch=stable
file://CVE-2026-0968_p1.patch \
file://CVE-2026-0968_p2.patch \
file://CVE-2026-0967.patch \
+ file://CVE-2026-0965.patch \
"
SRC_URI:append:toolchain-clang = " file://0001-CompilerChecks.cmake-drop-Wunused-variable-flag.patch"