Skip to content

Commit 39e7f24

Browse files
committed
Fix handling of paths with dirfd on Windows
This change fixes an issue with all system calls ending with *at(), when the caller passes `dirfd != AT_FDCWD` and an absolute path. It's because the old code was turning paths like C:\bin\ls into \\C:\bin\ls\C:\bin\ls after being converted from paths like /C/bin/ls. I noticed this when the Emacs dired mode stopped working. It's unclear if it's a regression with Cosmopolitan Libc or if this was introduced by the Emacs v29 upgrade. It also impacted posix_spawn() for which a newly minted example now exists.
1 parent a089c07 commit 39e7f24

File tree

10 files changed

+373
-46
lines changed

10 files changed

+373
-46
lines changed

examples/spawn.c

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#if 0
2+
/*─────────────────────────────────────────────────────────────────╗
3+
│ To the extent possible under law, Justine Tunney has waived │
4+
│ all copyright and related or neighboring rights to this file, │
5+
│ as it is written in the following disclaimers: │
6+
│ • http://unlicense.org/ │
7+
│ • http://creativecommons.org/publicdomain/zero/1.0/ │
8+
╚─────────────────────────────────────────────────────────────────*/
9+
#endif
10+
11+
// posix_spawn() example
12+
//
13+
// This program demonstrates the use of posix_spawn() to run the command
14+
// `ls --dired` and capture its output. It teaches several key features:
15+
//
16+
// - Changing the working directory for the child process
17+
// - Redirecting stdout and stderr to pipes
18+
// - Handling the output from the child process
19+
//
20+
// The primary advantage of using posix_spawn() instead of the
21+
// traditional fork()/execve() combination for launching processes is
22+
// safety, efficiency, and cross-platform compatibility.
23+
//
24+
// 1. On Linux, FreeBSD, and NetBSD:
25+
//
26+
// Cosmopolitan Libc's posix_spawn() uses vfork() under the hood on
27+
// these platforms automatically, since it's faster than fork(). It's
28+
// because vfork() creates a child process without needing to copy
29+
// the parent's page tables, making it more efficient, especially for
30+
// large processes. Furthermore, vfork() avoids the need to acquire
31+
// every single mutex (see pthread_atfork() for more details) which
32+
// makes it scalable in multi-threaded apps, since the other threads
33+
// in your app can keep going while the spawning thread waits for the
34+
// subprocess to call execve(). Normally vfork() is error-prone since
35+
// there exists few functions that are @vforksafe. the posix_spawn()
36+
// API is designed to offer maximum assurance that you can't shoot
37+
// yourself in the foot. If you do, then file a bug with Cosmo.
38+
//
39+
// 2. On Windows:
40+
//
41+
// posix_spawn() avoids fork() entirely. Windows doesn't natively
42+
// support fork(), and emulating it can be slow and memory-intensive.
43+
// By using posix_spawn(), we get a much faster process creation on
44+
// Windows systems, because it only needs to call CreateProcess().
45+
// Your file actions are replayed beforehand in a simulated way. Only
46+
// Cosmopolitan Libc offers this level of quality. With Cygwin you'd
47+
// have to use its proprietary APIs to achieve the same performance.
48+
//
49+
// 3. Simplified error handling:
50+
//
51+
// posix_spawn() combines process creation and program execution in a
52+
// single call, reducing the points of failure and simplifying error
53+
// handling. One important thing that happens with Cosmopolitan's
54+
// posix_spawn() implementation is that the error code of execve()
55+
// inside your subprocess, should it fail, will be propagated to your
56+
// parent process. This will happen efficiently via vfork() shared
57+
// memory in the event your Linux environment supports this. If it
58+
// doesn't, then Cosmopolitan will fall back to a throwaway pipe().
59+
// The pipe is needed on platforms like XNU and OpenBSD which do not
60+
// support vfork(). It's also needed under QEMU User.
61+
//
62+
// 4. Signal safety:
63+
//
64+
// posix_spawn() guarantees your signal handler callback functions
65+
// won't be executed in the child process. By default, it'll remove
66+
// sigaction() callbacks atomically. This ensures that if something
67+
// like a SIGTERM or SIGHUP is sent to the child process before it's
68+
// had a chance to call execve(), then the child process will simply
69+
// be terminated (like the spawned process would) instead of running
70+
// whatever signal handlers the spawning process has installed. If
71+
// you've set some signals to SIG_IGN, then that'll be preserved for
72+
// the child process by posix_spawn(), unless you explicitly call
73+
// posix_spawnattr_setsigdefault() to reset them.
74+
//
75+
// 5. Portability:
76+
//
77+
// posix_spawn() is part of the POSIX standard, making it more
78+
// portable across different UNIX-like systems and Windows (with
79+
// appropriate libraries). Even the non-POSIX APIs we use here are
80+
// portable; e.g. posix_spawn_file_actions_addchdir_np() is supported
81+
// by glibc, musl, freebsd, and apple too.
82+
//
83+
// These benefits make posix_spawn() a preferred choice for efficient
84+
// and portable process creation in many scenarios, especially when
85+
// launching many processes or on systems where process creation
86+
// performance is critical.
87+
88+
#define _GNU_SOURCE
89+
#include <fcntl.h>
90+
#include <spawn.h>
91+
#include <stdio.h>
92+
#include <stdlib.h>
93+
#include <string.h>
94+
#include <sys/wait.h>
95+
#include <unistd.h>
96+
97+
#define PIPE_READ 0
98+
#define PIPE_WRITE 1
99+
100+
int main() {
101+
pid_t pid;
102+
int status, ret;
103+
posix_spawnattr_t attr;
104+
posix_spawn_file_actions_t actions;
105+
char *const argv[] = {"ls", "--dired", NULL};
106+
int pipe_stdout[2], pipe_stderr[2];
107+
108+
// Initialize file actions
109+
ret = posix_spawnattr_init(&attr);
110+
if (ret != 0) {
111+
fprintf(stderr, "posix_spawnattr_init failed: %s\n", strerror(ret));
112+
return 1;
113+
}
114+
115+
// Explicitly request vfork() from posix_spawn() implementation
116+
//
117+
// This is currently the default for Cosmopolitan Libc, however you
118+
// may want to set this anyway, for portability with other platforms.
119+
// Please note that vfork() isn't officially specified by POSIX, so
120+
// portable code may want to omit this and just use the default.
121+
ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
122+
if (ret != 0) {
123+
fprintf(stderr, "posix_spawnattr_setflags failed: %s\n", strerror(ret));
124+
return 1;
125+
}
126+
127+
// Initialize file actions
128+
ret = posix_spawn_file_actions_init(&actions);
129+
if (ret != 0) {
130+
fprintf(stderr, "posix_spawn_file_actions_init failed: %s\n",
131+
strerror(ret));
132+
return 1;
133+
}
134+
135+
// Change directory to $HOME
136+
ret = posix_spawn_file_actions_addchdir_np(&actions, getenv("HOME"));
137+
if (ret != 0) {
138+
fprintf(stderr, "posix_spawn_file_actions_addchdir_np failed: %s\n",
139+
strerror(ret));
140+
return 1;
141+
}
142+
143+
// Create pipes for stdout and stderr
144+
if (pipe(pipe_stdout) == -1 || pipe(pipe_stderr) == -1) {
145+
perror("pipe");
146+
return 1;
147+
}
148+
149+
// Redirect child's stdout to pipe
150+
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stdout[PIPE_WRITE],
151+
STDOUT_FILENO);
152+
if (ret != 0) {
153+
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stdout) failed: %s\n",
154+
strerror(ret));
155+
return 1;
156+
}
157+
158+
// Redirect child's stderr to pipe
159+
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stderr[PIPE_WRITE],
160+
STDERR_FILENO);
161+
if (ret != 0) {
162+
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stderr) failed: %s\n",
163+
strerror(ret));
164+
return 1;
165+
}
166+
167+
// Close unused write ends of pipes in the child process
168+
ret = posix_spawn_file_actions_addclose(&actions, pipe_stdout[PIPE_READ]);
169+
if (ret != 0) {
170+
fprintf(stderr,
171+
"posix_spawn_file_actions_addclose (stdout read) failed: %s\n",
172+
strerror(ret));
173+
return 1;
174+
}
175+
ret = posix_spawn_file_actions_addclose(&actions, pipe_stderr[PIPE_READ]);
176+
if (ret != 0) {
177+
fprintf(stderr,
178+
"posix_spawn_file_actions_addclose (stderr read) failed: %s\n",
179+
strerror(ret));
180+
return 1;
181+
}
182+
183+
// Spawn the child process
184+
ret = posix_spawnp(&pid, "ls", &actions, NULL, argv, NULL);
185+
if (ret != 0) {
186+
fprintf(stderr, "posix_spawn failed: %s\n", strerror(ret));
187+
return 1;
188+
}
189+
190+
// Close unused write ends of pipes in the parent process
191+
close(pipe_stdout[PIPE_WRITE]);
192+
close(pipe_stderr[PIPE_WRITE]);
193+
194+
// Read and print output from child process
195+
char buffer[4096];
196+
ssize_t bytes_read;
197+
198+
printf("Stdout from child process:\n");
199+
while ((bytes_read = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer))) >
200+
0) {
201+
write(STDOUT_FILENO, buffer, bytes_read);
202+
}
203+
204+
printf("\nStderr from child process:\n");
205+
while ((bytes_read = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer))) >
206+
0) {
207+
write(STDERR_FILENO, buffer, bytes_read);
208+
}
209+
210+
// Wait for the child process to complete
211+
if (waitpid(pid, &status, 0) == -1) {
212+
perror("waitpid");
213+
return 1;
214+
}
215+
216+
// Clean up
217+
posix_spawn_file_actions_destroy(&actions);
218+
posix_spawnattr_destroy(&attr);
219+
close(pipe_stdout[PIPE_READ]);
220+
close(pipe_stderr[PIPE_READ]);
221+
222+
if (WIFEXITED(status)) {
223+
printf("Child process exited with status %d\n", WEXITSTATUS(status));
224+
} else if (WIFSIGNALED(status)) {
225+
printf("Child process terminated with signal %s\n",
226+
strsignal(WTERMSIG(status)));
227+
} else {
228+
printf("Child process did not exit normally\n");
229+
}
230+
231+
return 0;
232+
}

