Skip to content

Commit

Permalink
Rewrite getcwd()
Browse files Browse the repository at this point in the history
This change addresses a bug that was reported in #923 where bash on
Windows behaved strangely. It turned out that our weak linking of
malloc() caused bash's configure script to favor its own getcwd()
function, which is implemented in the most astonishing way, using
opendir() and readdir() to recursively construct the current path.

This change moves getcwd() into LIBC_STDIO so it can strongly link
malloc(). A new __getcwd() function is now introduced, so all the
low-level runtime services can still use the actual system call. It
provides the Linux Kernel API convention across platforms, and is
overall a higher-quality implementation than what we had before.

In the future, we should probably take a closer look into why bash's
getcwd() polyfill wasn't working as intended on Windows, since there
might be a potential opportunity there to improve our readdir() too.
  • Loading branch information
jart committed Nov 2, 2023
1 parent a46ec61 commit 1eb6484
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 207 deletions.
1 change: 1 addition & 0 deletions libc/calls/calls.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ bool32 isexecutable(const char *);
bool32 isregularfile(const char *);
bool32 issymlink(const char *);
char *commandv(const char *, char *, size_t);
int __getcwd(char *, size_t);
int clone(void *, void *, size_t, int, void *, void *, void *, void *);
int fadvise(int, uint64_t, uint64_t, int);
int makedirs(const char *, unsigned);
Expand Down
2 changes: 0 additions & 2 deletions libc/calls/calls.mk
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ o//libc/calls/prctl.o: \
# we always want -Os because:
# it's early runtime mandatory and quite huge without it
o//libc/calls/getcwd.greg.o \
o//libc/calls/getcwd-nt.greg.o \
o//libc/calls/getcwd-xnu.greg.o \
o//libc/calls/statfs2cosmo.o: private \
CFLAGS += \
-Os
Expand Down
84 changes: 0 additions & 84 deletions libc/calls/getcwd-nt.greg.c

This file was deleted.

193 changes: 133 additions & 60 deletions libc/calls/getcwd.greg.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,154 @@
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
│ PERFORMANCE OF THIS SOFTWARE. │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/state.internal.h"
#include "libc/calls/struct/metastat.internal.h"
#include "libc/calls/syscall-nt.internal.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/strace.internal.h"
#include "libc/intrin/weaken.h"
#include "libc/limits.h"
#include "libc/log/backtrace.internal.h"
#include "libc/mem/mem.h"
#include "libc/nt/files.h"
#include "libc/stdio/sysparam.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/at.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/errfuns.h"

