Home > Net >  Why memory mapping to `STDOUT_FILENO` failed?
Why memory mapping to `STDOUT_FILENO` failed?

Time:11-22

I have written some code to test the mmap system call.

Here I want to map the virtual memory address space to the STDOUT, and print a string via the pointer ptr that returned by the mmap.

int main()
{
    void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_PRIVATE, STDOUT_FILENO, 0);
    memcpy(ptr, "hello", 6);
}

But this code failed:

$ gcc mmap.c
$ ./a.out
Segmentation fault (core dumped)

And I have tested mmap on STDIN, it is ok.

int main()
{
    // executed by `./a.out < text.txt`
    void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_PRIVATE, STDIN_FILENO, 0);
    write(STDOUT_FILENO, ptr, 1024); 
}

Why mmap on STDOUT failed here? And what are the differences between mmap on STDIN and STDOUT ?

CodePudding user response:

As a general rule, when you use services, check the error codes to narrow down the problem. Re-writing your program as follow:

#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_PRIVATE, STDOUT_FILENO, 0);
    if (MAP_FAILED == ptr) {
      fprintf(stderr, "mmap(): error '%m' (%d)\n", errno);
      return 1;
    } else {
      memcpy(ptr, "hello", 6);
    }

    return 0;
}

The program displays the following:

$ ./a.out
mmap(): error 'No such device' (19)

That is to say that you get the ENODEV error code. Looking at the manual, you get the following explanation:

ENODEV The underlying filesystem of the specified file does not support memory mapping.

Actually, the file descriptor is pointing on the terminal device driver and the latter does not allow the mmap() operation.

Concerning the second program mapping STDIN_FILENO, when you run it like that:

$ ./a.out < text.txt

The preceding makes STDIN_FILENO "point" on the text.txt file, not the input terminal. Hence, mmap() works here...

So, with the same idea, we expect that the first program works with:

$ ./a.out > foo.txt

Since the output is no longer a terminal but a file. But you get the EACCES error:

$ ./a.out > foo.txt
mmap(): error 'Permission denied' (13)

The answer is explained in this post. A file must be opened with the read access to be mapped but the standard output of a program launched by the shell is opened O_WRONLY. Hence, the mmap() fails. Under Linux, it is possible to use a trick. The file descriptors are symbolic links in /proc/pid/fd directory. So, it is possible to reopen the file pointed by the symbolic link /proc/pid/fd/1 with O_RDWR flag and use the famous close()/dup() trick to make the file descriptor number 1 (STDOUT_FILENO) point on this newly opened file. A call to ftruncate() is necessary to reserve space in the file and MAP_SHARED flag is required to make visible the modifications to other processes (e.g. the shell when the program terminates):

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

int main(void)
{
    char symlink[256];
    int fd;

    snprintf(symlink, sizeof(symlink), "/proc/%d/fd/1", getpid());
    fd = open(symlink, O_RDWR);
    if (fd < 0) {
      fprintf(stderr, "fopen(): error '%m' (%d)\n", errno);
      return 1;
    }
    close(STDOUT_FILENO);
    dup(fd);
    close(fd);
    ftruncate(STDOUT_FILENO, 1024);
    void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_SHARED, STDOUT_FILENO, 0);
    if (MAP_FAILED == ptr) {
      fprintf(stderr, "mmap(%d): error '%m' (%d)\n", fd, errno);
      return 1;
    } else {
      memcpy(ptr, "hello", 6);
    }

    return 0;
}

Hence, you get what you expect:

$ ./a.out
mmap(3`): error 'No such device' (19) # The output is the terminal (mmap() forbidden)
$ ./a.out > foo.txt   # The output is a file
$ cat foo.txt
hello$

CodePudding user response:

The difference is not stdin vs stdout

It is between a regular disk file descriptor and some pseudo-tty file descriptor. See stat(2) (more precisely fstat) and inode(7).

As regular file (of type S_IFREG) can be lseek(2)-ed and the Linux kernel is doing some equivalent when fetching a virtual memory page with mmap.

A socket or pseudo-tty cannot be lseek-ed.

You could try echo foo | a.out and it will also fail, since a pipe(7) cannot be lseek-ed.

Of course, you should read the documentation of mmap(2). It can fail, and you should use errno(3) or perror(3) on failure.

So code instead:

void *ptr = 
   mmap(NULL, 1024, PROT_WRITE | PROT_READ, 
        MAP_PRIVATE, STDIN_FILENO, 0);
if (ptr == MMAP_FAILED) 
   { perror("mmap"); exit(EXIT_FAILURE); }

Try also strace(1) on existing command-line programs to learn more about syscalls(2)

  • Related