1

I need to unlink an empty directory with

unlinkat(dir_fd, ".", AT_REMOVEDIR)

and I am getting EINVAL errno, which, according to official GNU documentation, must mean "An invalid flag value was specified in flags", which is not the case, as AT_REMOVEDIR is (the only) allowed flag.

SSCCE is as follows. Note, that in the simplest code, I do have the directory name, so I could have used rmdir. In the real situation, I don't have the name, I only have the descriptor, to an empty directory, and I need to remove it - so I have to use unlinkat .

foobar.c:

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    int dir_fd =
        open("dir", O_RDONLY | O_PATH | O_DIRECTORY);

    if (-1 == dir_fd) {
        perror("open");
        return(1);
    }
    
    if (unlinkat(dir_fd, ".", AT_REMOVEDIR)) {
        perror("unlinkat");
        fprintf(stderr, "errno %d\n", errno);
        return(1);
    }
}

and I get this behaviour:

$mkdir dir
$gcc -lc foobar.c
$./a.out
unlinkat: Invalid argument
errno 22
$errno 22
EINVAL 22 Invalid argument

and GNU documentation for unlinkat says

EINVAL An invalid flag value was specified in flags.

Mark Galeck
  • 6,155
  • 1
  • 28
  • 55
  • 3
    https://linux.die.net/man/2/rmdir `EINVAL pathname has . as last component.` – tkausl Aug 06 '23 at 00:36
  • @tkausl so then I have to get the name (it's gonna be a major pain in the a*s) and use `rmdir` , right? No other way? – Mark Galeck Aug 06 '23 at 00:42
  • @tkausl basically, I forgot about the cardinal rule: if something appears to not follow the documentation as read in ordinary fashion, read it more carefully then. – Mark Galeck Aug 06 '23 at 01:00

1 Answers1

3

When called with AT_REMOVEDIR, unlinkat(2) behaves as rmdir(2), and ultimately fails to the same reasons.

For rmdir(2):

EINVAL pathname has . as last component.

You have seemingly flipped the interface. Looking at your example in isolation, it should be

/* foobar.c */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
    int dir_fd = open(".", O_RDONLY | O_PATH | O_DIRECTORY);

    if (-1 == dir_fd) {
        perror("open");
        return(1);
    }

    if (unlinkat(dir_fd, "dir", AT_REMOVEDIR)) {
        perror("unlinkat");
        fprintf(stderr, "errno %d\n", errno);
        return(1);
    }

    close(dir_fd);
}
$ mkdir dir
$ ./a.out

where a relative pathname given to unlinkat is resolved relative to the directory referred to by dirfd.

(Or the special value of AT_FDCWD could be used for the first argument of unlinkat to remove a file relative to the current working directory.)

This particular example is obviously a rather complex way of doing the same thing rmdir("dir") does, and it certainly does not help you if you do not know the name of the directory to be removed.

For your actual problem: On Linux, you can try something along the lines of the following example, that uses readlink(2) to read /proc/self/fd/NNN, where NNN is the file descriptor (for which you do not have a name), in order to retrieve a pathname for rmdir.

(See: Retrieve filename from file descriptor in C)

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int mystery_directory(void)
{
    char dir[] = "XXXXXX";

    if (!mkdtemp(dir))
        return -1;

    return open(dir, O_RDONLY | O_PATH | O_DIRECTORY);
}

int main(void)
{
    /* We have magically arrived at this file descriptor */
    int dir_fd = mystery_directory();

    if (-1 == dir_fd) {
        perror("mystery_directory");
        return EXIT_FAILURE;
    }

    char fdrl[128];
    int len = snprintf(fdrl, sizeof fdrl, "/proc/self/fd/%d", dir_fd);

    if (len < 0 || len >= sizeof fdrl) {
        fprintf(stderr, "Something has gone very wrong.\n");
        return EXIT_FAILURE;
    }

    printf("procfs entry: <%s>\n", fdrl);

    char path[4096];
    ssize_t r = readlink(fdrl, path, sizeof path - 1);

    if (r < 0) {
        perror("readlink");
        return EXIT_FAILURE;
    }

    close(dir_fd);
    path[r] = 0;

    printf("Attempting to remove directory: \n\t%s\n", path);

    if (-1 == rmdir(path)) {
        perror("rmdir");
        return EXIT_FAILURE;
    }

    puts("Success!");
}
$ pwd
/home/so
$ ./a.out
procfs entry: </proc/self/fd/3>
Attempting to remove directory: 
    /home/so/IOgkes
Success!
Oka
  • 23,367
  • 6
  • 42
  • 53
  • I accepted it and yet I downvoted your answer - the reason is, I clearly stated that I don't have the name, so your sample code is pointless (even in more ways than one). Delete it and I will vote it back up :) – Mark Galeck Aug 06 '23 at 00:56
  • OK, I see, I knew about the `/proc` thing, but not precisely, I thought it was going to be painful too, but seems OK... – Mark Galeck Aug 06 '23 at 01:08