Today I Learned: openat()
Tags: c, linux, syscall
Published: 2021-08-28 17:53:27

> This post is originally published on yoursunny.com blog https://yoursunny.com/t/2021/openat/

## fopen and open

In C programming language, the `<stdio.h>` header supplies functions for file input and output.
To open a file, we usually use the [fopen](https://en.cppreference.com/w/c/io/fopen) function.
It is defined by the C language standard and works in every operating system.

Working at a lower level, there's also the [open](https://man7.org/linux/man-pages/man2/open.2.html) function.
It is a system call provided by the Linux kernel and exposed through glibc.

Both `fopen` and `open` have an input parameter: the file pathname, as a NUL-terminated string.
These two functions are declared like this:

```c
FILE* fopen(const char* filename, const char* mode);

int open(const char* pathname, int flags);
```

If the file we want to access is in the [current working directory](https://en.wikipedia.org/wiki/Working_directory), or we have the full pathname of the file as a string, this is easy to use.
However, sometimes we want to access a file relative to another directory, and the above API isn't so easy to use.

## Directory Path + Filename

One such occasion is in my [NDNph](https://yoursunny.com/t/2020/NDNph-intro/) library: I wanted to use a directory in the filesystem as a persistent key-value store, where object keys are used as filenames, and object value is written as file content.
The API for this key-value store looks like this:

```c
typedef struct KV KV;

/**
 * @brief Open a key-value store at specified path
 * @param[out] kv key-value store object
 * @param dir directory pathname
 * @return whether success
 */
bool KV_Open(KV* kv, const char* dir);

/**
 * @brief Write @p value to file @p dir "/" @p key
 * @param kv key-value store object
 * @param key filename
 * @param value file content
 * @param size size of file content
 * @return whether success
 */
bool KV_Save(KV* kv, const char* key, const uint8_t* value, size_t size);
```

Since `fopen` and `open` want the file pathname as a single string, I have to concatenate `dir` and `key` in `KV_Save`.
This in turn requires saving a copy of `dir` in `KV_Open` function.

```c
typedef struct KV
{
  char* dir;
} KV;

bool KV_Open(KV* kv, const char* dir)
{
  struct stat st;
  if (stat(dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
    return false;
  }
  kv->dir = strdup(dir);
  return true;
}

bool KV_Save(KV* kv, const char* key, const uint8_t* value, size_t size)
{
  char pathname[PATH_MAX];
  int res = snprintf(pathname, sizeof(pathname), "%s/%s", kv->dir, key);
  if (res < 0 || res >= sizeof(pathname)) {
    return false;
  }

  int fd = open(pathname, O_WRONLY | O_CREAT, 0644);
  if (fd < 0) {
    return false;
  }

  // TODO write and close file
}
```

## openat

This week, I came across a new function: [openat](https://man7.org/linux/man-pages/man2/openat.2.html).
It operates in the same way as `open`, except that it supports specifying a *relative* pathname interpreted relative to another directory, which is represented by a file descriptor.

The function signature of **openat** is:

```c
int openat(int dirfd, const char* pathname, int flags);
```

This allows me to simplify the key-value store:

```c
typedef struct KV
{
  int dirfd;
} KV;

bool KV_Open(KV* kv, const char* dir)
{
  kv->dirfd = open(dir, O_RDONLY | O_DIRECTORY);
  return kv->dirfd >= 0;
}

bool KV_Save(KV* kv, const char* key, const uint8_t* value, size_t size)
{
  int fd = openat(kv->dirfd, key, O_WRONLY | O_CREAT, 0644);
  if (fd < 0) {
    return false;
  }

  // TODO write and close file
}
```

`KV_Open` opens the directory as a file descriptor with the `open` function.
The `O_DIRECTORY` flag ensures we are opening a directory instead of a regular file.
It's no longer necessary to save a copy of the directory path.

`KV_Save` calls `openat` with the directory file descriptor and the filename `key`.
It's no longer necessary to perform string concatenation.
The code is 5 lines shorter than the `open`-based solution.

## Conclusion and Code Download

This article introduces Linux [openat](https://man7.org/linux/man-pages/man2/openat.2.html) syscall that I recently discovered.
The `openat` function enables resolving a filename or relative path, relative to another directory that is not the current working directory.
It can do so without requiring manual string concatenation.

Code samples (whole program including load/save/delete functions):

* [open.c](https://yoursunny.com/t/2021/openat/open.c): key-value store implemented with `open`
* [openat.c](https://yoursunny.com/t/2021/openat/openat.c): key-value store implemented with `openat`
* [view on GitHub Gist](https://gist.github.com/yoursunny/530750611f951941886ea82160c1927d)

Caution: this proof-of-concept code assumes that `key` is a valid filename.
It cannot safely handle untrusted and potentially malicious input.