How tmppage reuse changes the shape of pipebuffer corruption
pipe_buffer is not interesting because it is large. It is interesting because it sits in a small kmalloc slab, is created often, and carries pointers plus offsets that affect how the kernel copies data in and out of a pipe. A 16-element default array is only 640 bytes, which lands in kmalloc-1k. That makes it a tidy target for slab adjacency when the heap is already busy.
tmp_page reuse changes the usual exploit shape. Instead of treating each pipe write as a fresh event, the kernel can end up reusing the same temporary page across a sequence of operations. If an attacker can corrupt the metadata that governs that page, the same page content can be pushed back into later pipe activity. That is where repeated arbitrary write behaviour starts to show up, rather than a single corrupted transfer.
The important detail is that pipe_buffer corruption is not only about flags. If the page pointer, offset, and length are disturbed, the kernel can be made to copy from or to places it should not touch. That is the route that turns a damaged pipe into arbitrary read/write behaviour. Dirty Pipe style flag corruption sits lower on the ladder, but it is the same basic problem: the kernel trusts fields that should have stayed stable.
Where the reuse path meets anon_pipe_get_page and anon_pipe_put_page
The reuse path sits in the page lifecycle around anon_pipe_get_page and anon_pipe_put_page. One side hands a page into pipe handling, the other drops it back when the buffer is consumed or replaced. That hand-off matters because reuse depends on state surviving longer than it should, or being observed again in a later pipe operation.
If the reference and release logic is clean, tmp_page reuse is just an implementation detail. If the accounting is off, or a page is held in a state the next write can reach, the pipe starts behaving like a repeatable write gadget. This is where partial overwrite bugs stop being neat one-offs. The same page can be cycled through the pipe path again, with stale state still attached to it.
pipe_inode_info is part of that picture because it tracks the pipe’s buffer array and the accounting around it. The array is allocated with kcalloc(pipe_bufs, sizeof(struct pipe_buffer), GFP_KERNEL_ACCOUNT), so the number of buffers decides both the shape of the object and the memory pressure around it. A buffer array that looks harmless in a test case can be awkward in practice once reuse and release behaviour are involved.
The slab and accounting limits that decide whether a pipe is useful
Pipe capacity is not fixed in the useful sense. fcntl(..., F_SETPIPE_SZ, size) changes the capacity, and the kernel rounds it to page counts. The default size is 65536 bytes, which is 16 pages. The minimum is 4096 bytes, one page. The unprivileged ceiling comes from /proc/sys/fs/pipe-max-size, shown here as 1048576 bytes.
That looks generous until per-user limits kick in. alloc_pipe_info() applies a soft quota through too_many_pipe_buffers_soft(), and once that limit is crossed, new unprivileged pipes start smaller. In the example case, the drop is from 65536 bytes to 8192 bytes after 1024 pipes. That means new pipes begin with only two buffers, not sixteen.
That change matters for two reasons. First, it alters heap spraying. A 16-element pipe buffer array can sit in kmalloc-1k, but a smaller pipe is a different shape and a different allocation pattern. Second, it limits how much corrupted state can be staged before the pipe fills or blocks. A pipe with too few buffers is awkward if the goal is to keep a useful write primitive alive across multiple operations.
There is also plain old accounting friction. Creating the sort of pipe farm used in this sort of testing needs a higher RLIMIT_NOFILE, and the test case uses 2048 pipes. That is already a nuisance for unprivileged abuse, which is the point of the limit. The kernel is not being dramatic; it is just making the exploit plumbing more irritating than the glossy write-ups suggest.
What breaks when you try to turn it into a practical primitive
The primitive looks cleaner on paper than it does on a live machine. Reuse is sensitive to timing, pipe fill state, and whether the target pipe is empty enough for the next write to proceed. Writes to a non-empty pipe can block even when the pipe is not full, which is exactly the sort of detail that ruins a neat exploit chain.
Heap spraying also stops being tidy once the soft quota has reduced new pipes to two buffers. That leaves more partial slabs and fewer neatly packed targets. A pipe object that was useful at one stage of the spray can become a poor fit for the next one, which is annoying in the least cinematic way possible.
The classic end goals are still the same: overwrite a file page, corrupt modprobe_path, or shape a broader arbitrary read/write primitive. But the tmp_page reuse angle raises the bar for reliability. It asks for better control over page lifetime, cleaner sequencing around anon_pipe_get_page and anon_pipe_put_page, and more patience with the pipe’s own accounting rules. A pipe is a useful nuisance, not magic.



