getcwd() · chdir() · fchdir() · openat() | EmbeddedPathashala

 

Current Working Directory
getcwd() · chdir() · fchdir() · openat() | EmbeddedPathashala
📍
getcwd()
📂
chdir()
🔢
fchdir()
🆕
openat()

What is the Working Directory?

Every process has a current working directory (CWD). When you use a relative path like ./file.txt or just file.txt, the kernel starts resolving it from the CWD. You can retrieve it with getcwd() and change it with chdir().

getcwd()chdir() fchdir()openat() AT_FDCWDPATH_MAX chroot()TOCTOU
getcwd() — Get the Current Directory
#include <unistd.h>
char *getcwd(char *cwdbuf, size_t size);
/* Returns pointer to cwdbuf on success, NULL on error */

Fills the buffer with the absolute pathname of the current working directory, null-terminated. Buffer should be at least PATH_MAX bytes (typically 4096).

On Linux/glibc, passing NULL allocates a buffer of PATH_MAX bytes and returns a pointer. You must free() it when done. But this is a GNU extension — avoid it in portable code.
chdir() and fchdir() — Change the Directory
#include <unistd.h>
int chdir(const char *pathname);
/* Change CWD to pathname (follows symlinks) */

#define _XOPEN_SOURCE 500
int fchdir(int fd);
/* Change CWD to the directory referred to by fd */

fchdir() is useful when you want to temporarily change to another directory and reliably return. You save the current directory as an fd first:

int saved = open(".", O_RDONLY);   /* Save current dir */
chdir("/some/other/path");          /* Go elsewhere */
/* ... do work ... */
fchdir(saved);                      /* Return reliably */
close(saved);
✅ fchdir() is more reliable than getcwd()+chdir() because: (1) it works even if the path is longer than PATH_MAX, (2) the fd is immune to directory renames that happen in between.

openat() — Open Relative to a Directory FD
#define _XOPEN_SOURCE 700
#include <fcntl.h>

int openat(int dirfd, const char *pathname, int flags, /* mode_t mode */);

Just like open(), but pathname is resolved relative to dirfd (an open directory file descriptor) instead of the CWD.

pathname is absolute: dirfd is ignored. Behaves exactly like open().
pathname is relative + dirfd = AT_FDCWD: Same as open() using CWD.
pathname is relative + dirfd = valid fd: Resolved relative to that directory.

Other similar calls: faccessat(), fstatat(), mkdirat(), unlinkat(), renameat(), symlinkat(), readlinkat().

📌 Two reasons to use openat(): (1) Avoids race conditions in multi-threaded code, (2) Lets different threads work in different “virtual” working directories.

chroot() — Change the Root Directory (Jail)
#define _BSD_SOURCE
#include <unistd.h>
int chroot(const char *pathname);  /* Requires CAP_SYS_CHROOT */

Changes the process’s root directory. After chroot("/jail"), the process sees /jail as /. It cannot access files above /jail using absolute paths.

⚠️ Common chroot jail escape: if you forget to chdir(“/”) after chroot(), the process can still access outside files using relative paths via the old CWD.
⚠️ A process with an open fd to a directory outside the jail can escape using fchdir(fd) + chroot(“.”).

Code Examples

Example 1: Get and print the current working directory
#include <stdio.h>
#include <unistd.h>
#include <limits.h>

int main() {
    char buf[PATH_MAX];

    if (getcwd(buf, sizeof(buf)) == NULL) {
        perror("getcwd");
        return 1;
    }
    printf("Current working directory: %s\n", buf);
    return 0;
}
Example 2: Save and restore CWD using fchdir()
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>

