2
\$\begingroup\$

I'm implementing a dirpath and fdirpath functions, which allow to retrieve path from the directory handle (or its descriptor). This can be useful e.g. if one needs to implement some functions like openat(3) on systems which lack similar functions, but have fork(3), mmap(3) and pthreads. I'd like to know your opinions, suggestions on how to improve these functions, proposals, advice and even remarks. The function shall remain as portable as possible (by portability I mean POSIX).

Source code

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>


static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

size_t fdirpath(int fd, char *buffer, size_t size)
{
    int mfd = -1;
    pid_t pid = 0;
    int status = 0;
    size_t msize = 0;
    char *mdata = NULL;
    char mpath[] = "/tmp/dirpath/XXXXXX";
    int const mflags = MAP_SHARED;
    int const mprot = (PROT_READ | PROT_WRITE);
    size_t *total = NULL;

    if ((fd == -1)
    ||  ((buffer == NULL) && (size != 0))
    ||  ((buffer != NULL) && (size == 0))) {
        errno = EINVAL;
        return (size_t)-1;
    }
    pthread_mutex_lock(&mutex);
    if (mkdir("/tmp/dirpath", 0755) == -1) {
        if (errno != EEXIST) {
            return (size_t)-1;
        }
    }
    if ((mfd = mkstemp(mpath)) == -1) {
        return (size_t)-1;
    }
    pthread_mutex_unlock(&mutex);

    /* Create a temporary shared file */
    msize += sizeof(size_t);
    msize += size;
    if (ftruncate(mfd, (off_t)msize)) {
        status = errno;
        close(mfd);
        unlink(mpath);
        errno = status;
        return (size_t)-1;
    }
    if ((mdata = mmap(NULL, msize, mprot, mflags, mfd, 0)) == MAP_FAILED) {
        status = errno;
        close(mfd);
        unlink(mpath);
        errno = status;
        return (size_t)-1;
    }
    total = (size_t*)mdata;
    mdata += sizeof(size_t);
    if (buffer != NULL) {
        memcpy(mdata, buffer, size);
    }
    mdata -= sizeof(size_t);

    /* Determine directory path in child process */
    if ((pid = fork()) == 0) {
        size_t len = 64;
        char *path = NULL;
        void *temp = NULL;

        if (fchdir(fd) == -1) {
            exit(errno);
        }
        total = (size_t*)mdata;
        while (temp == NULL) {
            temp = path;
            path = realloc(path, len);
            if (path == NULL) {
                free(temp);
                exit(ENOMEM);
            }
            memset(path, 0, len);
            temp = getcwd(path, len);
            if (temp == NULL) {
                if (errno != ERANGE) {
                    free(path);
                    exit(errno);
                }
                temp = NULL;
                len *= 2;
            }
        }
        *total = strlen(path);
        if (buffer != NULL) {
            mdata += sizeof(size_t);
            size = ((*total > size) ? size : *total);
            memcpy(mdata, path, size);
        }
        free(path);
        exit(0);
    } else if (pid == -1) {
        status = errno;
        close(mfd);
        unlink(mpath);
        errno = status;
        return (size_t)-1;
    }

    /* Acquire shared data and store it */
    waitpid(pid, &status, 0);
    if (status != 0) {
        close(mfd);
        unlink(mpath);
        munmap(mdata, msize);
        errno = status;
        return (size_t)-1;
    }
    if (buffer != NULL) {
        mdata += sizeof(size_t);
        size = ((*total > size) ? size : *total);
        memcpy(buffer, mdata, size);
        mdata -= sizeof(size_t);
    }
    size = *total;
    if (close(mfd) == -1) {
        status = errno;
        unlink(mpath);
        munmap(mdata, msize);
        errno = status;
        return (size_t)-1;
    }
    if (unlink(mpath) == -1) {
        status = errno;
        munmap(mdata, msize);
        errno = status;
        return (size_t)-1;
    }
    if (munmap(mdata, msize) == -1) {
        return (size_t)-1;
    }

    /* Perform cleanup routines */
    pthread_mutex_lock(&mutex);
    if (rmdir("/tmp/dirpath") == -1) {
#if defined(ENOEMPTY) && (ENOEMPTY != EEXIST)
        if ((errno != EEXIST) && (errno != ENOEMPTY)) {
            return (size_t)-1;
        }
#else
        if (errno != EEXIST) {
            return (size_t)-1;
        }
#endif
    }
    pthread_mutex_unlock(&mutex);
    return size;
}


