0

After a major OS upgrade this C code behaviour has changed:

...
if ((fd = open(argv[1], O_RDWR | O_SYNC)) == -1)
    FATAL;
printf("character device %s opened.\n", argv[1]);
fflush(stdout);

/* map one page */
map_base = mmap(0xe0000000, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_base == (void *)-1)
    FATAL;
printf("Memory mapped at address %p.\n", map_base);
...

With a binary inherited from an old OS, "old mmap" returns a virtual address 0x7fb20d725000. If I rebuild the same C file on a new OS, it returns 0xe0000000 which seems to be a physical, and subsequent code - which uses this returned address - now fails with a segmentation fault.

How to force mmap to work as before without downgrading the OS or using old binary? Any modern flags for gcc or mmap itself?

Run a code example below with sudo ./test /dev/zero 0x01000000 : (/dev/zero instead of a real device gives the same results)

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <byteswap.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
/* ltoh: little to host */
/* htol: little to host */
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define ltohl(x)       (x)
#define ltohs(x)       (x)
#define htoll(x)       (x)
#define htols(x)       (x)
#elif __BYTE_ORDER == __BIG_ENDIAN
#define ltohl(x)     __bswap_32(x)
#define ltohs(x)     __bswap_16(x)
#define htoll(x)     __bswap_32(x)
#define htols(x)     __bswap_16(x)
#endif

#define FATAL do { fprintf(stderr, "Error at line %d, file %s (%d) [%s]\n", __LINE__, __FILE__, errno, strerror(errno)); exit(1); } while(0)

#define MAP_SIZE (16*1024*1024UL)
#define MAP_MASK (MAP_SIZE - 1)

int main(int argc, char **argv)
{
    int fd;
    void *map_base, *virt_addr;
    uint32_t read_result, writeval;
    off_t target;
    char *device;
    if (argc != 3) {
        fprintf(stderr,
            "\nUsage:\t%s <device> <address> [[type] data]\n"
            "\tdevice  : character device to access\n"
            "\taddress : memory address to access\n\n",
            argv[0]);
        exit(1);
    }

    device = strdup(argv[1]);
    target = strtoul(argv[2], 0, 0);
    fprintf("argc = %d, device: %s, address: 0x%08x\n", argc, device, (unsigned int)target);

    if ((fd = open(argv[1], O_RDWR | O_SYNC)) == -1)
        FATAL;
    fprintf(stdout, "character device %s opened.\n", argv[1]);
      fflush(stdout);

    /* map one page */
    map_base = mmap(0xe0000000, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map_base == (void *)-1)
        FATAL;
    fprintf(stdout, "Memory mapped at address %p.\n", map_base);
      fflush(stdout);

    /* calculate the virtual address to be accessed */
    virt_addr = map_base + target;
    /* read only */
    read_result = *((uint32_t *) virt_addr);
    /* swap 32-bit endianess if host is not little-endian */
    read_result = ltohl(read_result);
    printf("Read 32-bit value at address 0x%08x (%p): 0x%08x\n",
                (unsigned int)target, virt_addr, (unsigned int)read_result);

    if (munmap(map_base, MAP_SIZE) == -1)
        FATAL;
    close(fd);
    return 0;
}
qmastery
  • 77
  • 1
  • 10
  • 1
    Are you changing anything in the code ? Such as headers or the way you are compiling? Please create a [mcve]. Also I am curious how `FATAL` is defined – Eugene Sh. May 11 '20 at 16:10
  • 1
    You will have to be a lot more specific than this. How are you using the page after mapping it? *What* fails with segmentation fault? Why are you mapping it at that address specifically in the first place? What architecture are you on? I suppose x86 64 bit, is that right? Also, that's definitely *not* a physical address. Please provide a minimal example code that can be compiled to reproduce the problem. – Marco Bonelli May 11 '20 at 16:15
  • @EugeneSh. I'm not changing anything yet, just trying to rebuild the old code with the same behaviour. A reproducible example code is also ready (FATAL just helps to print the error message). – qmastery May 11 '20 at 16:24
  • 1
    Please reduce the code. It is definitely not minimal – Eugene Sh. May 11 '20 at 16:25
  • @EugeneSh. reduced in 3 times (hard to compress it more) – qmastery May 11 '20 at 16:34
  • @MarcoBonelli Using writes/reads to the virtual addresses within the mapped area, which get internally mirrored to their real equivalent. Yes, arch is x86_64. Redid the example. – qmastery May 11 '20 at 16:36
  • 1
    @qmastery your code looks and works fine, apart from the `fprintf` which takes a file as first argument, and `mmap` where you should cast the address to `void *`. I don't see any obvious error. Are you sure that running it causes a segmentation fault? If that's the case it would probably also be helpful to add your kernel version (`uname -r`). – Marco Bonelli May 11 '20 at 16:38
  • @MarcoBonelli Noticed this problem doesn't happen with a low addresses. Please try a `0x01000000` : it works with old binary but fails with a new one. `sudo ./test /dev/zero 0x01000000` . Kernel ver- 4.15, earlier it was 3.10 or even older. – qmastery May 11 '20 at 16:47
  • 2
    @qmastery well of course it happens if you are mapping `16 * 1024 * 1024` (which is `0x01000000`) and then you try to read something beyond that (i.e. any offset higher than or equal to `0x01000000`, which is just outsize the mapped area). What do you expect? Of course it will cause a segmentation fault. There isn't really much to discuss... – Marco Bonelli May 11 '20 at 16:54
  • @MarcoBonelli I see! Still curious: why the old binary works (it mmaps more than we asked?), and why a new mmap's virtual address is identical to a physical one? – qmastery May 11 '20 at 16:56
  • 2
    @qmastery first of all *"a new mmap's virtual address is identical to a physical one"* literally doesn't mean *anything*. That's a virtual address, period. There is no direct correlation between a given virtual address and another given physical address. That is not a physical address. Secondly, I have no idea why the "old" binary works, I cannot say for sure if you don't show the code, but it is most probably because a bigger area was mapped. Third, there is no need to map at `0xe...`. Fourth, there is no need to map *everything* from the start. You can map with an offset. – Marco Bonelli May 11 '20 at 16:59

