最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c - `write(dev_null, addr...)` to check validity of virtual memory address `addr` using `errno=EFAULT` fails - Stack Overflow

programmeradmin3浏览0评论

My question is based on the solution suggested to a similar question.

Suppose I have an arbitrary virtual address. The solution asks to try writing to "/dev/null" with that virtual address as the input buffer. If the address is not valid, errno shall be set to EFAULT. To which I wrote the small code as a test:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int isMemoryMapped(void* addr) {
    int dev_null = open("/dev/null", O_WRONLY);
    if (dev_null == -1) {
        perror("Failed to open /dev/null");
        exit(EXIT_FAILURE);
    }

    errno = 0;
    // Attempt to write to /dev/null
    ssize_t result = write(dev_null, addr, sizeof(void*)); // <--- segfaults here 
                                                           // if the address is invalid 
    close(dev_null);
    return (result != -1 && errno != EFAULT); // Return 1 if accessible, 0 if not
}

int main() {
    int a = 10, b = 20, c = 30;
    void* ptrArr[] = {&a, &b, &c};

    printf("Starting address of ptrArr: %p\n", ptrArr);

    for (int i = 0; i < 1000000; i++) {
        if (isMemoryMapped(&ptrArr[i])) {
            printf("%d : Address %p is accessible, content = %p\n", i, &ptrArr[i], ptrArr[i]);
        } else {
            printf("%d : Address %p is NOT accessible\n", i, &ptrArr[i]);
            break;
        }
    }
    return 0;
}

But unfortunately the program segfaults while trying perform the write operation:

...
1197 : Address 0x7ffeea336fe8 is accessible, content = 0x6f6d656d5f656661
1198 : Address 0x7ffeea336ff0 is accessible, content = 0x6e6163735f7972
1199 : Address 0x7ffeea336ff8 is accessible, content = (nil)
Segmentation fault (core dumped)

Any suggestion on what might be going wrong?

However using mincore() works:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

int isMemoryMapped(void* addr) {
    // Align address to page boundary
    long pageSize = sysconf(_SC_PAGESIZE);
    void* pageAlignedAddr = (void*)((uintptr_t)addr & ~(pageSize - 1));

    unsigned char vec;
    if (mincore(pageAlignedAddr, pageSize, &vec) == 0) {
        return 1;  // Memory is mapped
    }
    return 0;  // Memory is NOT mapped
}

I am using Ubuntu 22.04.5 LTS x86_64 with 5.15.0-100-generic kernel.

My question is based on the solution suggested to a similar question.

Suppose I have an arbitrary virtual address. The solution asks to try writing to "/dev/null" with that virtual address as the input buffer. If the address is not valid, errno shall be set to EFAULT. To which I wrote the small code as a test:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int isMemoryMapped(void* addr) {
    int dev_null = open("/dev/null", O_WRONLY);
    if (dev_null == -1) {
        perror("Failed to open /dev/null");
        exit(EXIT_FAILURE);
    }

    errno = 0;
    // Attempt to write to /dev/null
    ssize_t result = write(dev_null, addr, sizeof(void*)); // <--- segfaults here 
                                                           // if the address is invalid 
    close(dev_null);
    return (result != -1 && errno != EFAULT); // Return 1 if accessible, 0 if not
}

int main() {
    int a = 10, b = 20, c = 30;
    void* ptrArr[] = {&a, &b, &c};

    printf("Starting address of ptrArr: %p\n", ptrArr);

    for (int i = 0; i < 1000000; i++) {
        if (isMemoryMapped(&ptrArr[i])) {
            printf("%d : Address %p is accessible, content = %p\n", i, &ptrArr[i], ptrArr[i]);
        } else {
            printf("%d : Address %p is NOT accessible\n", i, &ptrArr[i]);
            break;
        }
    }
    return 0;
}

But unfortunately the program segfaults while trying perform the write operation:

...
1197 : Address 0x7ffeea336fe8 is accessible, content = 0x6f6d656d5f656661
1198 : Address 0x7ffeea336ff0 is accessible, content = 0x6e6163735f7972
1199 : Address 0x7ffeea336ff8 is accessible, content = (nil)
Segmentation fault (core dumped)

Any suggestion on what might be going wrong?

However using mincore() works:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

int isMemoryMapped(void* addr) {
    // Align address to page boundary
    long pageSize = sysconf(_SC_PAGESIZE);
    void* pageAlignedAddr = (void*)((uintptr_t)addr & ~(pageSize - 1));

    unsigned char vec;
    if (mincore(pageAlignedAddr, pageSize, &vec) == 0) {
        return 1;  // Memory is mapped
    }
    return 0;  // Memory is NOT mapped
}