/**
* Returns current working directory, e.g.
*
* const char *dirname = gc(getcwd(0,0)); // if malloc is linked
* const char *dirname = getcwd(alloca(PATH_MAX),PATH_MAX);
*
* @param buf is where UTF-8 NUL-terminated path string gets written,
* which may be NULL to ask this function to malloc a buffer
* @param size is number of bytes available in buf, e.g. PATH_MAX+1,
* which may be 0 if buf is NULL
* @return buf containing system-normative path or NULL w/ errno
* @error ERANGE, EINVAL, ENOMEM
*/
char *getcwd(char *buf, size_t size) {
char *p, *r;
if (buf) {
p = buf;
if (!size) {
einval();
STRACE("getcwd(%p, %'zu) %m", buf, size);
return 0;
}
} else if (_weaken(malloc)) {
unassert(!__vforked);
if (!size) size = PATH_MAX;
if (!(p = _weaken(malloc)(size))) {
STRACE("getcwd(%p, %'zu) %m", buf, size);
return 0;
#define XNU_F_GETPATH 50
#define XNU_MAXPATHLEN 1024

static int sys_getcwd_xnu(char *res, size_t size) {
int fd, len, rc = -1;
union metastat st[2];
char buf[XNU_MAXPATHLEN];
if ((fd = __sys_openat_nc(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY, 0)) != -1) {
if (__sys_fstat(fd, &st[0]) != -1) {
if (st[0].xnu.st_dev && st[0].xnu.st_ino) {
if (__sys_fcntl(fd, XNU_F_GETPATH, (uintptr_t)buf) != -1) {
if (__sys_fstatat(AT_FDCWD, buf, &st[1], 0) != -1) {
if (st[0].xnu.st_dev == st[1].xnu.st_dev &&
st[0].xnu.st_ino == st[1].xnu.st_ino) {
if ((len = strlen(buf)) < size) {
memcpy(res, buf, (rc = len + 1));
} else {
erange();
}
} else {
einval();
}
}
}
} else {
einval();
}
}
sys_close(fd);
}
return rc;
}

static int sys_getcwd_metal(char *buf, size_t size) {
if (size >= 5) {
strcpy(buf, "/zip");
return 5;
} else {
einval();
STRACE("getcwd() needs buf≠0 or __static_yoink(\"malloc\")");
return 0;
return erange();
}
*p = '\0';
if (!IsWindows()) {
if (IsMetal()) {
r = size >= 5 ? strcpy(p, "/zip") : 0;
} else if (IsXnu()) {
r = sys_getcwd_xnu(p, size);
} else if (sys_getcwd(p, size) != (void *)-1) {
r = p;
} else {
r = 0;
}

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

static dontinline textwindows int sys_getcwd_nt(char *buf, size_t size) {

// get current directory from the system
char16_t p16[PATH_MAX];
uint32_t n = GetCurrentDirectory(PATH_MAX, p16);
if (!n) return eacces(); // system call failed
if (n >= PATH_MAX) return erange(); // not enough room?!?

// convert utf-16 to utf-8
// we can't modify `buf` until we're certain of success
char p8[PATH_MAX], *p = p8;
n = tprecode16to8(p, PATH_MAX, p16).ax;
if (n >= PATH_MAX) return erange(); // utf-8 explosion

// turn \\?\c:\... into c:\...
if (p[0] == '\\' && //
p[1] == '\\' && //
p[2] == '?' && //
p[3] == '\\' && //
IsAlpha(p[4]) && //
p[5] == ':' && //
p[6] == '\\') {
p += 4;
n -= 4;
}

// turn c:\... into \c\...
if (IsAlpha(p[0]) && //
p[1] == ':' && //
p[2] == '\\') {
p[1] = p[0];
p[0] = '\\';
}

// we now know the final length
// check if the user supplied a buffer large enough
if (n >= size) {
return erange();
}

// copy bytes converting backslashes to slash
for (int i = 0; i < n; ++i) {
int c = p[i];
if (c == '\\') {
c = '/';
}
} else {
r = sys_getcwd_nt(p, size);
buf[i] = c;
}
if (!buf) {
if (!r) {
if (_weaken(free)) {
_weaken(free)(p);
}

// return number of bytes including nul
buf[n++] = 0;
return n;
}

/**
* Returns current working directory.
*
* Cosmo provides this function to address the shortcomings of getcwd().
* First, this function doesn't link malloc(). Secondly, this offers the
* Linux and NetBSD's getcwd() API across platforms since it's the best.
*
* @param buf receives utf-8 path and isn't modified on error
* @param size is byte capacity of `buf`
* @return bytes copied including nul on success, or -1 w/ errno
* @raise EACCES if the current directory path couldn't be accessed
* @raise ERANGE if `size` wasn't big enough for path and nul byte
* @raise EFAULT if `buf` points to invalid memory
*/
int __getcwd(char *buf, size_t size) {
int rc;
if (IsLinux() || IsNetbsd()) {
rc = sys_getcwd(buf, size);
} else if (IsXnu()) {
rc = sys_getcwd_xnu(buf, size);
} else if (IsWindows()) {
rc = sys_getcwd_nt(buf, size);
} else if (IsFreebsd() || IsOpenbsd()) {
if (sys_getcwd(buf, size) != -1) {
rc = strlen(buf) + 1;
} else if (SupportsFreebsd() && (errno == ENOMEM || errno == EINVAL)) {
rc = erange();
} else {
if (_weaken(realloc)) {
if ((p = _weaken(realloc)(r, strlen(r) + 1))) {
r = p;
}
}
rc = -1;
}
} else {
rc = sys_getcwd_metal(buf, size);
}
STRACE("getcwd(%p, %'zu) → %#s", buf, size, r);
return r;
STRACE("__getcwd([%#hhs], %'zu) → %d% m", rc != -1 ? buf : "n/a", size, rc);
return rc;
}
5 changes: 3 additions & 2 deletions libc/calls/getprogramexecutablename.greg.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ static inline void InitProgramExecutableNameImpl(void) {
if (q[0] == '.' && q[1] == '/') {
q += 2;
}
if (getcwd(p, e - p - 1 - 4)) { // for / and .com
while (*p) ++p;
int got = __getcwd(p, e - p - 1 - 4); // for / and .com
if (got != -1) {
p += got - 1;
*p++ = '/';
}
}
Expand Down
1 change: 0 additions & 1 deletion libc/calls/syscall-nt.internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
COSMOPOLITAN_C_START_

bool32 sys_isatty(int);
char *sys_getcwd_nt(char *, size_t);
int sys_chdir_nt(const char *);
int sys_close_epoll_nt(int);
int sys_dup_nt(int, int, int, int);
Expand Down
5 changes: 2 additions & 3 deletions libc/calls/syscall-sysv.internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ COSMOPOLITAN_C_START_
axdx_t __sys_fork(void);
axdx_t __sys_pipe(i32[hasatleast 2], i32);
axdx_t sys_getpid(void);
char *sys_getcwd(char *, u64);
char *sys_getcwd_xnu(char *, u64);
i32 __sys_dup3(i32, i32, i32);
i32 __sys_execve(const char *, char *const[], char *const[]);
i32 __sys_fcntl(i32, i32, ...);
Expand Down Expand Up @@ -53,6 +51,7 @@ i32 sys_fork(void);
i32 sys_fsync(i32);
i32 sys_ftruncate(i32, i64, i64);
i32 sys_getcontext(void *);
i32 sys_getcwd(char *, u64);
i32 sys_getpgid(i32);
i32 sys_getppid(void);
i32 sys_getpriority(i32, u32);
Expand Down Expand Up @@ -85,9 +84,9 @@ i32 sys_posix_openpt(i32);
i32 sys_renameat(i32, const char *, i32, const char *);
i32 sys_sem_close(i64);
i32 sys_sem_destroy(i64);
i32 sys_sem_destroy(i64);
i32 sys_sem_getvalue(i64, u32 *);
i32 sys_sem_init(u32, i64 *);
i32 sys_sem_destroy(i64);
i32 sys_sem_open(const char *, int, u32, i64 *);
i32 sys_sem_post(i64);
i32 sys_sem_trywait(i64);
Expand Down
2 changes: 1 addition & 1 deletion libc/calls/tmpdir.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ static void __tmpdir_init(void) {

if ((s = getenv("TMPDIR"))) {
if (*s != '/') {
if (!getcwd(__tmpdir.path, PATH_MAX)) {
if (__getcwd(__tmpdir.path, PATH_MAX) == -1) {
goto GiveUp;
}
strlcat(__tmpdir.path, "/", sizeof(__tmpdir.path));
Expand Down
2 changes: 1 addition & 1 deletion libc/mem/realpath.c
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ char *realpath(const char *filename, char *resolved)
output[q] = 0;

if (output[0] != '/') {
if (!getcwd(stack, sizeof(stack))) return 0;
if (__getcwd(stack, sizeof(stack)) == -1) return 0;
l = strlen(stack);
/* Cancel any initial .. components. */
p = 0;
Expand Down
Loading

0 comments on commit 1eb6484

Please sign in to comment.