1 Answers1

2

You seem to be confusing virtual and physical addresses. User programs usually only work with virtual addresses. The mmap syscall accepts an hint as first argument: a desired virtual address for the requested mapped area. See man 2 mmap for more information.

What was most likely happening with your previous program was that the call to mmap was probably something like:

map_area = mmap(NULL, /* same arguments here */);

This way, the operating system will choose an appropriate address and return it.

What you are doing in the new program instead, is letting the OS know that you would prefer a specific address (0xe...), and the OS will map memory at that address if possible (very likely). You really shouldn't need this, the program works regardless of the position of the mapped area, but in any case you can keep it.

The reason why you are getting a segmentation fault is because you are mapping an area of 16 * 1024 * 1024 bytes (0x01000000), but then you are accessing memory at an higher offset than the specified size (target >= 0x01000000).

The correct way to do what you are trying to do is to use the offset argument of mmap to request a map that starts at an appropriate offset in the file. Requesting a mapping of two pages starting at that offset will ensure that what you want to read or write will be correctly mapped (assuming the file is big enough, otherwise MAP_FAILED will be returned).

Here's how it should be done:

offset = target & 0xFFFFFFFFFFFFF000; // align target to page size

// Map two pages starting at 0xe... and corresponding to the calculated offset in the file.
map_base = mmap((void *)0xe0000000, 0x1000 * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, offset);

// ...

virt_addr = map_base + (target & 0xfff); // cut target to get offset within the mapped pages
read_result = *((uint32_t *) virt_addr);
read_result = ltohl(read_result);

printf("Read 32-bit value at address 0x%08x (%p): 0x%08x\n",
            (unsigned int)target, virt_addr, (unsigned int)read_result);
Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128