Skip to content

Commit

Permalink
Fix handling of paths with dirfd on Windows
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jart committed Sep 2, 2024
1 parent a089c07 commit 39e7f24
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 46 deletions.
232 changes: 232 additions & 0 deletions examples/spawn.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#if 0
/*─────────────────────────────────────────────────────────────────╗
│ To the extent possible under law, Justine Tunney has waived │
│ all copyright and related or neighboring rights to this file, │
│ as it is written in the following disclaimers: │
│ • http://unlicense.org/ │
│ • http://creativecommons.org/publicdomain/zero/1.0/ │
╚─────────────────────────────────────────────────────────────────*/
#endif

// posix_spawn() example
//
// This program demonstrates the use of posix_spawn() to run the command
// `ls --dired` and capture its output. It teaches several key features:
//
// - Changing the working directory for the child process
// - Redirecting stdout and stderr to pipes
// - Handling the output from the child process
//
// The primary advantage of using posix_spawn() instead of the
// traditional fork()/execve() combination for launching processes is
// safety, efficiency, and cross-platform compatibility.
//
// 1. On Linux, FreeBSD, and NetBSD:
//
// Cosmopolitan Libc's posix_spawn() uses vfork() under the hood on
// these platforms automatically, since it's faster than fork(). It's
// because vfork() creates a child process without needing to copy
// the parent's page tables, making it more efficient, especially for
// large processes. Furthermore, vfork() avoids the need to acquire
// every single mutex (see pthread_atfork() for more details) which
// makes it scalable in multi-threaded apps, since the other threads
// in your app can keep going while the spawning thread waits for the
// subprocess to call execve(). Normally vfork() is error-prone since
// there exists few functions that are @vforksafe. the posix_spawn()
// API is designed to offer maximum assurance that you can't shoot
// yourself in the foot. If you do, then file a bug with Cosmo.
//
// 2. On Windows:
//
// posix_spawn() avoids fork() entirely. Windows doesn't natively
// support fork(), and emulating it can be slow and memory-intensive.
// By using posix_spawn(), we get a much faster process creation on
// Windows systems, because it only needs to call CreateProcess().
// Your file actions are replayed beforehand in a simulated way. Only
// Cosmopolitan Libc offers this level of quality. With Cygwin you'd
// have to use its proprietary APIs to achieve the same performance.
//
// 3. Simplified error handling:
//
// posix_spawn() combines process creation and program execution in a
// single call, reducing the points of failure and simplifying error
// handling. One important thing that happens with Cosmopolitan's
// posix_spawn() implementation is that the error code of execve()
// inside your subprocess, should it fail, will be propagated to your
// parent process. This will happen efficiently via vfork() shared
// memory in the event your Linux environment supports this. If it
// doesn't, then Cosmopolitan will fall back to a throwaway pipe().
// The pipe is needed on platforms like XNU and OpenBSD which do not
// support vfork(). It's also needed under QEMU User.
//
// 4. Signal safety:
//
// posix_spawn() guarantees your signal handler callback functions
// won't be executed in the child process. By default, it'll remove
// sigaction() callbacks atomically. This ensures that if something
// like a SIGTERM or SIGHUP is sent to the child process before it's
// had a chance to call execve(), then the child process will simply
// be terminated (like the spawned process would) instead of running
// whatever signal handlers the spawning process has installed. If
// you've set some signals to SIG_IGN, then that'll be preserved for
// the child process by posix_spawn(), unless you explicitly call
// posix_spawnattr_setsigdefault() to reset them.
//
// 5. Portability:
//
// posix_spawn() is part of the POSIX standard, making it more
// portable across different UNIX-like systems and Windows (with
// appropriate libraries). Even the non-POSIX APIs we use here are
// portable; e.g. posix_spawn_file_actions_addchdir_np() is supported
// by glibc, musl, freebsd, and apple too.
//
// These benefits make posix_spawn() a preferred choice for efficient
// and portable process creation in many scenarios, especially when
// launching many processes or on systems where process creation
// performance is critical.