size_t dirpath(DIR *dir, char *buffer, size_t size)
{
    int fd = -1;

    if (dir == NULL) {
        errno = EINVAL;
        return (size_t)-1;
    }
    if ((fd = dirfd(dir)) == -1) {
        return (size_t)-1;
    }
    return fdirpath(fd, buffer, size);
}

Usage

int main(int argc, char *argv[])
{
    size_t size = 0;
    DIR *dir = NULL;
    char *buffer = NULL;

    /* Opening a directory */
    dir = opendir(".");
    if (dir == NULL) {
        perror("dirpath");
        return EXIT_FAILURE;
    }

    /* Calculating size */
    size = dirpath(dir, NULL, 0);
    if (size == (size_t)-1) {
        perror("dirpath");
        return EXIT_FAILURE;
    }

    /* Allocating memory */
    buffer = malloc(size);
    if (buffer == NULL) {
        perror("dirpath");
        return EXIT_FAILURE;
    }

    /* Determining path */
    if (dirpath(dir, buffer, size) == (size_t)-1) {
        perror("dirpath");
        return EXIT_FAILURE;
    }

    printf("%s\n", buffer);
    closedir(dir);
    free(buffer);
    return EXIT_SUCCESS;
}

Notes

I've tried to be as POSIX-compatible as it is possible; thus I didn't use MAP_ANON: though it's usage is pretty common, it is not included in POSIX standard (what a pity!).

I've used fork(3) to let functions work in the multithreaded environment: getcwd(3) may yield inappropriate results when chdir(3) is called in the different threads. Using a mutex was the very first idea that came to my mind, but I also wanted to try to avoid situations where someone else is calling chdir.

I've used a temporary directory since it seemed to better generate a unique filename for a temporary mmap'ed file. However, I'm unsure if it is correct, since function will fail right after unsuccessfull call to mkdir(3), which seems to be not very good decision. Perhaps it's better just to stick to mkstemp(3) without any directory.

mmap(3) is used here; perhaps it's better to stick to shm* functions? Or pipes? The point is to share the data between parent and child after fork(3), I'm not sure if mmap(3) is actually the best thing here (especially since I cannot use MAP_ANON due to portability reasons, so I have to unlink(3) file).

\$\endgroup\$
2
\$\begingroup\$

I've used fork(3) to let functions work in the multithreaded environment: getcwd(3) may yield inappropriate results when chdir(3) is called in the different threads.

This problem is solved by introducing more problems. It is very unsafe to fork multithreaded program in the first place (see this blog post for details). I don't know how to solve the path resolution problem in absence of *at() calls in a sane way. Maybe, I'd resort to running a path resolution daemon with a unix domain sockets to communicate open descriptors.

In any case, mmap backed up by the actual file doesn't sound right.

\$\endgroup\$
  • \$\begingroup\$ What's an amazing post! Thank you very much, I've never thought about such kind of problems. Well, actually functions can use platform-dependent solutions (e.g. readlink on /proc/PID/fd/XXX on Linux and Cygwin, F_GETPATH for OS X, etc.) and then fallback to current behaviour. However, on mentioned systems there are already *at() calls, if I recall correctly. Actually neither I think that using mmap with the real file is a good idea. The only reason why I do it is that POSIX doesn't support MAP_ANON (there is a proposal to bring this flag in the next standard though). \$\endgroup\$ – ghostmansd Feb 19 '15 at 21:22
  • \$\begingroup\$ What do you think about just using mutexes? That's the first idea that came to my mind, but I also wanted to try to avoid situations where someone else is calling chdir. \$\endgroup\$ – ghostmansd Feb 19 '15 at 21:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.