Storing Pointers in Shared Memory Why Absolute Pointers Break โ€” The Correct Offset-Based Approach

 

Storing Pointers in Shared Memory
Why Absolute Pointers Break โ€” The Correct Offset-Based Approach
๐Ÿ“‚ Section 48.6
โš ๏ธ Common Pitfall
๐ŸŽฏ Interview Ready

The Core Problem

When you build a linked list, binary tree, or any pointer-based structure inside a shared memory segment, you naturally want to store pointer values. But here is the trap: the kernel may attach the same shared memory segment at different virtual addresses in different processes. An absolute pointer (a real memory address) stored by process A is meaningless in process B.

The solution is simple: store offsets (distance in bytes from the start of the segment) instead of absolute addresses.

Key Terms in This Topic

absolute pointer relative offset baseaddr shmat() virtual address space ptrdiff_t linked list in shm index-based reference

Why shmat() Returns Different Addresses in Different Processes

When you call shmat(shmid, NULL, 0), the NULL second argument means “kernel, you choose the address”. The kernel scans the calling process’s virtual address space for a free region big enough to fit the segment. Since each process has its own independent layout โ€” its own stack, heap, shared libraries loaded at varying positions, and prior mappings โ€” the free region found for process A will commonly differ from the one found for process B.

Process A (PID 100)
0xFFFF0000 [stack]
0xB7E00000 โ† baseaddr_A
libc.so
0x08048000 [text]
โŸบ
Same physical
memory
Process B (PID 200)
0xFFFF0000 [stack]
0xC0010000 extra_lib.so
0xB6F00000 โ† baseaddr_B
libc.so
0x08048000 [text]
The bug: The segment starts at 0xB7E00000 in Process A and at 0xB6F00000 in Process B. If Process A stores the absolute address 0xB7E00100 (pointing 256 bytes into the segment), Process B reads that value and tries to dereference 0xB7E00100 โ€” which in Process B’s address space is not inside the shared segment. Result: crash or silent data corruption.

The Wrong Way โ€” Absolute Pointer in Shared Memory

/*
 * WRONG โ€” storing absolute pointers inside shared memory.
 * This works only if both processes happen to attach at the same address,
 * which is never guaranteed.
 */
#include <stdio.h>
#include <sys/shm.h>

struct BadNode {
    int             value;
    struct BadNode *next;    /* absolute pointer โ€” DANGEROUS in shm */
};

int main(void)
{
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
    struct BadNode *base = shmat(shmid, NULL, 0);

    struct BadNode *node1 = base;
    struct BadNode *node2 = base + 1;

    node1->value = 10;
    node2->value = 20;

    /*
     * WRONG: stores the absolute virtual address of node2.
     * In this process node2 might be at 0xB7E00010.
     * In another process the same physical memory could be at 0xB6F00010.
     * node1->next = 0xB7E00010 is garbage in the other process.
     */
    node1->next = node2;   /* WRONG */

    printf("node1->next = %p  (absolute โ€” wrong in another process)\n",
           (void *)node1->next);

    shmctl(shmid, IPC_RMID, NULL);
    shmdt(base);
    return 0;
}
    

The Correct Way โ€” Storing Relative Offsets

Instead of storing the target’s absolute address, store the distance (in bytes) from the start of the shared memory segment. Every process knows where its own baseaddr is, so every process can reconstruct the correct real pointer from the same offset value.


/*
 * CORRECT โ€” store offsets, not absolute pointers.
 *
 *   To store:   *p = (target - baseaddr);
 *   To restore: real_ptr = baseaddr + *p;
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>

struct GoodNode {
    int   value;
    long  next_offset;    /* bytes from segment base; -1 means no next */
};

int main(void)
{
    int   shmid;
    char *baseaddr;

    shmid   = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
    baseaddr = shmat(shmid, NULL, 0);   /* baseaddr = start of shm in THIS process */

    struct GoodNode *node1 = (struct GoodNode *)(baseaddr + 0);
    struct GoodNode *node2 = (struct GoodNode *)(baseaddr + sizeof(struct GoodNode));

    node1->value = 10;
    node2->value = 20;
    node2->next_offset = -1;   /* last node */

    /*
     * Store the OFFSET of node2 from the segment base.
     * Offset = (char *)node2 - baseaddr = sizeof(struct GoodNode)
     * This value is the same number regardless of where the segment is attached.
     */
    node1->next_offset = (char *)node2 - baseaddr;

    printf("[Parent] baseaddr=%p  node1 offset=0  node2 offset=%ld\n",
           baseaddr, node1->next_offset);

    /* Fork โ€” child attaches the SAME segment independently */
    if (fork() == 0) {
        /* Child calls shmat() โ€” may get a DIFFERENT virtual address */
        char *child_base = shmat(shmid, NULL, 0);

        printf("[Child]  child_base=%p  (may differ from parent's baseaddr)\n",
               child_base);

        /* Walk the list using offset arithmetic */
        struct GoodNode *cur = (struct GoodNode *)child_base;
        int step = 0;
        while (cur) {
            printf("[Child]  node[%d].value = %d\n", step++, cur->value);
            if (cur->next_offset == -1) break;
            /* Reconstruct real pointer using child's own baseaddr */
            cur = (struct GoodNode *)(child_base + cur->next_offset);
        }

        shmdt(child_base);
        exit(0);
    }

    wait(NULL);
    shmctl(shmid, IPC_RMID, NULL);
    shmdt(baseaddr);
    return 0;
}
/*
 * Compile: gcc -o offset_demo offset_demo.c
 * Output (addresses vary):
 * [Parent] baseaddr=0xb7e40000  node1 offset=0  node2 offset=16
 * [Child]  child_base=0xb7e30000  (may differ from parent's baseaddr)
 * [Child]  node[0].value = 10
 * [Child]  node[1].value = 20
 *
 * Correct even though child's base differs from parent's.
 */
    

