You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
postgres/src/test/modules/test_cloexec/test_cloexec.c

263 lines
6.1 KiB

Fix O_CLOEXEC flag handling in Windows port. PostgreSQL's src/port/open.c has always set bInheritHandle = TRUE when opening files on Windows, making all file descriptors inheritable by child processes. This meant the O_CLOEXEC flag, added to many call sites by commit 1da569ca1f (v16), was silently ignored. The original commit included a comment suggesting that our open() replacement doesn't create inheritable handles, but it was a mis- understanding of the code path. In practice, the code was creating inheritable handles in all cases. This hasn't caused widespread problems because most child processes (archive_command, COPY PROGRAM, etc.) operate on file paths passed as arguments rather than inherited file descriptors. Even if a child wanted to use an inherited handle, it would need to learn the numeric handle value, which isn't passed through our IPC mechanisms. Nonetheless, the current behavior is wrong. It violates documented O_CLOEXEC semantics, contradicts our own code comments, and makes PostgreSQL behave differently on Windows than on Unix. It also creates potential issues with future code or security auditing tools. To fix, define O_CLOEXEC to _O_NOINHERIT in master, previously used by O_DSYNC. We use different values in the back branches to preserve existing values. In pgwin32_open_handle() we set bInheritHandle according to whether O_CLOEXEC is specified, for the same atomic semantics as POSIX in multi-threaded programs that create processes. Backpatch-through: 16 Author: Bryan Green <dbryan.green@gmail.com> Co-authored-by: Thomas Munro <thomas.munro@gmail.com> (minor adjustments) Discussion: https://postgr.es/m/e2b16375-7430-4053-bda3-5d2194ff1880%40gmail.com
1 week ago
/*-------------------------------------------------------------------------
*
* test_cloexec.c
* Test O_CLOEXEC flag handling on Windows
*
* This program tests that:
* 1. File handles opened with O_CLOEXEC are NOT inherited by child processes
* 2. File handles opened without O_CLOEXEC ARE inherited by child processes
*
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include <fcntl.h>
#include <sys/stat.h>
#ifdef WIN32
#include <windows.h>
#endif
#include "common/file_utils.h"
#include "port.h"
static void run_parent_tests(const char *testfile1, const char *testfile2);
static void run_child_tests(const char *handle1_str, const char *handle2_str);
static bool try_write_to_handle(HANDLE h, const char *label);
int
main(int argc, char *argv[])
{
char testfile1[MAXPGPATH];
char testfile2[MAXPGPATH];
/* Windows-only test */
#ifndef WIN32
fprintf(stderr, "This test only runs on Windows\n");
return 0;
#endif
if (argc == 3)
{
/*
* Child mode: receives two handle values as hex strings and attempts
* to write to them.
*/
run_child_tests(argv[1], argv[2]);
return 0;
}
else if (argc == 1)
{
/* Parent mode: opens files and spawns child */
snprintf(testfile1, sizeof(testfile1), "test_cloexec_1_%d.tmp", (int) getpid());
snprintf(testfile2, sizeof(testfile2), "test_cloexec_2_%d.tmp", (int) getpid());
run_parent_tests(testfile1, testfile2);
/* Clean up test files */
unlink(testfile1);
unlink(testfile2);
return 0;
}
else
{
fprintf(stderr, "Usage: %s [handle1_hex handle2_hex]\n", argv[0]);
return 1;
}
}
static void
run_parent_tests(const char *testfile1, const char *testfile2)
{
#ifdef WIN32
int fd1,
fd2;
HANDLE h1,
h2;
char cmdline[1024];
STARTUPINFO si;
PROCESS_INFORMATION pi;
DWORD exit_code;
printf("Parent: Opening test files...\n");
/*
* Open first file WITH O_CLOEXEC - should NOT be inherited
*/
fd1 = open(testfile1, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
if (fd1 < 0)
{
fprintf(stderr, "Failed to open %s: %s\n", testfile1, strerror(errno));
exit(1);
}
/*
* Open second file WITHOUT O_CLOEXEC - should be inherited
*/
fd2 = open(testfile2, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (fd2 < 0)
{
fprintf(stderr, "Failed to open %s: %s\n", testfile2, strerror(errno));
close(fd1);
exit(1);
}
/* Get Windows HANDLEs from file descriptors */
h1 = (HANDLE) _get_osfhandle(fd1);
h2 = (HANDLE) _get_osfhandle(fd2);
if (h1 == INVALID_HANDLE_VALUE || h2 == INVALID_HANDLE_VALUE)
{
fprintf(stderr, "Failed to get OS handles\n");
close(fd1);
close(fd2);
exit(1);
}
printf("Parent: fd1=%d (O_CLOEXEC) -> HANDLE=%p\n", fd1, h1);
printf("Parent: fd2=%d (no O_CLOEXEC) -> HANDLE=%p\n", fd2, h2);
/*
* Spawn child process with bInheritHandles=TRUE, passing handle values as
* hex strings
*/
snprintf(cmdline, sizeof(cmdline), "\"%s\" %p %p",
GetCommandLine(), h1, h2);
/*
* Find the actual executable path by removing any arguments from
* GetCommandLine().
*/
{
char exe_path[MAX_PATH];
char *space_pos;
GetModuleFileName(NULL, exe_path, sizeof(exe_path));
snprintf(cmdline, sizeof(cmdline), "\"%s\" %p %p",
exe_path, h1, h2);
}
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(pi));
printf("Parent: Spawning child process...\n");
printf("Parent: Command line: %s\n", cmdline);
if (!CreateProcess(NULL, /* application name */
cmdline, /* command line */
NULL, /* process security attributes */
NULL, /* thread security attributes */
TRUE, /* bInheritHandles - CRITICAL! */
0, /* creation flags */
NULL, /* environment */
NULL, /* current directory */
&si, /* startup info */
&pi)) /* process information */
{
fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError());
close(fd1);
close(fd2);
exit(1);
}
printf("Parent: Waiting for child process...\n");
/* Wait for child to complete */
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
close(fd1);
close(fd2);
printf("Parent: Child exit code: %lu\n", exit_code);
if (exit_code == 0)
printf("Parent: SUCCESS - O_CLOEXEC behavior verified\n");
else
{
printf("Parent: FAILURE - O_CLOEXEC not working correctly\n");
exit(1);
}
#endif
}
static void
run_child_tests(const char *handle1_str, const char *handle2_str)
{
#ifdef WIN32
HANDLE h1,
h2;
bool h1_worked,
h2_worked;
/* Parse handle values from hex strings */
if (sscanf(handle1_str, "%p", &h1) != 1 ||
sscanf(handle2_str, "%p", &h2) != 1)
{
fprintf(stderr, "Child: Failed to parse handle values\n");
exit(1);
}
printf("Child: Received HANDLE1=%p (should fail - O_CLOEXEC)\n", h1);
printf("Child: Received HANDLE2=%p (should work - no O_CLOEXEC)\n", h2);
/* Try to write to both handles */
h1_worked = try_write_to_handle(h1, "HANDLE1");
h2_worked = try_write_to_handle(h2, "HANDLE2");
printf("Child: HANDLE1 (O_CLOEXEC): %s\n",
h1_worked ? "ACCESSIBLE (BAD!)" : "NOT ACCESSIBLE (GOOD!)");
printf("Child: HANDLE2 (no O_CLOEXEC): %s\n",
h2_worked ? "ACCESSIBLE (GOOD!)" : "NOT ACCESSIBLE (BAD!)");
/*
* For O_CLOEXEC to work correctly, h1 should NOT be accessible (h1_worked
* == false) and h2 SHOULD be accessible (h2_worked == true).
*/
if (!h1_worked && h2_worked)
{
printf("Child: Test PASSED - O_CLOEXEC working correctly\n");
exit(0);
}
else
{
printf("Child: Test FAILED - O_CLOEXEC not working correctly\n");
exit(1);
}
#endif
}
static bool
try_write_to_handle(HANDLE h, const char *label)
{
#ifdef WIN32
const char *test_data = "test\n";
DWORD bytes_written;
BOOL result;
result = WriteFile(h, test_data, strlen(test_data), &bytes_written, NULL);
if (result && bytes_written == strlen(test_data))
{
printf("Child: Successfully wrote to %s\n", label);
return true;
}
else
{
printf("Child: Failed to write to %s (error %lu)\n",
label, GetLastError());
return false;
}
#else
return false;
#endif
}