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 |2 Answers
Reset to default 2In 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.
/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:45break;
statement inmain()
and exits normally. – Ian Abbott Commented Mar 31 at 16:07setarch $(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