Offset Arithmetic โ€” Step by Step

baseaddr
(offset 0)
. . . data . . . target node
(offset = 256)
. . . data . . .
โ†โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” 256 bytes (the stored offset value) โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ†’

/* ---------- STORING (write side) ---------- */

char *baseaddr = shmat(shmid, NULL, 0);   /* start of shm in THIS process */
char *target   = baseaddr + 256;          /* some node 256 bytes into the segment */
long *p        = (long *)(baseaddr + 128);/* location where we store the reference */

/* Store the offset โ€” not the absolute address */
*p = target - baseaddr;        /* *p = 256 */

/* NEVER do this: */
/* *p = (long)target;          stores 0xB7E00100 โ€” useless in another process */


/* ---------- RESTORING (read side) ---------- */

/* In another process, baseaddr may be different */
char *baseaddr2 = shmat(shmid, NULL, 0);  /* e.g. 0xB6F00000 */

long *p2 = (long *)(baseaddr2 + 128);     /* same relative position, different absolute addr */

/* Reconstruct the correct pointer using THIS process's base */
char *target2 = baseaddr2 + *p2;          /* baseaddr2 + 256 โ€” correct! */


/* ---------- SUMMARY ---------- */
/*   Store:   offset   = (target  - baseaddr)
 *   Restore: real_ptr = (baseaddr + offset)
 *
 *   The offset (256) is the same number in all processes.
 *   The real_ptr differs per process โ€” and that is correct and expected.
 */
    

Alternative: Array Index References for Fixed-Size Structures

When all nodes are the same size, you can skip offset arithmetic entirely and use integer array indices as references. Index 3 always means array[3] regardless of where in virtual memory array starts.


#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>

#define MAX_NODES 64

struct IndexNode {
    int value;
    int next_index;    /* array index of next node; -1 = end */
};

struct ShmPool {
    int             head;              /* index of first node, -1 = empty */
    int             free_head;         /* index of first free slot */
    struct IndexNode nodes[MAX_NODES];
};

int main(void)
{
    int shmid = shmget(IPC_PRIVATE, sizeof(struct ShmPool), IPC_CREAT | 0660);
    struct ShmPool *pool = shmat(shmid, NULL, 0);

    /* Initialise free list */
    pool->head      = -1;
    pool->free_head =  0;
    for (int i = 0; i < MAX_NODES - 1; i++)
        pool->nodes[i].next_index = i + 1;
    pool->nodes[MAX_NODES - 1].next_index = -1;

    /* Insert three nodes (prepend) */
    for (int v = 1; v <= 3; v++) {
        int idx = pool->free_head;
        pool->free_head = pool->nodes[idx].next_index;
        pool->nodes[idx].value      = v * 10;
        pool->nodes[idx].next_index = pool->head;
        pool->head                  = idx;
    }

    /* Walk the list */
    printf("List: ");
    for (int i = pool->head; i != -1; i = pool->nodes[i].next_index)
        printf("%d ", pool->nodes[i].value);
    printf("\n");

    /*
     * In any other process that attaches this same segment:
     *   struct ShmPool *p2 = shmat(shmid, NULL, 0);
     * Index 0, 1, 2 mean the same thing relative to p2 โ€” no absolute address trouble.
     */

    shmctl(shmid, IPC_RMID, NULL);
    shmdt(pool);
    return 0;
}
/* Output: List: 30 20 10 */
    

Code: Binary Search Tree in Shared Memory Using Offsets

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>

typedef struct {
    int  value;
    long left_off;    /* offset from shm base; -1 = no child */
    long right_off;
} BSTNode;

#define MAX_NODES 32

typedef struct {
    long    root_off;            /* offset to root; -1 = empty */
    int     count;
    BSTNode nodes[MAX_NODES];
} BST;

/* Macros to convert between offset and pointer */
#define OFF2PTR(base, off) \
    ((off) == -1L ? NULL : (BSTNode *)((char *)(base) + (off)))
#define PTR2OFF(base, ptr) \
    ((long)((char *)(ptr) - (char *)(base)))

