IPC Namespaces
An IPC namespace (created with the
clone(2)/unshare(2)flagCLONE_NEWIPC) isolates the kernel’s two non-filesystem inter-process-communication facilities: System V IPC objects — message queues, semaphore sets, and shared-memory segments — and (since Linux 2.6.30) POSIX message queues (ipc_namespaces(7)). The defining trait these share, and the reason they need a dedicated namespace, is that their objects are not named by filesystem paths: a System V object is found by a numeric key or a returned identifier in a single global table, and a POSIX message queue is named in a specialmqueuefsfilesystem rather than the normal tree. Each IPC namespace gets its own private set of System V identifiers, its own POSIX message-queue filesystem, its own per-namespace tuning limits (kernel.sem,kernel.shmmax,kernel.msgmax, and friends), and its own/proc/sysvipcview. Objects created inside a namespace are visible only to its members, and when the last member exits, all of that namespace’s IPC objects are destroyed automatically.
Mental Model
The kernel keeps three global tables — one for message queues, one for semaphore sets, one for shared-memory segments — plus a filesystem of POSIX message queues. Without an IPC namespace, every process on the machine shares those tables. Two unrelated programs that both call shmget(0x1234, ...) will look up the same key 0x1234 in the same table and end up attached to the same segment, whether they meant to or not. An IPC namespace replaces the one shared set of tables with a per-namespace set of tables. Now each container has its own keyspace and its own identifier space; the key 0x1234 in container A and the key 0x1234 in container B index into entirely different tables and refer to different objects (or to nothing).
The mental shortcut is: IPC namespace = a private “IPC universe.” Inside it, ipcs lists only your objects, the keys you allocate collide only with each other, and the kernel.shmmax you set applies only to you. Nothing leaks across the boundary, and nothing from another namespace is reachable.
flowchart TB subgraph A["ipc_namespace A (container A)"] AS["semaphore table (ids[0])"] AM["msg queue table (ids[1])"] AH["shm table (ids[2])<br/>key 0x1234 -> seg #5"] AMQ["POSIX mqueuefs (own mount)"] end subgraph B["ipc_namespace B (container B)"] BS["semaphore table (ids[0])"] BM["msg queue table (ids[1])"] BH["shm table (ids[2])<br/>key 0x1234 -> seg #9"] BMQ["POSIX mqueuefs (own mount)"] end K["A process in A doing<br/>shmget(0x1234) reaches seg #5;<br/>the SAME call in B reaches seg #9 — NO collision"] A --> K B --> K
What it shows: two ipc_namespace objects, each with its own three System V tables (ids[0]=sem, ids[1]=msg, ids[2]=shm) and its own POSIX message-queue mount. The insight: the same well-known key 0x1234 resolves to different shared-memory segments in A and B, because the key-to-object lookup happens inside the per-namespace table — so two containers that independently chose the same key do not clash. Share one IPC namespace and that safety vanishes: the second shmget would attach to the first’s segment.
Why Two Containers Must Not Share a System V Segment
This is the concrete reason CLONE_NEWIPC exists, and it is worth spelling out because it is a correctness bug, not merely an aesthetic isolation nicety. System V shared memory is addressed by an integer key. The conventional way to derive a key is ftok(pathname, proj_id), which hashes a file’s inode and a small project byte into a 32-bit key — or applications simply hard-code a “well-known” key like 0xDEADBEEF. Many widely deployed programs do exactly this: PostgreSQL uses a System V shared-memory segment for its shared buffers and addresses it by a key derived from its data directory and port; Oracle’s SGA is a System V segment; countless legacy daemons pick a fixed key.
Now run two instances of such a program — say two PostgreSQL clusters — in two containers that share the host IPC namespace. Both compute (or hard-code) the same key. The first shmget(key, size, IPC_CREAT) creates segment, returns identifier 5. The second instance’s shmget(key, ...) finds that the key already exists in the shared table and, instead of creating its own, attaches to the first instance’s segment — or, if it passed IPC_CREAT | IPC_EXCL, fails with EEXIST. Either way the second database is now reading and writing the first database’s memory or refusing to start. That is silent data corruption or an outright crash, caused purely by a key collision that the two unrelated containers had no way to coordinate.
The IPC namespace eliminates the collision by construction. Each container gets its own ipc_namespace with its own ids[1] (the shared-memory table) and its own private keyspace. Container A’s key 0xDEADBEEF and container B’s key 0xDEADBEEF are looked up in different tables and create different segments. The two databases coexist with zero coordination. This is why every real container runtime gives each container its own IPC namespace by default — without it, any pair of containers using the same well-known SysV key would be a landmine. (Pods in Kubernetes are the deliberate exception: containers within one pod may share an IPC namespace via shareProcessNamespace-adjacent settings, because they are co-designed and want to communicate.)
The ipc_namespace Structure
The whole namespace is one struct, from include/linux/ipc_namespace.h (Linux v6.12). The important fields, annotated:
struct ipc_namespace {
struct ipc_ids ids[3]; /* the THREE SysV tables: sem, msg, shm */
int sem_ctls[4]; /* kernel.sem: semmsl, semmns, semopm, semmni */
int used_sems;
unsigned int msg_ctlmax; /* kernel.msgmax: max bytes in one message */
unsigned int msg_ctlmnb; /* kernel.msgmnb: default queue capacity */
unsigned int msg_ctlmni; /* kernel.msgmni: max number of msg queues */
struct percpu_counter percpu_msg_bytes; /* live bytes, accounted per-ns */
struct percpu_counter percpu_msg_hdrs;
size_t shm_ctlmax; /* kernel.shmmax: max bytes in one segment */
size_t shm_ctlall; /* kernel.shmall: max total shm pages */
unsigned long shm_tot; /* current total shm pages in THIS namespace */
int shm_ctlmni; /* kernel.shmmni: max number of segments */
int shm_rmid_forced; /* kernel.shm_rmid_forced toggle */
struct vfsmount *mq_mnt; /* the POSIX mqueuefs mount for this ns */
unsigned int mq_queues_count;
unsigned int mq_queues_max; /* /proc/sys/fs/mqueue/queues_max */
unsigned int mq_msg_max; /* /proc/sys/fs/mqueue/msg_max */
unsigned int mq_msgsize_max; /* /proc/sys/fs/mqueue/msgsize_max */
...
struct ctl_table_set ipc_set; /* this ns's /proc/sys/kernel/* IPC sysctls */
struct ctl_table_header *ipc_sysctls;
struct user_namespace *user_ns; /* owning user namespace */
struct ucounts *ucounts; /* per-user limit on IPC namespaces */
struct ns_common ns; /* inode number + ops (the /proc symlink) */
} __randomize_layout;The single most important field is ids[3] — an array of three struct ipc_ids, the three System V tables. The index constants are defined in ipc/util.h (v6.12): ids[IPC_SEM_IDS] (index 0) holds semaphore sets, ids[IPC_MSG_IDS] (index 1) holds message queues, and ids[IPC_SHM_IDS] (index 2) holds shared-memory segments. Each ipc_ids contains an idr (an integer-to-pointer radix tree that maps the kernel-assigned identifier to the object) and a key_ht rhashtable (that maps the user-supplied key to the object). Two lookups, two namespaces of meaning: the identifier is what shmget/msgget/semget return and what later *ctl/*at calls use; the key is what the user passes to find-or-create an object. Both tables live inside the namespace, which is the mechanical fact behind per-container key isolation.
The rest of the struct is per-namespace tuning: the sem_ctls/msg_ctl*/shm_ctl* fields are the live values behind /proc/sys/kernel/sem, kernel.msgmax, kernel.shmmax, and so on. Because they are fields of ipc_namespace, changing kernel.shmmax inside a container changes only that container’s limit — the sysctl is namespaced, exactly as ipc_namespaces(7) lists. mq_mnt is this namespace’s private POSIX message-queue filesystem mount, and the mq_* fields back /proc/sys/fs/mqueue/*. user_ns records the owning user namespace (all privilege checks go through it); ucounts enforces the per-user max_ipc_namespaces quota; ns carries the inode you see as ipc:[4026531839] in /proc/$$/ns/ipc.
The Per-Namespace Defaults (v6.12)
When a new IPC namespace is created, each sub-table is initialized to compiled-in defaults. These come from ipc/sem.c, ipc/shm.c, and ipc/msg.c (v6.12):
void sem_init_ns(ns) { ns->sc_semmsl=SEMMSL; ns->sc_semmns=SEMMNS;
ns->sc_semopm=SEMOPM; ns->sc_semmni=SEMMNI; }
void shm_init_ns(ns) { ns->shm_ctlmax=SHMMAX; ns->shm_ctlall=SHMALL;
ns->shm_ctlmni=SHMMNI; }
int msg_init_ns(ns) { ns->msg_ctlmax=MSGMAX; ns->msg_ctlmnb=MSGMNB;
ns->msg_ctlmni=MSGMNI; }Resolving each constant against the v6.12 UAPI headers:
| sysctl | constant | v6.12 default | meaning |
|---|---|---|---|
kernel.sem field 1 (semmsl) | SEMMSL | 32000 | max semaphores per set |
kernel.sem field 2 (semmns) | SEMMNS | SEMMNI*SEMMSL | max semaphores system-wide (per ns) |
kernel.sem field 3 (semopm) | SEMOPM | 500 | max operations per semop() call |
kernel.sem field 4 (semmni) | SEMMNI | 32000 | max semaphore sets |
kernel.shmmax | SHMMAX | ULONG_MAX - 2^24 | max bytes in one segment |
kernel.shmall | SHMALL | ULONG_MAX - 2^24 (pages) | max total shm pages |
kernel.shmmni | SHMMNI | 4096 | max number of segments |
kernel.msgmax | MSGMAX | 8192 | max bytes in one message |
kernel.msgmnb | MSGMNB | 16384 | default queue byte capacity |
kernel.msgmni | MSGMNI | 32000 | max number of message queues |
The one value that surprises people: SHMMAX is effectively unlimited in modern kernels — ULONG_MAX - (1UL << 24) is roughly 16 exabytes on a 64-bit machine, not the historical 32 MiB (33554432) that older tuning guides and from-memory answers still quote. The v6.12 uapi/linux/shm.h comment explains the choice: the old small default forced operators to bump shmmax for every nontrivial database, so the kernel raised it to “essentially no limit” and left the real bounding to per-process memory and the shmall page total. If you are writing a note or a sysctl file from memory, this is the number to double-check.
Uncertain
Verify: the exact historical kernel release in which
SHMMAXwas raised from33554432toULONG_MAX - 2^24. Reason: the v6.12 source value is pinned (it isULONG_MAX - (1UL << 24)), but the changeover release was not fetched in this task. To resolve:git log -p include/uapi/linux/shm.hfor the commit that changed the#define SHMMAX. The current-value claim is firmly grounded in v6.12 source; only the “when did it change” date is unpinned. uncertain
How It Actually Works
Creation runs through copy_ipcs() in ipc/namespace.c (v6.12):
struct ipc_namespace *copy_ipcs(unsigned long flags,
struct user_namespace *user_ns, struct ipc_namespace *ns)
{
if (!(flags & CLONE_NEWIPC))
return get_ipc_ns(ns); /* no flag: share, bump refcount */
return create_ipc_ns(user_ns, ns); /* flag set: build a fresh one */
}Without CLONE_NEWIPC the child simply shares the parent’s namespace (refcount up). With the flag, create_ipc_ns() builds a brand-new universe from scratch — note it does not memcpy the parent’s objects (unlike UTS, which copies the parent’s hostname); a new IPC namespace starts empty of objects, with only the default limits:
static struct ipc_namespace *create_ipc_ns(user_ns, old_ns)
{
ucounts = inc_ipc_namespaces(user_ns); /* enforce per-user quota */
ns = kzalloc(sizeof(*ns), GFP_KERNEL_ACCOUNT); /* charged to cgroup */
ns_alloc_inum(&ns->ns); /* assign inode number */
ns->user_ns = get_user_ns(user_ns);
mq_init_ns(ns); /* mount a private POSIX mqueuefs */
setup_mq_sysctls(ns); /* register /proc/sys/fs/mqueue/* for this ns */
setup_ipc_sysctls(ns); /* register /proc/sys/kernel/{sem,shm*,msg*} */
msg_init_ns(ns); /* defaults + empty msg table */
sem_init_ns(ns); /* defaults + empty sem table */
shm_init_ns(ns); /* defaults + empty shm table */
return ns;
}So creating an IPC namespace allocates the struct, charges a ucounts slot (failing with -ENOSPC if the user is over max_ipc_namespaces), mounts a fresh private mqueuefs, registers a fresh set of per-namespace sysctl files, and initializes three empty SysV tables with the compiled-in default limits. The allocation uses GFP_KERNEL_ACCOUNT, so the namespace’s kernel memory is charged to the creating cgroup. One subtlety visible in the source: if the ucounts slot is unavailable because frees are pending, the code does flush_work(&free_ipc_work) and retries — IPC namespaces are freed asynchronously through a work queue (free_ipc_work) to avoid the cost of synchronize_rcu in the unmount path, so a just-exited namespace may take a moment to release its quota slot.
Automatic destruction
ipc_namespaces(7) states the lifecycle plainly: “When an IPC namespace is destroyed (i.e., when the last process that is a member of the namespace terminates), all IPC objects in the namespace are automatically destroyed.” This is a genuinely useful property and a contrast with the host’s IPC, where a leaked System V segment survives the death of every process that used it (the classic “ghost ipcs entry” that needs a manual ipcrm). Because a container’s IPC objects live in its own namespace, the moment the container’s last process exits — and any bind-mounted /proc/.../ns/ipc reference is dropped — the kernel tears the namespace down and reclaims every queue, semaphore set, and segment it held. No leaked SysV objects accumulate across container restarts.
The /proc surfaces that become per-namespace
Per ipc_namespaces(7), an IPC namespace makes the following /proc interfaces distinct: the POSIX message-queue controls in /proc/sys/fs/mqueue; the System V controls in /proc/sys/kernel — namely msgmax, msgmnb, msgmni, sem, shmall, shmmax, shmmni, and shm_rmid_forced; and the object listing in /proc/sysvipc. The last is what ipcs(1) reads — which is why ipcs inside a container shows only that container’s objects. The brief’s kernel.sem / kernel.shmmax / kernel.msgmax are members of this larger namespaced set, not the whole of it.
Trying It By Hand
$ ipcmk -M 1024 # create a 1 KiB SysV shm segment on the host
Shared memory id: 0
$ ipcs -m # host sees it
------ Shared Memory Segments --------
key shmid ...
0x... 0 ...
$ sudo unshare --ipc bash # enter a NEW ipc namespace
# ipcs -m # empty — the host's segment is invisible
------ Shared Memory Segments --------
key shmid ...
# ipcmk -M 1024 # create one here
Shared memory id: 0 # id 0 again — separate keyspace!
# readlink /proc/$$/ns/ipc
ipc:[4026532289] # different inode from the host's ipc namespace
# exit
$ ipcs -m # host still has only its original segmentThe --ipc flag sets CLONE_NEWIPC. Inside the new namespace ipcs is empty — proof that the host’s segment lives in a different ipc_namespace. Creating a segment inside reuses identifier 0, demonstrating that the identifier space (the idr) is independent. The differing /proc/self/ns/ipc inode confirms the namespaces are distinct kernel objects. When the subshell exits, that namespace and its segment are destroyed automatically, while the host’s original segment is untouched.
POSIX message queues are isolated the same way through mqueuefs:
$ sudo unshare --ipc bash
# mkdir /dev/mqueue && mount -t mqueue none /dev/mqueue
# touch is not how you create them — use mq_open(3), but:
# ls /dev/mqueue # only this namespace's queues appearEach IPC namespace has its own mq_mnt, so the mqueuefs mounted at /dev/mqueue shows only the queues created within that namespace.
Common Misunderstandings
“The IPC namespace isolates all forms of IPC.” No — it isolates only the non-filesystem-named IPC: System V message queues/semaphores/shared memory and POSIX message queues. Pipes and FIFOs (named by path), UNIX domain sockets (named by path or abstract socket), eventfd/signalfd/pidfd, and POSIX shared memory via shm_open (which lives under /dev/shm, a normal tmpfs, and is therefore isolated by the mount namespace, not the IPC namespace) are all outside the IPC namespace’s scope. The line is precisely the one ipc_namespaces(7) draws: objects identified by something other than a filesystem pathname.
“/dev/shm is part of the IPC namespace.” A frequent and consequential confusion. /dev/shm is a tmpfs mount; POSIX shm_open(3) objects are files in it. Their isolation comes from the mount namespace (each container has its own /dev/shm mount), not from CLONE_NEWIPC. You can have a private IPC namespace and still share /dev/shm if you share the mount namespace, and vice versa. The System V shmget family is in the IPC namespace; the POSIX shm_open family is not.
“A new IPC namespace inherits the parent’s objects.” It does not. Unlike a UTS namespace (which copies the parent’s hostname), a fresh IPC namespace starts with three empty tables — only the default limits are inherited (and those are compiled-in defaults, not the parent’s possibly-tuned values; see create_ipc_ns calling *_init_ns). There is no way for a child IPC namespace to see objects the parent created before the unshare.
“kernel.shmmax is global.” Since the IPC namespace exists, it is per-namespace — a container can set its own kernel.shmmax without affecting the host or sibling containers, because the value is a field of ipc_namespace. The same goes for kernel.sem, kernel.msgmni, and the rest of the namespaced list above.
Alternatives and When to Choose Them
If your goal is merely to prevent SysV key collisions between two cooperating programs you control, you can sometimes sidestep the namespace by choosing disjoint keys (e.g. derive keys from PIDs or use IPC_PRIVATE). But that requires coordination and is fragile against third-party software with hard-coded keys — which is exactly why containers use the namespace instead: it removes the requirement to coordinate at all. For IPC that must cross a container boundary deliberately, the modern preference is filesystem-named channels — UNIX domain sockets over a shared volume, or shared tmpfs files — which are governed by the mount namespace and are far easier to reason about than shared System V keys. New code rarely chooses System V IPC at all; it survives mainly in legacy databases and middleware, which is the population the IPC namespace exists to protect from each other.
Among the eight namespaces, IPC and UTS are the two “simple” ones, but IPC carries materially more state (three tables, a private filesystem mount, a dozen sysctls) than UTS (one six-field struct). The load-bearing namespaces remain PID, mount, and network; IPC is included in essentially every container because the cost is small and the SysV-collision hazard is real.
Production Notes
OCI runtimes request CLONE_NEWIPC by listing {"type": "ipc"} in the namespaces array of the OCI config.json; runc creates the namespace before exec, and the runtime typically also mounts a fresh /dev/shm (mount-namespace territory) so that POSIX shared memory is private too. In Kubernetes, every pod gets its own IPC namespace, and the containers within a pod share it — which is why two containers in one pod can communicate via a System V queue but two containers in different pods cannot. A pod can opt into the host’s IPC namespace with spec.hostIPC: true (rarely used, security-sensitive, since it exposes the host’s SysV objects), and historically the small default /dev/shm size (64 MiB) has been a real source of crashes for memory-database and ML workloads that map large POSIX shared-memory regions — but note that limit is a tmpfs size set on the mount, not a kernel.shmmax/IPC-namespace property, a distinction operators frequently get wrong when debugging “out of shared memory” errors.
A second operational point: because a namespace’s IPC objects are destroyed when its last member exits, a container restart cleanly reclaims any SysV segments the old instance held — no ipcrm cleanup is needed, in stark contrast to bare-metal deployments where a crashed database can leave orphaned segments that block the next start. This automatic reclamation is one of the quiet wins of running such software in a container.
See Also
- Linux Namespaces Overview — the parent concept; the eight namespace types and the
CLONE_NEW*flags - UTS Namespaces — the sibling “simple” namespace (hostname / NIS domain); contrast: UTS copies the parent’s value, IPC starts empty
- System V IPC — the message queues, semaphore sets, and shared-memory segments this namespace isolates
- POSIX Message Queues — the
mqueuefs-backed queues, the second resourceCLONE_NEWIPCisolates - Mount Namespaces — owns
/dev/shm(POSIXshm_open) isolation, which is not the IPC namespace’s job - PID Namespaces, Network Namespaces — the other load-bearing namespaces
- User Namespaces — owns the privilege model; the IPC namespace records its owning user namespace
- clone unshare and setns — the syscalls that create, detach, and join namespaces
- Linux Containers and Isolation MOC — the parent map (section B, Namespaces)