2026, Jan 12 23:00

Feeding a Pwntools Child Process on fd 2: Why STDERR Won't Take Input and How to dup2 STDIN

Learn why pwntools can't write to STDERR, how to pipe input safely, and how to remap file descriptors with dup2 so targets reading from fd 2 consume your data.

Feeding data to a child process with pwntools often looks trivial until you try to push bytes into a specific file descriptor. The stumbling block is usually STDERR: people expect to write to fd 2 the same way they do to STDIN, and then wonder why nothing happens. Let’s walk through what’s actually writable, what isn’t, and how to correctly remap descriptors if your target reads from a nonstandard fd.

The setup that confuses many

Suppose you spin up a process and try to send bytes that the target will read from fd 2. A straightforward attempt writes to STDIN instead, because that’s what pwntools exposes for input.

from pwn import process, PIPE

# Start the program; mistakenly hoping to feed fd 2 (stderr)
child = process(['./target-app'], stdin=PIPE, stdout=PIPE)

# This always goes to the child's STDIN (fd 0), not fd 2
child.send(b'payload-for-fd2')

# Collect output as usual; this does not change where input lands
result = child.recvall()

Another common detour is trying to juggle a named pipe as a "file object" argument to process. The catch is that a FIFO write blocks until someone on the other end starts reading, which quickly turns into a deadlock if the wiring isn’t carefully arranged. That doesn’t solve the fd 2 question either.

What’s really happening with the fds

There’s no magic here. You can only push data into a child’s STDIN. STDERR is always an output handle: you can read it or redirect it, but you cannot write to the child’s fd 2 from the parent the way you write to STDIN. If the target actually performs an os.read(2, …), it will get nothing from your send calls. To make the program consume data via fd 2, you must duplicate your STDIN pipe onto fd 2 in the child before exec. If you don’t need that, the standard approach is to use send or sendline for input and stderr=PIPE (or a file) to capture error output.

Two practical paths forward

If your program reads from STDIN, keep it simple and stick to STDIN for input while piping its outputs. That gives you clean control over both channels without touching descriptor wiring.

from pwn import process, PIPE

# Feed the program over STDIN; capture both stdout and stderr
runner = process(['./target-app'], stdin=PIPE, stdout=PIPE, stderr=PIPE)

# Write input to STDIN
runner.sendline(b'input-over-stdin')

# Read back whatever you need (stdout/stderr capture is configured)
# Use your normal pwntools reads here

When the target explicitly reads from fd 2, you have to remap descriptors before the exec takes place. The idea is to duplicate fd 0 (your stdin pipe) onto fd 2 so that the child’s os.read(2, …) actually consumes the same stream you write via send. One clean way is to wrap the real binary in a tiny launcher that does the dup and then execs the program.

Create a small shim that duplicates STDIN to fd 2 and chains into the target:

# shim.py
import os, sys

def map_stdin_to_fd2():
    # Duplicate fd 0 (stdin) onto fd 2 (stderr) inside the child
    os.dup2(0, 2)
    # Replace this process with the actual target
    os.execvp(sys.argv[1], sys.argv[1:])

if __name__ == '__main__':
    map_stdin_to_fd2()

Then launch your target through that shim and write as usual to STDIN; the program’s fd 2 will receive it.

from pwn import process, PIPE

feeder = process(['python3', 'shim.py', './target-app'], stdin=PIPE, stdout=PIPE, stderr=PIPE)

# These bytes go to STDIN, which the shim has duplicated onto fd 2
feeder.send(b'bytes-consumed-on-fd2')

# Handle outputs as needed

Why this detail matters

When a binary reads from an unexpected descriptor, naive piping won’t work, and you can burn time chasing "why doesn’t send reach fd 2". Understanding that STDERR is an output-only channel from the parent’s perspective, and that feeding a different fd requires a dup before exec, saves you from fragile workarounds and blocking FIFOs. It also keeps your harness predictable: input always originates from STDIN on your side, and you purposefully remap what the child sees.

Takeaways

If the target consumes STDIN, use send or sendline and route outputs with stdout and stderr parameters; stderr=PIPE (or a file) lets you capture error messages cleanly. If the target consumes fd 2, duplicate your stdin pipe onto fd 2 in the child before exec so that your input stream is visible where the program expects it. That’s the reliable, minimal-change approach that keeps your pwntools workflow straightforward.