int main() {
    char buf[PATH_MAX];

    /* Save current directory as a file descriptor */
    int saved_dir = open(".", O_RDONLY | O_DIRECTORY);
    if (saved_dir == -1) { perror("open ."); return 1; }

    getcwd(buf, sizeof(buf));
    printf("Starting in: %s\n", buf);

    /* Change to /tmp */
    if (chdir("/tmp") == -1) { perror("chdir"); return 1; }
    getcwd(buf, sizeof(buf));
    printf("Changed to : %s\n", buf);

    /* Do some work in /tmp */
    FILE *f = fopen("workfile.txt", "w");
    fputs("temporary data", f);
    fclose(f);

    /* Return to original directory via fd — reliable! */
    if (fchdir(saved_dir) == -1) { perror("fchdir"); return 1; }
    close(saved_dir);

    getcwd(buf, sizeof(buf));
    printf("Returned to: %s\n", buf);
    return 0;
}
Example 3: openat() — thread-safe file opening
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    /* Open a base directory */
    int dirfd = open("/tmp", O_RDONLY | O_DIRECTORY);
    if (dirfd == -1) { perror("open dir"); return 1; }

    /* Create a file inside that dir using openat() */
    int fd = openat(dirfd, "myfile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("openat"); return 1; }

    write(fd, "Hello from openat()\n", 20);
    close(fd);

    /* Read it back */
    fd = openat(dirfd, "myfile.txt", O_RDONLY);
    char buf[64];
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Read back: %s", buf);
    close(fd);

    /* Cleanup using unlinkat() */
    unlinkat(dirfd, "myfile.txt", 0);
    close(dirfd);
    return 0;
}
Example 4: fstatat() — stat relative to a directory fd
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    int dirfd = open("/etc", O_RDONLY | O_DIRECTORY);

    struct stat sb;
    /* Stat /etc/passwd relative to dirfd */
    if (fstatat(dirfd, "passwd", &sb, 0) == -1) {
        perror("fstatat");
        return 1;
    }
    printf("/etc/passwd: size=%ld, inode=%lu\n",
           (long)sb.st_size, (unsigned long)sb.st_ino);

    /* Use AT_SYMLINK_NOFOLLOW to stat symlink itself */
    if (fstatat(dirfd, "hostname", &sb, AT_SYMLINK_NOFOLLOW) == 0) {
        printf("/etc/hostname: is symlink = %s\n",
               S_ISLNK(sb.st_mode) ? "yes" : "no");
    }

    close(dirfd);
    return 0;
}
Example 5: chroot() setup (run as root)
#define _BSD_SOURCE
#include <stdio.h>
#include <unistd.h>

int main() {
    /* This requires root privileges */

    /* Set up a minimal jail at /tmp/jail */
    /* (In practice you'd copy needed libraries too) */

    if (chroot("/tmp/jail") == -1) {
        perror("chroot (need root)");
        return 1;
    }

    /* CRITICAL: always chdir to / after chroot */
    if (chdir("/") == -1) {
        perror("chdir");
        return 1;
    }

    printf("Now jailed inside /tmp/jail\n");
    printf("Can only access files under /tmp/jail\n");

    /* Any absolute path like /etc/passwd now means /tmp/jail/etc/passwd */
    FILE *f = fopen("/etc/passwd", "r");
    if (!f)
        printf("No /etc/passwd inside jail (expected)\n");

    return 0;
}

Interview Questions

Q1: What is the current working directory (CWD) of a process?
It is the directory from which relative pathnames (those not starting with /) are resolved. Every process has one, inherited from its parent. It can be changed with chdir() or fchdir().
Q2: Why is fchdir() preferred over getcwd()+chdir() for saving and restoring CWD?
fchdir() holds the directory open by its inode. Even if the directory is renamed or moved while you are away, fchdir() still returns you to the correct place. getcwd() stores a path string that may become invalid if the directory is renamed.
Q3: What is the purpose of openat() and when would you use it?
openat() opens a file relative to an open directory fd rather than the CWD. It avoids TOCTOU race conditions in multi-threaded code (because another thread might chdir() between your check and your open). It also lets threads maintain independent “virtual” working directories.
Q4: What does AT_FDCWD mean in openat()?
It is a special constant that means “use the current working directory”. openat(AT_FDCWD, “file.txt”, flags) is exactly equivalent to open(“file.txt”, flags).
Q5: What is a chroot jail and what are its limitations?
chroot() changes the process’s root directory so it cannot access the real filesystem above that point. Limitations: (1) must chdir(“/”) after chroot or CWD escapes the jail, (2) a privileged process can break out by creating device files or receiving fds via sockets, (3) not suitable as a security boundary for privileged programs.
Q6: What is the feature test macro needed to use openat()?
Either #define _XOPEN_SOURCE 700 or #define _POSIX_C_SOURCE 200809L before including fcntl.h.

Leave a Reply

Your email address will not be published. Required fields are marked *