If subprocesses don’t inherit sandbox restrictions, can’t you trivially escape the sandbox by spawning a subprocess to do whatever work you want? What exactly are you proposing here?
If subprocesses don’t inherit sandbox restrictions, can’t you trivially escape the sandbox by spawning a subprocess to do whatever work you want? What exactly are you proposing here?
But the problem for ubiquitously minimizing privileges across myriad tools (almost all OpenBSD command-line tools use pledge and/or unveil, now) is that the calling program can't make assumptions about the permissions and files a child program might need. So for example, a PDF viewer (or for the sake of simplicity, let's say a PDF text extractor) might need to access /bin/gunzip. The calling program can't know this ahead of time. So it has no way of reliably including those necessary files in its permitted set. But even if it did, without major refactoring it would end up w/ a union of the privileges it needs itself and of all the privileges its child processes might need, which is the opposite of what you're trying to achieve with privilege minimization. Ultimately, the only program that can truly know--and the only one that should be tasked with knowing--what privileges are needed to perform an operation is the program performing that operation.
(And note that major refactoring, such as splitting a program into a series of interoperating processes w/ discrete privileges sets, is usually not a realistic option. If it were then solutions like Capsicum would be more than adequate. Certainly new software should try to use this approach if it can.)
At the end of the day, when it comes to security you can't really save vulnerable software from itself without touching that software. Attempting to build sandboxes outside that software doesn't work well in practice. Witness SELinux, which most people disable because the rules (invariably written by people other than the authors of the programs) are too brittle and prone to breaking functionality. But neither, of course, is it practical to rearchitect all our software.
The reason why pledge and unveil and their default semantics of non-inheritance work so well is that they provide an optimal middle-ground. Each program concerns itself w/ minimizing its own privileges, and relies on programs it invokes to do the same. And w/ a good API, like pledge and unveil, in a shockingly short period of time those expectations (that a child program does know how to restrict its own permissions) can be substantially met w/ minimal effort. The typical patch to OpenBSD utilities included just several lines of code spread around various critical areas. For example, open or (if you can't open immediately) unveil any file paths passed on the command-line. Ditto for extraneous resources like /bin/gzip or /etc/magic. Then drop as many other privileges as you can as soon as you can, like network access. The appropriate way to do this is always highly dependent on the function and architecture of the program.
Of course, if you're a leaf program then, yes, you want restrictions to be inherited by default in case some bug permits uncontrolled invocation of a utility. But in most of these cases this can be solved by, e.g., blocking exec. As a framework matures, sure, extensions can be added for the niche cases where you want to toggle inheritance and/or pre-specify restrictions as the invoker of another program. But these features should not be the emphasis, and they definitely shouldn't be provided in lieu of interfaces for non-inherited restrictions.
In the first problem we want to allow programs to drop privileges, like a good citizen and also spawn programs with dropped privileges, which requires some sort of inheritance. Modulating the inheritance part seems vital to allow it. As you mentioned the gunzip issue. We could also imagine, that if a program is allowed to run other programs and without inheritance it could just run tee to write files wherever it wants.
That's why on Android and other similar systems an app has to declare it's limits. Then it can still do work on behalf of another app via intents - in a way pipe/fork/exec. Limits are not inherited then. You can always limit what the application is allowed to run or communicate with.
The argument for pledge and unveil isn't that it's conceptually superior to, e.g., a pure object capabilities system; it's that in practice it's the interface most likely to be used effectively by software in the Unix programming environment to augment the existing model.
This is a recurring criticism from BSD fans, but unfortunately I think it mostly reminds us that humans are lazy and isn't a reflection of an actual problem. This machine, which does a bunch of stuff, turns out to have had SELinux in enforcing mode for many years, I wasn't sure actually so I had to go check when I began this rant, but yup, it was enabled when I installed whatever Fedora was current when my last PC's power supply gave up, maybe 2015 or so? I think if you could just disable pledge, you'd see "fixes" that do that too, for the same reason.
It's true that "disable SELinux" is a common "fix" proposed for problems, but it is also true that "disable certificate validation" is a common "fix" (even today people will copy-paste a "disable all validation" configuration into their app in hopes it'll solve some unrelated issue), and I'm quite sure if I poke around in Rust forums I'll find the same sort of person recommending just sticking everything inside an Unsafe block to "fix" a problem too... Just in case it feels like I'm sticking the boot into our industry here, this happens even in safety critical environments, it's just that they have more fail-safes than we do, so here's a story about a non-IT lazy "fix":
Railway signaller signs on for his night shift, the mechanical box he works in has had some work done that day, but is signed off as OK. Some time later that evening, he gets a call, driver is sat at a Danger signal for some time. He tries to release the signal to Clear, but it won't release. -Sigh- he figures those maintenance idiots have just fouled up the interlocks, but the rulebook is clear about how to override this, so he gives a rote speech, something along the lines of "Pass this signal at caution, prepared to stop at any obstruction, and obey all further signals". Next signal, the same happens. Same drill, "Pass this signal at caution... blah blah blah". Next signal the same. Wow, those clowns working on the box have really messed it up. And then two drivers call him. Because they are now face-to-face.
There had been a physical points failure (something actually broke, wore out, etc.), and the reason all those signals wouldn't release is that the interlock makes that release conditional on the "Points locked" status ahead, which if the signaller had looked was now reading "Failed". But he didn't look, because humans are lazy, and so instead of passing harmlessly on the opposite track the train being "cautioned through" was actually sent towards its sister instead. Fortunately the "fail safe" instruction to proceed only at caution averted catastrophe by giving the driver enough time to brake once he realised the problem.
Isn't this the core of the solution? Why shouldn't the authors of the program themselves supply the expected boundaries of the program they've written?
Point is, SELinux is not bound by the same problem you explain: it uses exec() as a domain boundary, so a PDFviewer calling gunzip would transition to the gunzip enforcement domain (if so configured), allowing both gunzip and the PDFviewer to run with minimal permissions.
I can't say I agree with this. If it were true, containers would not be so ubiquitous.
There is a reason you have to run your containers as NON ROOT. It's because containers can leak root access(last I checked, though I doubt it's changed any).
Pledges must reflect the capabilities of a program. Yes the "proc" and "exec" pledge promises are powerful but they can be constrained by their environment such as chroots or calls to unveil.
As an aside if you have very good knowledge of what a pledged program executes you can opt into setting up pledge for it ahead of time in the form of execpromises.