F_SETPIPE_SZ and soft page quota effects

FSETPIPESZ and soft page quota effects

A pipe that starts at 8 KiB instead of 64 KiB changes the shape of a spray in a way that is easy to miss and awkward to ignore. The per-user soft page quota can push newly created unprivileged pipes down to the smaller default, even when the code asked for the usual capacity. That matters because pipe capacity is not just a throughput setting, it also drives how many pipe_buffer entries get allocated and where they land in slab caches.

When fcntl(F_SETPIPE_SZ) is used, the request is rounded to a power-of-two page count. A pipe() created at the default size gives a 64 KiB pipe, which is 16 pages. That means 16 pipe_buffer elements, which sit in kmalloc-1k because the array is 16 × 40 bytes, or 640 bytes. A smaller pipe changes that layout straight away.

How the soft quota quietly changes new pipe capacity

The soft limit is checked when alloc_pipe_info() sets up the pipe. If the user has crossed pipe-user-pages-soft, new pipes stop using the larger default and fall back to PIPE_MIN_DEF_BUFFERS, which is 2. In practice that drops the new pipe to 8192 bytes, not 65536. The pipe still works, but the spray no longer fills the same slab cache footprint.

That shift can happen after enough pipes exist under the same UID. The example threshold in the supplied material appears after creating 1024 pipes. Nothing about the call site changes. The pipe just comes back smaller.

The 65536-byte default versus the 8192-byte fallback

A 65536-byte pipe means 16 pages and 16 pipe_buffer slots. An 8192-byte pipe means 2 pages and 2 pipe_buffer slots. The difference is not cosmetic. The larger pipe gives a dense block of pipe_buffer objects. The smaller one leaves far less contiguous pipe metadata behind and changes the odds of landing in the same hole twice.

/proc/sys/fs/pipe-max-size caps the maximum size an unprivileged user can request, with a default of 1048576 bytes, or 256 pages. That is the upper bound. The soft quota is the thing that quietly cuts the other way for new pipes.

Why that smaller pipe changes pipe_buffer spray shape

The usual exploit interest in pipe_buffer comes from field corruption, not from the pipe as a file abstraction. The array size, the slab it lands in, and the way writes consume buffers all affect whether a spray produces useful adjacency or just noise. A 2-slot pipe is much less forgiving than a 16-slot pipe.

kmalloc-1k versus the leftover space in smaller slabs

A default 16-page pipe allocates a 640-byte pipe_buffer array from kmalloc-1k. That is a neat fit, with room left in the slab object. When many such allocations happen, that leftover space can matter for hole plugging and for how other objects sit next to each other.

A smaller pipe does not give the same pattern. Two pipe_buffer entries are only 80 bytes, so the allocation drops into a much smaller kmalloc class. That changes the slab cache pressure and the number of partial slabs left behind. In other words, the spray can still work, but the shape is different and the room for neat placement gets worse. Kernel heap spraying tends to punish assumptions like that.

For primitives that rely on struct page references or page-level redirection, that difference is even less friendly. A smaller pipe gives fewer opportunities to line up the page pointer state you were hoping for.

What anon_pipe_write() does differently when the pipe is not empty

anon_pipe_write() behaves differently when the pipe already holds data. The practical consequence is simple: a write may block until the pipe empties if there is not enough room, and the smaller the pipe, the easier it is to hit that state. With only two buffers, the behaviour becomes obvious very quickly.

That matters for spraying because a non-empty pipe does not accept the same write pattern as an empty one. If the pipe is not empty, the code has less slack for buffer churn, and that can change which slab objects stay alive long enough to matter. It also means a setup that looked stable with 16 buffers can start stalling as soon as the soft quota pushes new pipes down to the minimum.

PIPE_MIN_DEF_BUFFERS is 2 for a reason. Below that, the mechanics get awkward fast, and the pipe stops behaving like the roomy spray primitive people often assume it is.

Practical checks before you trust your spray setup

Read back the pipe size with F_GETPIPE_SZ before and after resizing. If a pipe that should be 65536 bytes comes back as 8192 bytes, the soft quota has already changed the conditions. Do not assume the first few pipes prove anything about later ones.

If you are building a heap spray that depends on pipe() or F_SETPIPE_SZ, check the user page counters and the unprivileged pipe size limit first. A setup that works cleanly with one UID or on an idle system can fall apart once the soft quota starts forcing new pipes into the minimum default. That is a boring failure mode, which is exactly why it gets missed.

Keep the pipe count and file descriptor limits in mind as well. If RLIMIT_NOFILE is too low, the test does not reach the state you think it does. If the pipe size quietly drops, the spray layout changes before the interesting part begins.

Related posts

Weekly Tech Digest | 06 Jul 2026

Stay updated with the latest in tech! This digest covers AI ethics, auto industry shifts, and the impact of politics on technology, exploring today's pressing issues.

wolfCOSE zero-allocation parsing in embedded C

wolfCOSE looks sensible only if you care about what your firmware actually has to carry. I like that, because on small targets the wrong crypto feature can cost more than the message itself, and there...

restic | v0.19.1

restic v0 19 1: safer FUSE mounts and mountpoint checks, robust backup source and exclude handling, clearer CLI JSON output, Windows SFTP deletion fixes