libc/calls/mkntpathat.c

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
╚─────────────────────────────────────────────────────────────────────────────*/
1919
#include "libc/calls/internal.h"
2020
#include "libc/calls/syscall_support-nt.internal.h"
21+
#include "libc/intrin/kprintf.h"
2122
#include "libc/intrin/strace.h"
2223
#include "libc/macros.h"
2324
#include "libc/nt/enum/fileflagandattributes.h"
@@ -27,6 +28,18 @@
2728
#include "libc/sysv/consts/at.h"
2829
#include "libc/sysv/errfuns.h"
2930

31+
static int IsAlpha(int c) {
32+
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
33+
}
34+
35+
static bool IsAbsolutePathWin32(char16_t *path) {
36+
if (path[0] == '\\')
37+
return true;
38+
if (IsAlpha(path[0]) && path[1] == ':')
39+
return true;
40+
return false;
41+
}
42+
3043
static textwindows int __mkntpathath_impl(int64_t dirhand, const char *path,
3144
int flags,
3245
char16_t file[hasatleast PATH_MAX]) {
@@ -39,7 +52,7 @@ static textwindows int __mkntpathath_impl(int64_t dirhand, const char *path,
3952
return -1;
4053
if (!filelen)
4154
return enoent();
42-
if (file[0] != u'\\' && dirhand != AT_FDCWD) { // ProTip: \\?\C:\foo
55+
if (dirhand != AT_FDCWD && !IsAbsolutePathWin32(file)) {
4356
dirlen = GetFinalPathNameByHandle(dirhand, dir, ARRAYLEN(dir),
4457
kNtFileNameNormalized | kNtVolumeNameDos);
4558
if (!dirlen)
@@ -49,7 +62,8 @@ static textwindows int __mkntpathath_impl(int64_t dirhand, const char *path,
4962
dir[dirlen] = u'\\';
5063
memcpy(dir + dirlen + 1, file, (filelen + 1) * sizeof(char16_t));
5164
memcpy(file, dir, ((n = dirlen + 1 + filelen) + 1) * sizeof(char16_t));
52-
return __normntpath(file, n);
65+
int res = __normntpath(file, n);
66+
return res;
5367
} else {
5468
return filelen;
5569
}

libc/intrin/createfile.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ CreateFile(const char16_t *lpFileName, //
5656
hHandle = __imp_CreateFileW(lpFileName, dwDesiredAccess, dwShareMode,
5757
opt_lpSecurity, dwCreationDisposition,
5858
dwFlagsAndAttributes, opt_hTemplateFile);
59-
NTTRACE("CreateFile(%#hs, %s, %s, %s, %s, %s, %ld) → {%ld, %d}", lpFileName,
59+
NTTRACE("CreateFile(%#!hs, %s, %s, %s, %s, %s, %ld) → {%ld, %d}", lpFileName,
6060
_DescribeNtFileAccessFlags(buf_accessflags, dwDesiredAccess),
6161
_DescribeNtFileShareFlags(buf_shareflags, dwShareMode),
6262
_DescribeNtSecurityAttributes(buf_secattr, opt_lpSecurity),

libc/intrin/wsarecvfrom.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "libc/intrin/likely.h"
2424
#include "libc/intrin/strace.h"
2525
#include "libc/nt/runtime.h"
26+
#include "libc/nt/struct/iovec.h"
2627
#include "libc/nt/thunk/msabi.h"
2728
#include "libc/nt/winsock.h"
2829
#include "libc/runtime/runtime.h"
@@ -54,8 +55,8 @@ textwindows int WSARecvFrom(
5455
}
5556
if (UNLIKELY(__strace > 0) && strace_enabled(0) > 0) {
5657
kprintf(STRACE_PROLOGUE "WSARecvFrom(%lu, [", s);
57-
DescribeIovNt(inout_lpBuffers, dwBufferCount,
58-
rc != -1 ? NumberOfBytesRecvd : 0);
58+
_DescribeIovNt(inout_lpBuffers, dwBufferCount,
59+
rc != -1 ? NumberOfBytesRecvd : 0);
5960
kprintf("], %u, [%'u], %p, %p, %p, %s, %p) → %d %d\n", dwBufferCount,
6061
NumberOfBytesRecvd, opt_out_fromsockaddr, opt_inout_fromsockaddrlen,
6162
inout_lpFlags, DescribeNtOverlapped(opt_inout_lpOverlapped),

0 commit comments

Comments
 (0)