#define _GNU_SOURCE
#include <fcntl.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define PIPE_READ 0
#define PIPE_WRITE 1

int main() {
pid_t pid;
int status, ret;
posix_spawnattr_t attr;
posix_spawn_file_actions_t actions;
char *const argv[] = {"ls", "--dired", NULL};
int pipe_stdout[2], pipe_stderr[2];

// Initialize file actions
ret = posix_spawnattr_init(&attr);
if (ret != 0) {
fprintf(stderr, "posix_spawnattr_init failed: %s\n", strerror(ret));
return 1;
}

// Explicitly request vfork() from posix_spawn() implementation
//
// This is currently the default for Cosmopolitan Libc, however you
// may want to set this anyway, for portability with other platforms.
// Please note that vfork() isn't officially specified by POSIX, so
// portable code may want to omit this and just use the default.
ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
if (ret != 0) {
fprintf(stderr, "posix_spawnattr_setflags failed: %s\n", strerror(ret));
return 1;
}

// Initialize file actions
ret = posix_spawn_file_actions_init(&actions);
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_init failed: %s\n",
strerror(ret));
return 1;
}

// Change directory to $HOME
ret = posix_spawn_file_actions_addchdir_np(&actions, getenv("HOME"));
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_addchdir_np failed: %s\n",
strerror(ret));
return 1;
}

// Create pipes for stdout and stderr
if (pipe(pipe_stdout) == -1 || pipe(pipe_stderr) == -1) {
perror("pipe");
return 1;
}

// Redirect child's stdout to pipe
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stdout[PIPE_WRITE],
STDOUT_FILENO);
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stdout) failed: %s\n",
strerror(ret));
return 1;
}

// Redirect child's stderr to pipe
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stderr[PIPE_WRITE],
STDERR_FILENO);
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stderr) failed: %s\n",
strerror(ret));
return 1;
}

// Close unused write ends of pipes in the child process
ret = posix_spawn_file_actions_addclose(&actions, pipe_stdout[PIPE_READ]);
if (ret != 0) {
fprintf(stderr,
"posix_spawn_file_actions_addclose (stdout read) failed: %s\n",
strerror(ret));
return 1;
}
ret = posix_spawn_file_actions_addclose(&actions, pipe_stderr[PIPE_READ]);
if (ret != 0) {
fprintf(stderr,
"posix_spawn_file_actions_addclose (stderr read) failed: %s\n",
strerror(ret));
return 1;
}

// Spawn the child process
ret = posix_spawnp(&pid, "ls", &actions, NULL, argv, NULL);
if (ret != 0) {
fprintf(stderr, "posix_spawn failed: %s\n", strerror(ret));
return 1;
}

// Close unused write ends of pipes in the parent process
close(pipe_stdout[PIPE_WRITE]);
close(pipe_stderr[PIPE_WRITE]);

// Read and print output from child process
char buffer[4096];
ssize_t bytes_read;

printf("Stdout from child process:\n");
while ((bytes_read = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer))) >
0) {
write(STDOUT_FILENO, buffer, bytes_read);
}

printf("\nStderr from child process:\n");
while ((bytes_read = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer))) >
0) {
write(STDERR_FILENO, buffer, bytes_read);
}

// Wait for the child process to complete
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return 1;
}

// Clean up
posix_spawn_file_actions_destroy(&actions);
posix_spawnattr_destroy(&attr);
close(pipe_stdout[PIPE_READ]);
close(pipe_stderr[PIPE_READ]);

if (WIFEXITED(status)) {
printf("Child process exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child process terminated with signal %s\n",
strsignal(WTERMSIG(status)));
} else {
printf("Child process did not exit normally\n");
}

