15 comments

[ 2.7 ms ] story [ 44.9 ms ] thread
How does pledge interact with multiple threads?

Is it just "it just takes effect at the time of call and it's up to you to synchronize whatever you need to synchronize"? Anything racing with a pledge call in an observable way is clearly tempting fate anyhow.

Linux pledge is built on top of seccomp, which by default only applies a newly added filter to the current thread, and copies it to all future threads cloned from the current thread. There exists a flag (SECCOMP_FILTER_FLAG_TSYNC) to automatically copy the filter to all other threads of the current process, but the pledge library does not set this flag.

If the flag were used, then each thread's filter tree would be atomically swapped out from its old version, then the thread would be set to be in SECCOMP_MODE_FILTER if it isn't already. Setting the mode causes the SYSCALL_WORK_SECCOMP bit to be set in the thread's syscall_work field, which the thread checks every time it enters a syscall from user mode. Therefore, any currently-running syscall in the thread may or may not go through, but all future syscalls will be subject to the new filter.

If current long-running syscalls are a concern, then I think (but haven't tested) it would be sufficient to define a signal handler without SA_RESTART, then send the corresponding signal to every other thread to interrupt all their syscalls. The handler could even have a counter that gets incremented so that the caller knows when all threads have been interrupted; this is basically how libcs implement a process-wide setuid/setgid on top of Linux's per-thread syscalls.

Does that matter in practice? Isn’t the normal use of pledge to parse your command line arguments, environment and configuration file(s), call pledge to tell the OS what kinds of things you’ll do, and then do them, and isn’t that checking as good as always done before starting a second thread? (Or does libc start threads in some cases?)

Also, the OpenBSD man page (https://man.openbsd.org/pledge.2) states “The pledge() system call forces the current process into a restricted-service operating mode”, indicating it’s process-wide.

Reading https://news.ycombinator.com/item?id=38032251, that seems different from its Linux port.

It probably matters for Go since presumably the go runtime starts up some of it’s worker threads before calling your main. Though actually now I’m not so sure, seems worth digging into.
> the go runtime starts up some of it’s worker threads before calling your main

Of course! I think the chance of that happening must be about 100%. When else would it start such threads?

It would be an interesting bug if somebody managed to get a garbage collector thread to read files or to make network requests fo them…

Yes, that's the problem.

It has mostly died down but a segment of the developer community took great exception to the designers of Go calling it a "systems" language when it had GC, which, as everybody knows, makes it worthless for all tasks ever. (Sarcasm.) However, in my experience, the bigger problem with it as a "systems language" is that the runtime has already spawned off threads by the time the first line of any user-controllable code has been executed, and goroutines get mapped arbitrarily to OS-level threads by the runtime (there's an option to pin a goroutine to the thread it is currently in, but not to a specific thread, IIRC), meaning that anything that is set at the thread level in UNIX is effectively unavailable to you in your current process.

In light of that fact, and in light of the fact I see no code here, I remain unsure how the original poster actually has integrated pledge into Go.

The only C code I have under my maintenance is code whose sole purpose in life is to be run as setuid, take some command line arguments, open some privileged files, start a known executable, and pass the file handles to that executable directly, so the unprivileged user running the executable can not see the contents of those files. (The system is locked down in other ways too, e.g., the users can not run strace to see the contents, LD_PRELOAD is locked down, etc., and as the final defense against internet rando systems designers criticizing this design on the basis of a single sentence, it's not the only line of defense here either.) I would _prefer_ to use Go, but there's no safe way to drop privileges in a Go executable, and for various technical reasons the exec support in Go does not _quite_ cut it. (It is close, but no cigar; we can not quite specify all the details we need from the outside.)

I am not a big fan of Go's design, and express it routinely.

However I dearly appreciate that users of TinyGo or TamaGo couldn't care less what others think a systems language is all about, happily targeting bare metal and firmware workloads.

Then there is the whole point of writing GPU debuggers, compilers and linkers is systems programming.

I was always wondering if it’s possible to figure pledges in compile-time for Go, e.g., declare in your module “I will never access network” and make compiler verify that. (Wouldn’t work for assembly for sure, but probably is okay for most of the modules)
Go lets you run arbitrary shell commands. Even if you couldn't, you can do virtually anything with IO. Even if it had some way to guess the file you're trying to open is a socket, you don't necessarily need a socket. You can be writing to a named pipe or even a tty that serves as the input to ncat which keeps an active tcp session with some C&C server always running as a system daemon. That might sound ridiculous, but real-world malware often works in pairs like that where one process will simply see if another exists, do bad stuff if so, and forget about it if not. They're attacking millions if not billions of hosts, so don't need them all to join the botnet. Only the kernel at runtime knows what you're doing at the level of detail needed to prevent something like this.
I recently saw a post about Cackle [1] coming out of the rust ecosystem which looks pretty cool. Similar to what others have said, there's always a chance that a C lib or shell script is leveraged to bypass compile time guarantees. I think that's why it's important to do both. Do what you can to keep your supply chain safe and limit your runtime as much as possible.

[1] https://davidlattimore.github.io/making-supply-chain-attacks...

/proc would like a word with you

But yes technically such a setup is possible (not with any mainstream OS though). It would have to be very restrictive w.r.t mounts and virtual file systems.

Skimmed the code, seems like https://humungus.tedunangst.com/r/pledge/v/tip/f/pledge.go ought to call xUnveilEnd() (...right?)
Seems like it, though he mentions in the post that you can call pledge (without "unveil") after unveil to prevent further changes (and it'll handle committing the changes on Linux, which isn't necessary on OpenBSD), to achieve the same thing.