alloc_pipe_info: why soft limits shrink new pipes

allocpipeinfo: why soft limits shrink new pipes

alloc_pipe_info() builds the pipe state and picks the buffer array size from the pipe’s capacity. A normal unprivileged pipe starts at 65536 bytes, or 16 pages, which gives 16 pipe_buffer slots. Each struct pipe_buffer is 40 bytes, so the array is 640 bytes and sits in kmalloc-1k.

Once the per-user soft page count is exceeded, new pipes stop getting that default shape. The kernel uses too_many_pipe_buffers_soft() to decide when to clamp the next pipe down to PIPE_MIN_DEF_BUFFERS, which is 2. That drops the capacity to 8192 bytes on a 4096-byte page system, and the buffer array shrinks to 80 bytes. On many builds that moves allocation into kmalloc-96.

How pipe capacity turns into pipe_buffer storage

Pipe size is not just an I/O limit. It decides how many pipe_buffer entries are allocated, and that decides the slab cache. F_SETPIPE_SZ changes both in one go. The pipe capacity is rounded to a page-based size, then the kernel allocates the matching number of buffer slots.

That gives a blunt but useful distinction:

  • 16-page pipes: 16 entries, kmalloc-1k
  • 2-page pipes: 2 entries, often kmalloc-96

The placement shift matters more than the raw byte count. A spray that used to touch kmalloc-1k can end up feeding kmalloc-96 instead, or split across different caches if the machine is already busy. That changes which adjacent objects are likely to be hit.

The default 16-page pipe and the kmalloc-1k array

The default 65536-byte pipe is the useful one for broad spraying because it creates more buffer slots per allocation. More slots mean more metadata in one place, which can help when a primitive needs repeated pipe_buffer objects nearby. The cost is size. A pipe like that does not land in the same slab class as the smaller two-buffer form.

FSETPIPESZ changes both capacity and slab placement

fcntl(fd, F_SETPIPE_SZ, size) is not just a way to make a pipe larger or smaller. It also reselects the allocation size for the pipe_buffer array. That means a pipe can move between slab caches during the same process lifetime, which is awkward for assumptions about stable heap layout.

The kernel also caps unprivileged growth with /proc/sys/fs/pipe-max-size, which defaults to 1048576 bytes. Shrinking is the more interesting path here. It is the one that changes the buffer count and pushes new pipes into a tighter slab class.

When pipe-user-pages-soft forces new pipes down to two buffers

The soft limit works per user. Once enough pipe pages exist under that UID, later pipes get the reduced default. In practice that means a process can create a few large pipes, hit the soft ceiling, and then watch new pipes come up small without any obvious error.

That behaviour is useful for defenders because it reduces the amount of pipe memory a user can grab for heap shaping. It is awkward for anyone relying on pipe spray consistency because the cache target changes mid-run.

too_many_pipe_buffers_soft() and the per-user count

too_many_pipe_buffers_soft() checks the total page count used by pipes for that user. It does not care that a new pipe would be tiny. Once the count crosses the soft threshold, the new pipe gets the reduced default size.

There is a practical detail here: the limit is based on pages, not pipe count. Two users can create the same number of pipes and get different results if their pipes have different sizes. That makes test runs noisy if the setup is not controlled.

Why 8192-byte pipes still matter for spraying and blocking

An 8192-byte pipe still has enough room to be useful. It keeps two pipe_buffer slots, which is the point for slab placement. It also gives enough room for small write patterns that depend on pipe state rather than volume.

There is a catch: writes to non-full pipes may block until the pipe is emptied. Smaller pipes hit that edge sooner, which changes timing and can expose brittle test code. If a harness assumes it can keep writing without draining, the two-buffer form will make it wait in places that looked harmless before.

What that shrink changes for hardening and exploitation

The hardening effect is simple. New pipes become less useful as generic spray objects once the soft limit kicks in. The exploitation effect is also simple. The same machine can hand out different slab caches to different pipes depending on current per-user usage, which makes placement less predictable.

pipe_buffer matters because corruption of its fields can produce real primitives. Flags corruption has been used for Dirty Pipe style overwrites. Other fields can support read and write primitives or page-level reuse tricks. A setup that depends on one cache class can fall apart if half the fresh pipes move to another.

kmalloc-96 versus kmalloc-192 on the same machine

The exact cache label varies with build details and object layout. On one machine a reduced pipe array may fit kmalloc-96; on another it may land in kmalloc-192 if the structure size, slab alignment, or debug options shift the cutoff. The point is not the exact cache name. The point is that the cache class can change in a way that breaks a spray plan built around a single assumption.

That is awkward for cross-cache work. A spray can leave many partial slabs with spare slots, but the slots may be in the wrong cache for the object you wanted to influence. The result is wasted memory and less predictable adjacency.

Pipe writes, non-full buffers, and the operational edge cases

The two-buffer pipe has less slack. A write that looked harmless on a 16-page pipe can block sooner on a reduced one. Small changes in writer behaviour can expose deadlocks, timing gaps, or test failures that never showed up on the larger default pipe.

That matters in hardening work too. A reduced pipe limit does not remove pipe_buffer from the attack surface. It just changes the geometry. Defenders still need to think about slab class churn, and attackers still need to account for the fact that one process can see both large and tiny pipes on the same host.

A sane test setup for checking the limit in practice

A sensible test uses enough file descriptors to create a large number of pipes, with RLIMIT_NOFILE raised first. The referenced experiment used 2048 pipes, which is enough to push a user over the soft page threshold and watch the new default shrink from 65536 bytes to 8192 bytes.

Check F_GETPIPE_SZ before and after the quota pressure. If the limit is active, the new pipes should report the smaller capacity. That gives a clean way to confirm that the soft limit is affecting allocation shape, not just pipe throughput.

Related posts

Corecrypto proofs against FIPS 203 and FIPS 204

Apple’s corecrypto formal verification work is interesting because it is properly dull, in the best sense. It ties a portable C implementation back to FIPS 203 and FIPS 204, then leaves the optimised...

alloc_pipe_info: why soft limits shrink new pipes

When pipe-user-pages-soft bites, the kernel stops being predictable in the way people like to assume. I prefer to check F_GETPIPE_SZ and watch the slab class shift for myself, because the difference...

Vector | v0.57.0

Vector v0 57 0 security first release: hardenings, intentional breaking changes, upgrade checklist, rollback options, docs and community support