Attention is currently required from: srkyn.
Hello plaisthos, razvanc,
I'd like you to reexamine a change. Please visit
http://gerrit.openvpn.net/c/openvpn/+/1881?usp=email
to look at the new patch set (#3).
Change subject: compat: close all unrelated fds in forked helpers
......................................................................
compat: close all unrelated fds in forked helpers
The forked auth-pam, down-root, and port-share helpers only closed
descriptor numbers 3 through 100. Descriptors above that range could
remain open in long-lived helper processes, including helpers that
intentionally retain privilege.
Add a shared compatibility helper that uses close_range() when available
and falls back to the process descriptor limit. Preserve the helper
command socket and add coverage for both low and high preserved
descriptors.
Change-Id: Iacd56d245e9ab30cfb25b2b364ca661fa289dc55
Signed-off-by: David Sarkisyan <[email protected]>
---
M CMakeLists.txt
M config.h.cmake.in
M configure.ac
M src/compat/Makefile.am
A src/compat/close-fds.c
A src/compat/close-fds.h
M src/openvpn/ps.c
M src/plugins/auth-pam/Makefile.am
M src/plugins/auth-pam/auth-pam.c
M src/plugins/down-root/Makefile.am
M src/plugins/down-root/down-root.c
M tests/unit_tests/openvpn/Makefile.am
M tests/unit_tests/openvpn/test_misc.c
13 files changed, 285 insertions(+), 74 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/81/1881/3
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2c3324d..204cca1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,6 +19,7 @@
include(CheckLinkerFlag OPTIONAL)
include(CheckTypeSize)
include(CheckStructHasMember)
+include(CMakePushCheckState)
include(CTest)
option(UNSUPPORTED_BUILDS "Allow unsupported builds" OFF)
@@ -196,6 +197,10 @@
check_symbol_exists(fork unistd.h HAVE_FORK)
check_symbol_exists(execve unistd.h HAVE_EXECVE)
check_symbol_exists(ftruncate unistd.h HAVE_FTRUNCATE)
+ cmake_push_check_state()
+ list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_GNU_SOURCE)
+ check_symbol_exists(close_range unistd.h HAVE_CLOSE_RANGE)
+ cmake_pop_check_state()
check_symbol_exists(nice unistd.h HAVE_NICE)
check_symbol_exists(setgid unistd.h HAVE_SETGID)
check_symbol_exists(setuid unistd.h HAVE_SETUID)
@@ -424,6 +429,8 @@
src/compat/compat-gettimeofday.c
src/compat/compat-strsep.c
src/compat/compat-strtok_r.c
+ src/compat/close-fds.c
+ src/compat/close-fds.h
src/openvpn/argv.c
src/openvpn/argv.h
src/openvpn/base64.c
@@ -854,6 +861,11 @@
src/openvpn/siphash.c
src/openvpn/siphash_reference.c
)
+ if (NOT WIN32)
+ target_sources(test_misc PRIVATE
+ src/compat/close-fds.c
+ )
+ endif ()
target_sources(test_ncp PRIVATE
src/openvpn/crypto_epoch.c
diff --git a/config.h.cmake.in b/config.h.cmake.in
index c3bb5a5..c507787 100644
--- a/config.h.cmake.in
+++ b/config.h.cmake.in
@@ -75,6 +75,9 @@
/* Define to 1 if you have the `chsize' function. */
#cmakedefine HAVE_CHSIZE
+/* Define to 1 if you have the `close_range' function. */
+#cmakedefine HAVE_CLOSE_RANGE
+
/* struct cmsghdr needed for extended socket error support */
#cmakedefine HAVE_CMSGHDR
diff --git a/configure.ac b/configure.ac
index 469a475..baa185a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -567,7 +567,7 @@
syslog openlog mlockall getrlimit getgrnam setgid \
setgroups flock gettimeofday \
setsid chdir \
- chsize ftruncate execve getpeereid basename dirname \
+ chsize ftruncate execve getpeereid basename dirname close_range \
epoll_create strsep strtok_r \
])
diff --git a/src/compat/Makefile.am b/src/compat/Makefile.am
index 22867b3..12c3f95 100644
--- a/src/compat/Makefile.am
+++ b/src/compat/Makefile.am
@@ -16,9 +16,10 @@
libcompat_la_SOURCES = \
compat.h \
+ close-fds.c close-fds.h \
compat-dirname.c \
compat-basename.c \
compat-gettimeofday.c \
compat-daemon.c \
compat-strsep.c \
- compat-strtok_r.c
\ No newline at end of file
+ compat-strtok_r.c
diff --git a/src/compat/close-fds.c b/src/compat/close-fds.c
new file mode 100644
index 0000000..335b6e1
--- /dev/null
+++ b/src/compat/close-fds.c
@@ -0,0 +1,82 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2026 David Sarkisyan
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifndef _WIN32
+
+#include "close-fds.h"
+
+#include <limits.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <syslog.h>
+
+void
+close_fds_except(int keep)
+{
+ closelog();
+
+#ifdef HAVE_CLOSE_RANGE
+ bool ranges_closed = true;
+ if (keep >= 3)
+ {
+ if (keep > 3)
+ {
+ const unsigned int last = (unsigned int)keep - 1;
+ ranges_closed = close_range(3, last, 0) == 0;
+ }
+
+ const unsigned int first = (unsigned int)keep + 1;
+ if (close_range(first, ~0U, 0) != 0)
+ {
+ ranges_closed = false;
+ }
+ }
+ else
+ {
+ ranges_closed = close_range(3, ~0U, 0) == 0;
+ }
+ if (ranges_closed)
+ {
+ return;
+ }
+#endif
+
+ const long open_max = sysconf(_SC_OPEN_MAX);
+ int max_fd = 1024;
+ if (open_max > 0)
+ {
+ max_fd = open_max > INT_MAX ? INT_MAX : (int)open_max;
+ }
+ for (int fd = 3; fd < max_fd; ++fd)
+ {
+ if (fd != keep)
+ {
+ close(fd);
+ }
+ }
+}
+
+#endif /* !_WIN32 */
diff --git a/src/compat/close-fds.h b/src/compat/close-fds.h
new file mode 100644
index 0000000..35fe7de
--- /dev/null
+++ b/src/compat/close-fds.h
@@ -0,0 +1,38 @@
+/*
+ * OpenVPN -- An application to securely tunnel IP networks
+ * over a single UDP port, with support for SSL/TLS-based
+ * session authentication and key exchange,
+ * packet encryption, packet authentication, and
+ * packet compression.
+ *
+ * Copyright (C) 2026 David Sarkisyan
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef CLOSE_FDS_H
+#define CLOSE_FDS_H
+
+#ifndef _WIN32
+/**
+ * Close descriptors inherited across fork that are unrelated to a helper.
+ *
+ * Standard input, standard output, standard error, and @p keep remain open.
+ * All other file descriptors starting at 3 are closed.
+ *
+ * @param keep file descriptor used for communication with the parent process
+ */
+void close_fds_except(int keep);
+#endif
+
+#endif /* CLOSE_FDS_H */
diff --git a/src/openvpn/ps.c b/src/openvpn/ps.c
index be91a99..dbe5a1e 100644
--- a/src/openvpn/ps.c
+++ b/src/openvpn/ps.c
@@ -33,6 +33,7 @@
#include "fdmisc.h"
#include "crypto.h"
#include "ps.h"
+#include "close-fds.h"
#include "memdbg.h"
@@ -96,30 +97,6 @@
}
/*
- * Close most of parent's fds.
- * Keep stdin/stdout/stderr, plus one
- * other fd which is presumed to be
- * our pipe back to parent.
- * Admittedly, a bit of a kludge,
- * but posix doesn't give us a kind
- * of FD_CLOEXEC which will stop
- * fds from crossing a fork().
- */
-static void
-close_fds_except(int keep)
-{
- socket_descriptor_t i;
- closelog();
- for (i = 3; i <= 100; ++i)
- {
- if (i != keep)
- {
- openvpn_close_socket(i);
- }
- }
-}
-
-/*
* Usually we ignore signals, because our parent will
* deal with them.
*/
diff --git a/src/plugins/auth-pam/Makefile.am b/src/plugins/auth-pam/Makefile.am
index e6dc27e..fb7fd30 100644
--- a/src/plugins/auth-pam/Makefile.am
+++ b/src/plugins/auth-pam/Makefile.am
@@ -9,6 +9,7 @@
AM_CFLAGS = \
-I$(top_srcdir)/include \
+ -I$(top_srcdir)/src/compat \
$(PLUGIN_AUTH_PAM_CFLAGS) \
$(OPTIONAL_CRYPTO_CFLAGS)
@@ -23,6 +24,7 @@
pamdl.c pamdl.h \
auth-pam.exports
openvpn_plugin_auth_pam_la_LIBADD = \
+ $(top_builddir)/src/compat/libcompat.la \
$(PLUGIN_AUTH_PAM_LIBS)
openvpn_plugin_auth_pam_la_LDFLAGS = $(AM_LDFLAGS) \
-export-symbols "$(srcdir)/auth-pam.exports" \
diff --git a/src/plugins/auth-pam/auth-pam.c b/src/plugins/auth-pam/auth-pam.c
index 948b6af..04bb7d8 100644
--- a/src/plugins/auth-pam/auth-pam.c
+++ b/src/plugins/auth-pam/auth-pam.c
@@ -51,6 +51,8 @@
#include <arpa/inet.h>
#include <openvpn-plugin.h>
+#include "close-fds.h"
+
#define DEBUG(verb) ((verb) >= 4)
/* Command codes for foreground -> background communication */
@@ -240,30 +242,6 @@
#endif /* ifdef DO_DAEMONIZE */
/*
- * Close most of parent's fds.
- * Keep stdin/stdout/stderr, plus one
- * other fd which is presumed to be
- * our pipe back to parent.
- * Admittedly, a bit of a kludge,
- * but posix doesn't give us a kind
- * of FD_CLOEXEC which will stop
- * fds from crossing a fork().
- */
-static void
-close_fds_except(int keep)
-{
- int i;
- closelog();
- for (i = 3; i <= 100; ++i)
- {
- if (i != keep)
- {
- close(i);
- }
- }
-}
-
-/*
* Usually we ignore signals, because our parent will
* deal with them.
*/
diff --git a/src/plugins/down-root/Makefile.am
b/src/plugins/down-root/Makefile.am
index 7ca5a4e..83bc04f 100644
--- a/src/plugins/down-root/Makefile.am
+++ b/src/plugins/down-root/Makefile.am
@@ -9,6 +9,7 @@
AM_CFLAGS = \
-I$(top_srcdir)/include \
+ -I$(top_srcdir)/src/compat \
$(OPTIONAL_CRYPTO_CFLAGS)
if ENABLE_PLUGIN_DOWN_ROOT
@@ -19,6 +20,8 @@
openvpn_plugin_down_root_la_SOURCES = \
down-root.c \
down-root.exports
+openvpn_plugin_down_root_la_LIBADD = \
+ $(top_builddir)/src/compat/libcompat.la
openvpn_plugin_down_root_la_LDFLAGS = $(AM_LDFLAGS) \
-export-symbols "$(srcdir)/down-root.exports" \
-module -shared -avoid-version -no-undefined
diff --git a/src/plugins/down-root/down-root.c
b/src/plugins/down-root/down-root.c
index cd57189..f0ee7ea 100644
--- a/src/plugins/down-root/down-root.c
+++ b/src/plugins/down-root/down-root.c
@@ -44,6 +44,8 @@
#include <openvpn-plugin.h>
+#include "close-fds.h"
+
#define DEBUG(verb) ((verb) >= 7)
/* Command codes for foreground -> background communication */
@@ -191,30 +193,6 @@
}
/*
- * Close most of parent's fds.
- * Keep stdin/stdout/stderr, plus one
- * other fd which is presumed to be
- * our pipe back to parent.
- * Admittedly, a bit of a kludge,
- * but posix doesn't give us a kind
- * of FD_CLOEXEC which will stop
- * fds from crossing a fork().
- */
-static void
-close_fds_except(int keep)
-{
- int i;
- closelog();
- for (i = 3; i <= 100; ++i)
- {
- if (i != keep)
- {
- close(i);
- }
- }
-}
-
-/*
* Usually we ignore signals, because our parent will
* deal with them.
*/
diff --git a/tests/unit_tests/openvpn/Makefile.am
b/tests/unit_tests/openvpn/Makefile.am
index 6db611e..71726be 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -388,6 +388,10 @@
$(top_srcdir)/src/openvpn/siphash.c \
$(top_srcdir)/src/openvpn/siphash_reference.c
+if !WIN32
+misc_testdriver_SOURCES += $(top_srcdir)/src/compat/close-fds.c
+endif
+
push_update_msg_testdriver_CFLAGS = -I$(top_srcdir)/src/openvpn \
-I$(top_srcdir)/src/compat \
-I$(top_srcdir)/tests/unit_tests/openvpn \
diff --git a/tests/unit_tests/openvpn/test_misc.c
b/tests/unit_tests/openvpn/test_misc.c
index a41c27b..d747738 100644
--- a/tests/unit_tests/openvpn/test_misc.c
+++ b/tests/unit_tests/openvpn/test_misc.c
@@ -42,6 +42,14 @@
#include "crypto.h"
#ifdef _WIN32
#include "win32-util.h"
+#else
+#include "close-fds.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
#endif
#include "test_schedule.h"
@@ -475,9 +483,134 @@
}
#endif /* _WIN32 */
+#ifndef _WIN32
+static bool
+fd_is_closed(int fd)
+{
+ errno = 0;
+ return fcntl(fd, F_GETFD) == -1 && errno == EBADF;
+}
+
+static bool
+fd_is_open(int fd)
+{
+ return fcntl(fd, F_GETFD) != -1;
+}
+
+static int
+run_close_fds_case(int keep_target)
+{
+ int control[2];
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) < 0)
+ {
+ return EXIT_FAILURE;
+ }
+
+ const pid_t pid = fork();
+ if (pid < 0)
+ {
+ close(control[0]);
+ close(control[1]);
+ return EXIT_FAILURE;
+ }
+
+ if (pid == 0)
+ {
+ close(control[0]);
+
+ int keep = control[1];
+ if (keep_target >= 3 && keep_target != keep)
+ {
+ if (dup2(keep, keep_target) < 0)
+ {
+ _exit(EXIT_FAILURE);
+ }
+ close(keep);
+ keep = keep_target;
+ }
+
+ const int source = open("/dev/null", O_RDWR);
+ if (source < 0)
+ {
+ _exit(EXIT_FAILURE);
+ }
+ for (int fd = STDIN_FILENO; fd <= STDERR_FILENO; ++fd)
+ {
+ if (dup2(source, fd) < 0)
+ {
+ _exit(EXIT_FAILURE);
+ }
+ }
+ if (dup2(source, 100) < 0 || dup2(source, 200) < 0)
+ {
+ _exit(EXIT_FAILURE);
+ }
+ if (source > STDERR_FILENO && source != keep)
+ {
+ close(source);
+ }
+
+ close_fds_except(keep);
+
+ const char result = fd_is_open(STDIN_FILENO) &&
fd_is_open(STDOUT_FILENO)
+ && fd_is_open(STDERR_FILENO) &&
fd_is_closed(100)
+ && fd_is_closed(200)
+ ? '1'
+ : '0';
+ if (write(keep, &result, sizeof(result)) != sizeof(result))
+ {
+ _exit(EXIT_FAILURE);
+ }
+ close(keep);
+ _exit(result == '1' ? EXIT_SUCCESS : EXIT_FAILURE);
+ }
+
+ close(control[1]);
+ char result = '0';
+ const ssize_t size = read(control[0], &result, sizeof(result));
+ close(control[0]);
+
+ int status = 0;
+ if (waitpid(pid, &status, 0) != pid)
+ {
+ return EXIT_FAILURE;
+ }
+
+ return size == sizeof(result) && result == '1' && WIFEXITED(status)
+ && WEXITSTATUS(status) == EXIT_SUCCESS
+ ? EXIT_SUCCESS
+ : EXIT_FAILURE;
+}
+
+static void
+test_close_fds_default_keep(void **state)
+{
+ (void)state;
+ assert_int_equal(run_close_fds_case(-1), EXIT_SUCCESS);
+}
+
+static void
+test_close_fds_keep_three(void **state)
+{
+ (void)state;
+ assert_int_equal(run_close_fds_case(3), EXIT_SUCCESS);
+}
+
+static void
+test_close_fds_keep_middle(void **state)
+{
+ (void)state;
+ assert_int_equal(run_close_fds_case(150), EXIT_SUCCESS);
+}
+#endif /* !_WIN32 */
+
const struct CMUnitTest misc_tests[] = {
#ifdef _WIN32
cmocka_unit_test(test_win_path_in_dir),
+#else
+ cmocka_unit_test(test_close_fds_default_keep),
+ cmocka_unit_test(test_close_fds_keep_three),
+ cmocka_unit_test(test_close_fds_keep_middle),
#endif
cmocka_unit_test(test_compat_lzo_string),
cmocka_unit_test(test_auth_fail_temp_no_flags),
--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1881?usp=email
To unsubscribe, or for help writing mail filters, visit
http://gerrit.openvpn.net/settings?usp=email
Gerrit-MessageType: newpatchset
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: Iacd56d245e9ab30cfb25b2b364ca661fa289dc55
Gerrit-Change-Number: 1881
Gerrit-PatchSet: 3
Gerrit-Owner: srkyn <[email protected]>
Gerrit-Reviewer: plaisthos <[email protected]>
Gerrit-Reviewer: razvanc <[email protected]>
Gerrit-CC: cron2 <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: srkyn <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel