/*
open(... O_NOFOLLOW) is sometimes a bit more than you want -- for example,
libXfont specifies O_NOFOLLOW when opening files to avoid the X server being
tricked into opening arbitrary files as root, but that prevents a user from
having symlinks to fonts (or installing fonts using stow). We want to allow
following a symlink to something owned by the same user.

This is ANSI C, for ease of integration into other applications.

This isn't POSIX; it assumes Linux 3.6 or later for all the *at/O_PATH stuff to
avoid TOCTOU races with symlinks. Other free Unixes are picking up similar
features, though.

cf ngx_openat_file_owner, which doesn't avoid opening the file:
http://lxr.nginx.org/source/src/core/ngx_open_file_cache.c
*/

#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

/* Behave like open(), but only follow symlinks if the uid/gid of the target
   matches the symlink.

   Returns -1 with errno == ELOOP if following symlinks fails. */
int open_semifollow(const char *pathname, int flags) {
    int depth;
    uid_t wantuid = 0;
    gid_t wantgid = 0;
    char procpath[64];

    /* These are all the resources that may be allocated.
       They're explicitly freed before being reallocated,
       and before this function returns. */
    char *target = strdup(pathname);
    int dirfd = AT_FDCWD;
    int pathfd = -1;
    int outfd = -1;

    if (target == NULL)
        goto out;

    for (depth = 0; ; ++depth) {
        char *slash = strrchr(target, '/');
        char *filename;
        struct stat st;
        int targetlen;

        /* Arbitrary, but this matches Linux's limit since 4.2 */
        if (depth > 40) {
            errno = ELOOP;
            goto out;
        }

        if (slash == NULL) {
            /* A name in the same directory */
            filename = target;
        } else {
            /* Changing directory */
            target[slash - target] = '\0';

            /* We do need to follow symlinks when opening the directory */
            int newdirfd = openat(dirfd, target, O_RDONLY | O_PATH);
            if (dirfd != AT_FDCWD)
                (void) close(dirfd);
            if (newdirfd == -1)
                goto out;
            dirfd = newdirfd;

            filename = slash + 1;
        }

        /* Check filename isn't "", in case openat(..., "", ...) works in the future */
        if (strlen(filename) == 0) {
            errno = ELOOP;
            goto out;
        }

        if (pathfd != -1)
            (void) close(pathfd);
        pathfd = openat(dirfd, filename, O_RDONLY | O_PATH | O_NOFOLLOW);
        if (pathfd == -1)
            goto out;

        if (fstat(pathfd, &st) == -1)
            goto out;

        filename = NULL; /* to prevent later use, as we're about to free target */

        if (depth == 0) {
            /* Remember the ownership of the first link */
            wantuid = st.st_uid;
            wantgid = st.st_gid;
        } else {
            /* Check later targets match the original ownership */
            if (wantuid != st.st_uid || wantgid != st.st_gid) {
                errno = ELOOP;
                goto out;
            }
        }

        if (!S_ISLNK(st.st_mode)) {
            /* Found a non-link */
            break;
        }

        /* st_size is the symlink's length on most (but not all!) filesystems */
        targetlen = (st.st_size > 0) ? (st.st_size + 1) : 256;
        while (1) {
            int n;

            free(target);
            target = malloc(targetlen + 1);
            if (target == NULL)
                goto out;

            n = readlinkat(pathfd, "", target, targetlen);
            if (n == -1) {
                goto out;
            } else if (n == targetlen) {
                /* Filled the buffer - try again with more */
                targetlen *= 2;

                /* This is also an arbitrary choice */
                if (targetlen > 65536) {
                    errno = ELOOP;
                    goto out;
                }
            } else {
                /* Got it! */
                target[n] = '\0';
                break;
            }
        }
    }

    /* We now have an O_PATH file descriptor, which we need to convert into a
       real one; this is apparently the least nasty way of doing so!
       Equivalent to openat(pathfd, "", flags), which isn't permitted... */
    if (snprintf(procpath, sizeof procpath, "/proc/self/fd/%d", pathfd)
        >= sizeof procpath) {
        errno = ELOOP;
        goto out;
    }
    outfd = open(procpath, flags & (~O_NOFOLLOW));

out:
    if (target != NULL)
        free(target);
    if (dirfd != AT_FDCWD)
        (void) close(dirfd);
    if (pathfd != -1)
        (void) close(pathfd);

    return outfd;
}

int main(int argc, char *argv[]) {
    printf("result = %d\n", open_semifollow(argv[1], O_RDONLY));
    return 0;
}