static void insert(BST *tree, int val)
{
    if (tree->count >= MAX_NODES) return;

    BSTNode *n  = &tree->nodes[tree->count++];
    n->value    = val;
    n->left_off = n->right_off = -1;
    long noff   = PTR2OFF(tree, n);

    if (tree->root_off == -1) { tree->root_off = noff; return; }

    BSTNode *cur = OFF2PTR(tree, tree->root_off);
    for (;;) {
        if (val < cur->value) {
            if (cur->left_off  == -1) { cur->left_off  = noff; break; }
            cur = OFF2PTR(tree, cur->left_off);
        } else {
            if (cur->right_off == -1) { cur->right_off = noff; break; }
            cur = OFF2PTR(tree, cur->right_off);
        }
    }
}

static void inorder(BST *tree, long off)
{
    if (off == -1) return;
    BSTNode *n = OFF2PTR(tree, off);
    inorder(tree, n->left_off);
    printf("%d ", n->value);
    inorder(tree, n->right_off);
}

int main(void)
{
    int shmid = shmget(IPC_PRIVATE, sizeof(BST), IPC_CREAT | 0660);
    BST *tree  = shmat(shmid, NULL, 0);
    tree->root_off = -1;
    tree->count    =  0;

    int vals[] = {50, 30, 70, 20, 40, 60, 80};
    for (int i = 0; i < 7; i++) insert(tree, vals[i]);

    printf("[Parent] In-order: ");
    inorder(tree, tree->root_off);
    printf("\n");

    if (fork() == 0) {
        BST *t2 = shmat(shmid, NULL, 0);  /* may attach at different address */
        printf("[Child]  In-order: ");
        inorder(t2, t2->root_off);
        printf("\n");
        shmdt(t2);
        exit(0);
    }
    wait(NULL);

    shmctl(shmid, IPC_RMID, NULL);
    shmdt(tree);
    return 0;
}
/*
 * Output:
 * [Parent] In-order: 20 30 40 50 60 70 80
 * [Child]  In-order: 20 30 40 50 60 70 80
 */
    

Summary: Three Approaches Compared
Approach Safe? Best For Notes
*p = target โŒ Never โ€” Stores absolute address โ€” meaningless in any process that attaches at a different address.
*p = target – baseaddr โœ… Correct Variable-size or complex layouts Offset is a universal value. Each process rebuilds the pointer with its own baseaddr.
array[index] โœ… Correct Fixed-size node pools Simplest approach when all elements are the same size.

Interview Questions โ€” Pointers in Shared Memory
Q1: Why is it wrong to store absolute pointers inside a System V shared memory segment?

Answer: shmat(shmid, NULL, 0) lets the kernel choose the attachment address independently for each process. The same segment may be at 0xB7E00000 in process A and 0xB6F00000 in process B. An absolute pointer stored by A, say 0xB7E00200, is not inside the segment in B’s address space at all. Dereferencing it causes a segfault or silent data corruption.

Q2: How do you correctly store a cross-process reference inside shared memory?

Answer: Store a relative byte offset from the segment’s start:

Store: *p = (target - baseaddr);
Restore: real_ptr = baseaddr + *p;

baseaddr is the return value of shmat() in the current process. The offset is the same number in every process; only the reconstructed pointer differs.

Q3: Can you force shmat() to attach at the same address in all processes to make absolute pointers work?

Answer: You can pass a fixed address as the second argument to shmat(shmid, fixed_addr, SHM_RND). However this is fragile: the address may already be occupied in some process (shmat fails), different processes may have different library layouts, and ASLR can cause conflicts. Offsets are always the portable, safe solution.

Q4: What is an alternative to byte offsets for referencing nodes within shared memory?

Answer: When all nodes are the same size, cast the shared memory as an array of fixed-size structures and use integer indices as references. Index 5 always means the 5th element of the array, regardless of where the array starts in virtual memory.

Q5: What type should the offset field be? Is int sufficient?

Answer: Use ptrdiff_t (from <stddef.h>) or long. On 64-bit systems these are 8 bytes. Using int (4 bytes, max ~2 GB) is only safe if the segment is guaranteed to be smaller than 2 GB. ptrdiff_t is the standard C type for pointer differences and is the most portable choice.

Q6: A developer argues that since child processes inherit the parent’s address space after fork(), shared memory is always at the same address. Is this safe?

Answer: Only immediately after fork() in the child, before any exec(). After exec() the address space is completely replaced and shared memory must be re-attached. Also, grandchildren, unrelated processes, or processes restarted after a crash will all attach at potentially different addresses. Relying on address equality is not a safe general strategy.

Q7: In the offset formula *p = (target – baseaddr), what does baseaddr represent and where is it stored?

Answer: baseaddr is the virtual address at which the shared memory segment starts in the current process. It is the return value of shmat() in that process. It is a process-local variable โ€” it must not be stored inside the shared memory itself. Each process saves its own copy of baseaddr and uses it for all offset calculations.

Next: shmctl() Control Operations

Learn how to delete, inspect, change permissions, and lock a shared memory segment.

โ†’ shmctl() Operations โ† /proc/PID/maps

Leave a Reply

Your email address will not be published. Required fields are marked *