msg_msg

#kmalloc-variable#msg_msg#sysvipc#heapspray#kmalloc-variable
Slabkmalloc-64kmalloc-96..4096 (variable, size-controlled)
Requiresoob-read-or-write-primitiveuaf-primitive
Givesoob-read-heap-leakoob-write-arbitrary-rw (via msg_msgseg->next corruption)

SysV Message Queue (msg_msg) IPC

1. msg_msg / msg_msgseg Structures

  • struct msg_msg defined in include/linux/msg.h
  • struct msg_msgseg defined locally in ipc/msgutil.c

struct msg_msg {
    struct list_head m_list;
    long m_type;
    size_t m_ts;              // message text size
    struct msg_msgseg *next;  // next segment, if message > DATALEN_MSG
    void *security;
    /* the actual message follows immediately */
};

struct msg_msgseg {
    struct msg_msgseg *next;
    /* the next part of the message follows immediately */
};
  • m_ts: Message text size, controlled by msgsnd(msgsz) , useful for heap grooming.
  • next: Points to the next msg_msgseg for larger messages. Corrupting it can enable arbitrary read/write by making the kernel follow an attacker-controlled pointer.

Size:

  • sizeof(struct msg_msg) is 48 bytes (m_list 16 + m_type 8 + m_ts 8 + next 8 + security 8).
  • DATALEN_MSG = PAGE_SIZE - sizeof(struct msg_msg)4096 - 48 = 4048 bytes fit inline in the first allocation.
  • If msgsz > DATALEN_MSG, the remainder is split into msg_msgseg chunks of DATALEN_SEG = PAGE_SIZE - sizeof(struct msg_msgseg)4096 - 8 = 4088 bytes each, chained via ->next.
  • Total allocation size for the first chunk is sizeof(struct msg_msg) + min(len, DATALEN_MSG) = 48 + msgsz for msgsz <= 4048. This means we can can hit any kmalloc size class from kmalloc-64 up to kmalloc-4k just by choosing msgsz = target_size - 48.

2. Exploitation

Three syscalls cover the whole primitive:

  1. msgget() — create the message queue (IPC object) that will hold the sprayed objects.
int msg_id = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
  1. msgsnd() — allocate + fill a msg_msg (calls load_msg()alloc_msg()kmem_buckets_alloc()).
struct msgbuf {
    long mtype;
    char mtext[SPRAY_SIZE];
};

struct msgbuf msg = { .mtype = 1 };
memset(msg.mtext, 'A', SPRAY_SIZE);   // SPRAY_SIZE = target_kmalloc_size - 48

msgsnd(msg_id, &msg, SPRAY_SIZE - HEADER_SIZE, 0);
  1. msgrcv() — read a message back out (calls do_msgrcv() → copies m_ts bytes to userspace, then frees via free_msg() unless peeking). This is the leak / reclaim step:
    • If a bug lets you inflate m_ts (OOB write) or you UAF a freed msg_msg and respray something else into it, msgrcv() will happily copy back more bytes than you originally sent — an OOB read / heap-leak primitive.
    • MSG_COPY flag (with a positive msgtyp used as an index) lets you peek at a message without dequeuing it, handy for repeated leaks from the same spray.
char leak[SPRAY_SIZE];
ssize_t n = msgrcv(qid, leak, sizeof(leak), 0, 0);
// leak now contains SPRAY_SIZE 
  1. msgctl(): msgctl(qid, IPC_RMID, NULL) frees every queued message in one shot (freeque()free_msg() on each). Useful for triggering a batch of frees at once (double-free / UAF setup), same role fcntl(F_SETPIPE_SZ) plays for pipe_buffer.
msgctl(msg_id, IPC_RMID, NULL);

3. Debugging

  • load_msg / alloc_msg (ipc/msgutil.c) , We can break here in gdb to watch msg_msg/msg_msgseg allocation and size selection.
  • free_msg (ipc/msgutil.c) , break here to analyse frees.
  • do_msgsnd / do_msgrcv (ipc/msg.c) , syscall entry points.

4. Mitigations

  • Dedicated slab bucketipc/msgutil.c:42-52 now allocates msg_msg from a private kmem_buckets_create("msg_msg", SLAB_ACCOUNT, ...) bucket set instead of the shared kmalloc-N caches.
  • seccomp-bpf syscall filtering — Eplicitly deny msgget/msgsnd/msgrcv/msgctl. Seen in hardened Kubernetes seccomp profiles.

Similar Notes: https://leo1.cc/posts/docs/msg_msg/