Skip to content

SVE-2026-0001

proxyto unreaped idle child

proxyto, the small PROXY-protocol front-end used in front of applications that do not understand PROXY protocol themselves, left its most recently exited child as a zombie while the parent daemon sat idle. The service kept running normally throughout. Fixed; severity Low.

FixedSeverity: LowComponent: saphira-proxyto

Saphira Linux dragon mascot

At a glance

Identifier
SVE-2026-0001
Title
proxyto unreaped idle child
Component
saphira-proxyto
Status
Fixed.
Severity
Low.

An SVE is Saphira's own public record. It is not a CVE and does not imply that an official CVE identifier has been assigned.

Summary

proxyto is the small PROXY-protocol front-end used in front of applications that do not understand PROXY protocol themselves. It has been running the public Saphira Gopher service successfully for approximately a week.

During routine inspection of the live service, including CPU, memory and process behaviour, an exited proxyto child was found remaining in zombie state while the parent daemon was idle.

This did not stop the service, expose data or cause a crash. The live daemon continued serving Gopher normally and used only around 700 KiB RSS.

What happened

proxyto forks a child to handle a connection.

The original SIGCHLD handler did not reap the child directly. Instead it set a flag:

static volatile sig_atomic_t do_reap = 0;

static void
sigchld(int sig)
{
    (void)sig;
    do_reap = 1;
}

The main accept loop later checked that flag and called waitpid(..., WNOHANG).

SIGCHLD was installed with SA_RESTART.

This detail mattered. When the child exited while the parent was blocked in accept(), the signal handler ran, however SA_RESTART caused accept() to resume rather than return to the main loop. The deferred reap therefore did not happen until another connection arrived.

The result was that the most recently exited child could remain as:

proxyto  ...  Z  [proxyto] <defunct>

while an otherwise idle daemon waited for its next connection.

Repeated testing indicated that this was bounded to the most recent child, rather than an endlessly growing collection of zombies: subsequent activity caused the previous child to be reaped while potentially leaving the newest one behind.

Reproduction and proof

The defect was reproduced by building the pre-fix and fixed binaries side by side in /tmp and running the same suite against both. The pre-fix binary failed the idle check, leaving exactly one zombie child while idle; the fixed binary passed the full suite with zero warnings under -Wall -Wextra -Wpedantic.

A regression test now polls /proc/<daemon>/task/<pid>/children for Z-state entries while the daemon is idle, so this behaviour cannot regress silently.

Resolution

Child reaping was moved into the SIGCHLD handler itself:

static void
sigchld(int sig)
{
    int saved_errno = errno;

    (void)sig;

    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;

    errno = saved_errno;
}

waitpid() is async-signal-safe, all exited children are reaped, and errno is preserved for the interrupted parent code.

The main accept loop no longer needs deferred reap bookkeeping.

Additional verification

During the same inspection, proxyto's privilege handling was verified with strace.

The daemon starts privileged so it can bind the Gopher listener on TCP port 70 and then permanently drops privileges to its dedicated package-owned identity:

getuid()    = 0
setgid(19)  = 0
setuid(19)  = 0
getuid()    = 19
getgid()    = 19
setuid(0)   = -1 EPERM

This proves the process cannot regain root after dropping to proxyto:proxyto UID/GID 19.

Impact

No evidence was found of:

  • • data disclosure
  • • privilege escalation
  • • remote code execution
  • • service interruption
  • • unbounded process-table exhaustion

The defect was nevertheless incorrect process lifecycle handling and left an unnecessary zombie process while the daemon was idle.

Why publish this?

Because Saphira should document its own mistakes with the same care used when discussing everybody else's.

A defect does not need to be catastrophic to deserve understanding.

It was found on the live service, reproduced, explained and fixed.

Transparency means publishing the small ones too.

← All SVE entries