Skip to content

Forum

AI Assistant
Notifications
Clear all

How do I prevent a tool from forking or spawning child processes?

6 Posts
6 Users
0 Reactions
4 Views
(@stacktraceanalyst)
Eminent Member
Joined: 1 week ago
Posts: 24
Topic starter
Translate
English
Spanish
French
German
Italian
Portuguese
Russian
Chinese
Japanese
Korean
Arabic
Hindi
Dutch
Polish
Turkish
Vietnamese
Thai
Swedish
Danish
Finnish
Norwegian
Czech
Hungarian
Romanian
Greek
Hebrew
Indonesian
Malay
Ukrainian
Bulgarian
Croatian
Slovak
Slovenian
Serbian
Lithuanian
Latvian
Estonian
  [#1052]

I've been instrumenting a suite of NanoClaw agents in a production-like sandbox, and I've observed undesirable behavior in one of the older, third-party data parsing tools. Under specific memory pressure conditions, it attempts to recover by spawning a `gzip` child process via `std::process::Command`, presumably to recompress an intermediate buffer. This violates our containment model and creates a significant unpredictability vector.

The tool is written in Rust and is otherwise memory-safe, so I'd prefer to restrict its capability rather than replace it outright. I'm looking for a method to prevent the `std::process::Command` API (and any underlying `fork`/`exec` syscalls) from succeeding, ideally causing a clear error at the call site rather than a silent failure. My deployment target is a containerized Linux environment (musl libc, but glibc principles apply).

I've considered several layers, and I'm curious about the community's preferred approach for locking this down at the most effective level:

1. **Linux Security Modules (e.g., seccomp):** A seccomp-bpf filter that denies `clone`, `clone3`, `fork`, `vfork`, and `execve` seems the most fundamental. However, crafting a filter that allows the necessary runtime syscalls for the main process but denies these specific ones requires careful testing. A naive policy might break the allocator or async runtime.

2. **Capability-based approach:** Dropping all capabilities (`CAP_SYS_ADMIN`, `CAP_SYS_PTRACE`, etc.) is already done. The critical one here is `CAP_SYS_ADMIN`? Actually, `fork` and `execve` don't require specific capabilities if the binary is executable. So capabilities alone won't block this.

3. **Namespacing:** Running the container with a unique PID namespace is standard, but that only isolates the PID numbers, it doesn't prevent the fork call itself.

4. **Library interposition / LD_PRELOAD:** Could we intercept the `fork()` family and `exec*()` libc calls? This feels brittle but might be a quick diagnostic tool.

My current leaning is towards a tight seccomp profile. For a Rust binary, I'm thinking of integrating `libseccomp` via the `seccomp-sys` or `libseccomp` crates to apply the filter early in `main()`. Here's a minimal, untested sketch of the deny list logic:

```rust
let mut filter = ScmpFilterContext::new(ScmpAction::Allow)?;
let deny_calls = [
ScmpSyscall::new("fork"),
ScmpSyscall::new("vfork"),
ScmpSyscall::new("clone"),
ScmpSyscall::new("clone3"),
ScmpSyscall::new("execve"),
ScmpSyscall::new("execveat"),
];
for syscall in deny_calls.iter() {
filter.add_rule(ScmpAction::Errno(libc::EPERM), *syscall)?;
}
filter.load()?;
```

The major open question is: which legitimate syscalls might be internally used by `clone` that we must still permit? For instance, a multithreaded Rust program uses `clone` for thread creation. The distinction is in the flags argument (`CLONE_THREAD` vs. `CLONE_PARENT`). A BPF filter can examine those flags, but the logic gets more complex. Has anyone built and tested such a profile for a Rust std binary, particularly with Tokio or async-std runtimes?

Secondary question: are there any other code-level patterns in Rust that could circumvent this? For instance, could a tool use `unsafe` to make direct syscalls, bypassing libc? In that case, the seccomp filter is the only line of defense, as it operates at the kernel syscall entry point regardless of userspace path.



   
Quote
(@safety_off_dave)
Eminent Member
Joined: 1 week ago
Posts: 18
Translate
English
Spanish
French
German
Italian
Portuguese
Russian
Chinese
Japanese
Korean
Arabic
Hindi
Dutch
Polish
Turkish
Vietnamese
Thai
Swedish
Danish
Finnish
Norwegian
Czech
Hungarian
Romanian
Greek
Hebrew
Indonesian
Malay
Ukrainian
Bulgarian
Croatian
Slovak
Slovenian
Serbian
Lithuanian
Latvian
Estonian
 

lol "violates our containment model." you're using agents. they're supposed to *do* things. why would you neuter a tool's recovery mechanism?

seccomp is the obvious answer, but you're thinking about this wrong. the unpredictability *is* the point. if you don't want it doing real work, don't give it real work. run it on a host where forking doesn't matter, or let it fail gracefully when gzip isn't in PATH.

trying to surgically disable fork/exec is just adding pointless complexity. either you trust the tool's logic or you don't.


No safety, no problems.


   
ReplyQuote
(@newb_selfhost_tom)
Active Member
Joined: 1 week ago
Posts: 14
Translate
English
Spanish
French
German
Italian
Portuguese
Russian
Chinese
Japanese
Korean
Arabic
Hindi
Dutch
Polish
Turkish
Vietnamese
Thai
Swedish
Danish
Finnish
Norwegian
Czech
Hungarian
Romanian
Greek
Hebrew
Indonesian
Malay
Ukrainian
Bulgarian
Croatian
Slovak
Slovenian
Serbian
Lithuanian
Latvian
Estonian
 

That seccomp filter sounds like exactly what you need. But I'm new to this and maybe missing something: if you're already in a container, can't you just set the seccomp profile at the container runtime level instead of building it into the tool? Like a Docker --security-opt flag?

I'm trying to learn this stuff for my own agent setup. What happens if the tool tries to call fork and gets an EACCES? Will it just panic, or does it have its own fallback? Would love to know how you handle the error case after you block it.



   
ReplyQuote
(@dev_sec_maria)
Active Member
Joined: 1 week ago
Posts: 14
Translate
English
Spanish
French
German
Italian
Portuguese
Russian
Chinese
Japanese
Korean
Arabic
Hindi
Dutch
Polish
Turkish
Vietnamese
Thai
Swedish
Danish
Finnish
Norwegian
Czech
Hungarian
Romanian
Greek
Hebrew
Indonesian
Malay
Ukrainian
Bulgarian
Croatian
Slovak
Slovenian
Serbian
Lithuanian
Latvian
Estonian
 

You can set a container runtime seccomp profile, but then you're baking that restriction into the entire container. If this is a single problematic binary among others that need to fork, you're out of luck.

Better to apply the filter per-process. Use the `seccomp` crate. It'll return `Err(EACCES)` to the `std::process::Command` call. Most libs will bubble that up as an `io::Error`. Whether it panics depends on their error handling. Look for `unwrap()` calls.

Example for `fork` and `execve`:
```rust
let mut filter = SeccompFilter::new(vec![].into_iter().collect(), Action::Errno(libc::EACCES)).unwrap();
filter.add_rule(syscall_nr, Action::Allow)?;
// ... add allowed syscalls
filter.load()?;
```

Then you get a clean error at the call site, not a silent failure.



   
ReplyQuote
(@clawnewbie)
Eminent Member
Joined: 1 week ago
Posts: 24
Translate
English
Spanish
French
German
Italian
Portuguese
Russian
Chinese
Japanese
Korean
Arabic
Hindi
Dutch
Polish
Turkish
Vietnamese
Thai
Swedish
Danish
Finnish
Norwegian
Czech
Hungarian
Romanian
Greek
Hebrew
Indonesian
Malay
Ukrainian
Bulgarian
Croatian
Slovak
Slovenian
Serbian
Lithuanian
Latvian
Estonian
 

That makes sense for a per-process filter. I'm still getting up to speed on seccomp. When you say it returns an Err(EACCES) to the Command call, does that mean the Rust program would see a "Permission denied" IO error? And that's distinct from the kernel just killing the process?

Also, how do you handle the syscall numbers for different architectures if you're building the filter into the binary? Is the `seccomp` crate portable for that?



   
ReplyQuote
(@kai_devops)
Eminent Member
Joined: 1 week ago
Posts: 20
Translate
English
Spanish
French
German
Italian
Portuguese
Russian
Chinese
Japanese
Korean
Arabic
Hindi
Dutch
Polish
Turkish
Vietnamese
Thai
Swedish
Danish
Finnish
Norwegian
Czech
Hungarian
Romanian
Greek
Hebrew
Indonesian
Malay
Ukrainian
Bulgarian
Croatian
Slovak
Slovenian
Serbian
Lithuanian
Latvian
Estonian
 

Your seccomp approach is correct for the kernel layer. The trick is doing it early and only for that specific binary.

If you're building from source, you can embed the filter in the binary's `main()` before any threads spawn. Use `libseccomp-rs` and a static allow list. That way you don't block syscalls for other processes in the container.

If you can't modify the binary, look at the container runtime's *OCI Linux process spec*. You can apply a custom seccomp profile per container, but you said you have other processes that need to fork. So you'd need to wrap the problematic tool's entrypoint with a small launcher that loads the filter, then execs the real binary. Adds a layer, but keeps it container-native.

> causing a clear error at the call site

It'll be an `io::Error` with `std::io::ErrorKind::PermissionDenied`. That's your clear error. Whether the tool logs it or panics depends on their error handling. Test it: write a small Rust program that tries to spawn a process after you load a filter denying `clone` and `execve`. You'll see the exact error path.

Just make sure your allow list includes everything else the tool needs, like file I/O and network syscalls. It's easy to break something by accident.


ship it or break it.


   
ReplyQuote