GPTPS runs inside your process. That single fact sets the whole trust model, so this document says plainly what is and is not a boundary. It is short on purpose: an honest small list beats an impressive long one.
Open a GitHub issue for anything that is already public or low-impact. For something exploitable, use GitHub's private vulnerability reporting on the repository's Security tab so a fix can land before the details do.
Task bodies are trusted. A GPTPS_EXEC_INPROC task is a C function pointer
called on a worker thread in your address space. It can read every byte your process
holds, corrupt the engine, and crash the host. This is not a weakness in the design —
it is what "in-process" means, and it is why INPROC is fast. Do not run code you
distrust this way.
Config files and add-on paths are code. gptps_open(path) parses TOML, and a
top-level addons = ["libfoo.so"] array causes each entry to be dlopened. A
writable config file is therefore equivalent to arbitrary code execution in your
process. Treat the config path with exactly the care you would treat a shared
library search path: if an attacker can write it, they own the process.
The engine does not sandbox. There is no seccomp filter, no namespace, no capability drop, and no privilege separation anywhere in the core.
GPTPS_EXEC_OOP and GPTPS_EXEC_PROGRAM give you resource isolation and a
guaranteed kill. That is genuinely valuable and most task systems do not offer it:
- an OS-enforced memory cap (cgroup v2
memory.max+memory.swap.max=0on Linux,RLIMIT_ASelsewhere), with an OOM-kill surfaced asGPTPS_E_NOMEM; - a hard
SIGKILLon the deadline or ongptps_cancel, bounded even when the task declared no timeout; - a crash that takes down only the child, reported as
GPTPS_E_TASK, leaving the host and every other in-flight item untouched.
They do not give you privilege isolation, and this is the part that matters:
GPTPS_EXEC_OOPforks. The child inherits a full copy of the host address space — every secret, key, and token the parent held — plus its file descriptors and its entire environment.GPTPS_EXEC_PROGRAMforks and execs, so the child does not inherit memory, but it does inherit the environment and any descriptor not marked close-on-exec. A bareargv[0]is resolved through the inheritedPATH.- Neither drops privileges. A child of a root process is a root process.
child_setup is the seam where you fix that. It runs in the child after fork
and before the work (or the exec), so it is where chdir, setuid/setgid,
setrlimit, unsetenv, closing inherited descriptors, and installing a seccomp
filter belong. It must be async-signal-safe. If you are running code you actually
distrust, child_setup is not optional — and you should still put the whole process
inside a container or VM, because a seccomp filter you wrote is not a security
boundary a determined attacker respects.
An engine created before a fork() must not be used in the child: its mutex may
be held by a thread that did not survive, so the child would deadlock. GPTPS detects
this and returns GPTPS_E_SHUTDOWN from every entry point rather than hanging — and
the check runs before the lock is taken, because taking it is itself the hang.
gptps_shutdown is included, which matters most: it would otherwise go on to join
dispatcher and worker pthread_ts that do not exist in the child.
tests/test_fork.c forks a live threaded engine and asserts
the refusal across mutating, read-only, settings and teardown entry points.
A child that opens its own engine after forking is fully supported. The POSIX rule
stands regardless: a forked child of a threaded process should exec() or _exit().
(addons/gptps_xport in engine mode is exactly this case: each forked worker opens a
fresh engine of its own after the fork and never touches the parent's. Its handler mode
runs the handler directly and opens no engine.)
GPTPS never changes a process-wide signal disposition — the host owns those. The
POSIX program executor suppresses SIGPIPE per-fd (F_SETNOSIGPIPE) or by masking
it on its own thread, never with signal(SIGPIPE, SIG_IGN).
The converse is a requirement on the host: do not set SIGCHLD to SIG_IGN, and
do not run a wait-any reaper, if you use GPTPS_EXEC_PROGRAM. Either auto-reaps
the executor's children, so its waitpid() returns ECHILD and the exit status is
unrecoverable. GPTPS reports GPTPS_E_TASK rather than assume success — assuming
success would silently convert every failing program into a passing one. On such a
host every GPTPS_EXEC_PROGRAM task therefore fails immediately.
GPTPS_EXEC_OOP is unaffected; it carries its status over its own result pipe.
The engine is bounded by construction, and the bounds are the defence:
| Bound | Setting | Default |
|---|---|---|
| Queued (un-admitted) items | limits.max_intake_depth |
unbounded — set this if submitters are untrusted |
| Concurrently running items | limits.max_concurrent_tasks |
one per detected core |
| Memory admitted at once | limits.max_memory_bytes |
~0.75 × detected RAM |
| Retained dead letters | limits.max_dead_letters |
1024, oldest evicted |
| Shutdown drain | limits.shutdown_grace_ms |
30s, then in-flight work is cancelled |
| Bytes buffered from a child | (compile-time) | 16 MiB per result |
The one default that is deliberately permissive is max_intake_depth: unbounded
intake is right for a host that submits its own work and wrong for one that accepts
work from elsewhere. If a submitter can outrun your workers, set it and handle
GPTPS_E_FULL.
Stated so nobody infers them:
- No authentication, no encryption, and nothing GPTPS ships opens a socket. The
core has no network code at all.
addons/gptps_xportuses asocketpairto its own forked children, and its framing is not authenticated; do not put it on a socket you do not control. addons/gptps_remotedefines a cross-host wire format — and only that. It is a codec: it encodes and decodes bytes and opens nothing. It is deliberately not published as a release artifact, because shipping it is what would create a version-1 peer in the world and make the format permanent. If you build a transport on it, understand what you are taking on: thetaskfield of a request is a dispatch key chosen by the peer, so on a listening socket that is remote code selection by anyone who can connect. Run it inside a trusted boundary — loopback, WireGuard, a TLS terminator, an SSH tunnel — never on an open port. GPTPS will not grow authentication or encryption of its own; that is a different product with a different failure model.- Payloads and results are opaque bytes. Nothing is validated, sanitized, or size-checked beyond the caps above.
- The dead-letter list holds original payloads in memory. If those are sensitive, drain it.
- Settings persistence (
gptps_settings_save) writes a world-readable file with the process umask.