return 0;
}
18 changes: 16 additions & 2 deletions libc/calls/mkntpathat.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/calls/internal.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/intrin/kprintf.h"
#include "libc/intrin/strace.h"
#include "libc/macros.h"
#include "libc/nt/enum/fileflagandattributes.h"
Expand All @@ -27,6 +28,18 @@
#include "libc/sysv/consts/at.h"
#include "libc/sysv/errfuns.h"

static int IsAlpha(int c) {
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
}

static bool IsAbsolutePathWin32(char16_t *path) {
if (path[0] == '\\')
return true;
if (IsAlpha(path[0]) && path[1] == ':')
return true;
return false;
}

static textwindows int __mkntpathath_impl(int64_t dirhand, const char *path,
int flags,
char16_t file[hasatleast PATH_MAX]) {
Expand All @@ -39,7 +52,7 @@ static textwindows int __mkntpathath_impl(int64_t dirhand, const char *path,
return -1;
if (!filelen)
return enoent();
if (file[0] != u'\\' && dirhand != AT_FDCWD) { // ProTip: \\?\C:\foo
if (dirhand != AT_FDCWD && !IsAbsolutePathWin32(file)) {
dirlen = GetFinalPathNameByHandle(dirhand, dir, ARRAYLEN(dir),
kNtFileNameNormalized | kNtVolumeNameDos);
if (!dirlen)
Expand All @@ -49,7 +62,8 @@ static textwindows int __mkntpathath_impl(int64_t dirhand, const char *path,
dir[dirlen] = u'\\';
memcpy(dir + dirlen + 1, file, (filelen + 1) * sizeof(char16_t));
memcpy(file, dir, ((n = dirlen + 1 + filelen) + 1) * sizeof(char16_t));
return __normntpath(file, n);
int res = __normntpath(file, n);
return res;
} else {
return filelen;
}
Expand Down
2 changes: 1 addition & 1 deletion libc/intrin/createfile.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ CreateFile(const char16_t *lpFileName, //
hHandle = __imp_CreateFileW(lpFileName, dwDesiredAccess, dwShareMode,
opt_lpSecurity, dwCreationDisposition,
dwFlagsAndAttributes, opt_hTemplateFile);
NTTRACE("CreateFile(%#hs, %s, %s, %s, %s, %s, %ld) → {%ld, %d}", lpFileName,
NTTRACE("CreateFile(%#!hs, %s, %s, %s, %s, %s, %ld) → {%ld, %d}", lpFileName,
_DescribeNtFileAccessFlags(buf_accessflags, dwDesiredAccess),
_DescribeNtFileShareFlags(buf_shareflags, dwShareMode),
_DescribeNtSecurityAttributes(buf_secattr, opt_lpSecurity),
Expand Down
5 changes: 3 additions & 2 deletions libc/intrin/wsarecvfrom.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "libc/intrin/likely.h"
#include "libc/intrin/strace.h"
#include "libc/nt/runtime.h"
#include "libc/nt/struct/iovec.h"
#include "libc/nt/thunk/msabi.h"
#include "libc/nt/winsock.h"
#include "libc/runtime/runtime.h"
Expand Down Expand Up @@ -54,8 +55,8 @@ textwindows int WSARecvFrom(
}
if (UNLIKELY(__strace > 0) && strace_enabled(0) > 0) {
kprintf(STRACE_PROLOGUE "WSARecvFrom(%lu, [", s);
DescribeIovNt(inout_lpBuffers, dwBufferCount,
rc != -1 ? NumberOfBytesRecvd : 0);
_DescribeIovNt(inout_lpBuffers, dwBufferCount,
rc != -1 ? NumberOfBytesRecvd : 0);
kprintf("], %u, [%'u], %p, %p, %p, %s, %p) → %d %d\n", dwBufferCount,
NumberOfBytesRecvd, opt_out_fromsockaddr, opt_inout_fromsockaddrlen,
inout_lpFlags, DescribeNtOverlapped(opt_inout_lpOverlapped),
Expand Down
Loading

0 comments on commit 39e7f24

Please sign in to comment.