I am using Ubuntu 22.04.5 LTS x86_64 with 5.15.0-100-generic kernel.

Share Improve this question edited Mar 30 at 17:01 Abhishek Ghosh asked Mar 30 at 16:56 Abhishek GhoshAbhishek Ghosh 6959 silver badges24 bronze badges 4
  • 1 Relevant: stackoverflow/questions/5029632/… Bottom line: /dev/null is special and may or may not work for in this application. A regular file has a better chance to work, but it's not guaranteed either. It may work with one kernel and not in another. – n. m. could be an AI Commented Mar 31 at 3:32
  • 1 Try writing to a pipe, as suggested in stackoverflow/a/7138421/5264491 . Writing to a pipe needs to attempt to read the memory, whereas writing to /dev/null might not bother reading the memory. The Linux kernel hasn't bothered reading the memory for writes to /dev/null since at least kernel version 2.0.1. (I didn't check earlier kernels.) – Ian Abbott Commented Mar 31 at 15:45
  • 1 Interestingly, when I run the code under GDB, it reaches the break; statement in main() and exits normally. – Ian Abbott Commented Mar 31 at 16:07
  • 1 Also setarch $(uname -m) -R ./program ran normally (where ./program is the executable). This disables ASLR, which puts the stack at the top of the user-mode virtual address space. – Ian Abbott Commented Mar 31 at 16:22
Add a comment  | 

2 Answers 2

Reset to default 2

In C, you can't "test" whether an arbitrary pointer is valid by simply passing it to a system call.

When you do:

ssize_t result = write(dev_null, addr, sizeof(void *));

The kernel tries to read sizeof(void *) bytes starting at addr in your process's memory. If addr is totally out of range or otherwise invalid the kernel can't even copy from that userspace pointer into its internal buffer. It triggers a page fault at the CPU level, which ends up delivering a SISSEGV to your process. Your process dies before the write() call can return with errno = EFAULT.

Also, in your loop:

for (int i = 0; i < 1000000; i++) {
    if (isMemoryMapped(&ptrArr[i])) {
        ...
    }
    else {
        ...
        break;
    }
}

you're iterating way past the end of ptrAddr, which only has 3 elements (&a, &b, &c).

If you really need to check for "mappable" memory, you can use proc/self/maps, parsing your own memory maps to see which address ranges are "valid" for your process.

In practice, though, it's usually best to avoid code that tries to test if a pointer is valid. If it's under your program's control, you should already know if it's valid.

On Linux, The write system call will eventually call the vfs_write() function in the kernel (in "fs/read_write.c"). It will call access_ok() on the user space address region to verify that it lies within the permissible range for user-space addresses, but it does not check that the region is mapped. If the access_ok() check fails, it will fail with an EFAULT error.

If the access_ok() check passes, it checks that the file region being written is valid. If all is OK, it calls either the write or write_iter file operation handler for the file. For the /dev/null file, the function called is write_null() in "drivers/char/mem.c". The only thing that function does is return the length of the region being written. It does not access the user-space memory.

I think the SIGSEGV signal is being raised during the call to printf(). If isMemoryMapped() returns 1 incorrectly for an unmapped address, the evaluation of the argument expression ptrArr[i] will dereference the pointer, which will cause the signal to be raised.

One thing that I noticed is that the program runs OK under the GDB debugger (which makes debugging the problem tricky!), but that seems to be because it disabled randomization of the virtual address space (i.e. disabled ASLR), so that the stack was mapped to the top of the user-space portion of the virtual address space. This caused the access_ok() check in the kernel's vfs_write() function to fail with the EFAULT error. The same thing occurs when ASLR is disabled by other means, for example when the program is run via setarch $(uname -m) -R ./progname (where ./progname is the executable under test).

This answer suggests writing to a dummy pipe() file descriptor, but does not supply example code. Here is a modified version of isMemoryMapped() that uses a temporary pipe. It seems to work for me:

int isMemoryMapped(void* addr) {
    int save_errno = errno;
    int pipefd[2];
    // Create pipe
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    // Attempt to write to pipe
    ssize_t result = write(pipefd[1], addr, 1);
    int result_errno = errno;
    // Close pipe
    close(pipefd[0]);
    close(pipefd[1]);
    errno = save_errno;
    // Return 1 if accessible, 0 if not
    return (result != -1 || result_errno != EFAULT);
}

Note that this only tests if the address is readable, not writeable, and would not be good for addresses that have been mmap()ed to memory mapped I/O hardware registers due to possible nasty side-effects.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论