Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: runtime: add LockOSThreadRecursive to lock descendent goroutines recursively (for fine-grained Linux Landlock) #70993

Closed
AkihiroSuda opened this issue Dec 25, 2024 · 5 comments
Labels
Milestone

Comments

@AkihiroSuda
Copy link
Contributor

Proposal Details

Currently, runtime.LockOSThread does not lock the caller's descendent goroutines to the caller's OS thread recursively.
I'd like to propose adding a new function LockOSThreadRecursive to allow recursive locking.
This proposal does not affect the behavior of the existing LockOSThread function.

Expected usecase: Landlock

One of the expected usecases is to apply Landlock (Linux kernel's security module) to a specific package/function of Go program while leaving other packages/functions unconfined.
The target package/function will be wrapped with a goroutine that is recursively locked to a specific OS thread.

https://docs.kernel.org/userspace-api/landlock.html

Every new thread resulting from a clone(2) inherits Landlock domain restrictions from its parent. This is similar to seccomp inheritance (cf. Seccomp BPF (SECure COMPuting with filters)) or any other LSM dealing with task’s credentials(7). For instance, one process’s thread may apply Landlock rules to itself, but they will not be automatically applied to other sibling threads (unlike POSIX thread credential changes, cf. nptl(7)).

LockOSThreadRecursive will ensure that the Landlock policy isn't accidentally jailbroken when the goroutine spawns child goroutines.

(Side note: the meaning of "lock" in "Landlock" is different from "lock" in "LockOSThread")

Caveats

A thread confined with Landlock may still affect other threads' behavior by changing other threads' variables, via unsafe, cgo, asm, reflect, /proc/PID/mem, process_vm_writev(2), etc.
These loopholes may be detected with a third-party linter tool.

Relevant discussion

There was once a similar proposal submitted by another person on February 9th, 2018:

Their proposal was rejected on March 6th, 2018: #23758 (comment)

Sorry, but no. This is a whole different scheduler from Go's current design, not just a simple tweak, as @aclements comments start to explain.

However, as the context has changed since 2018 (Landlock wasn't available in the upstream Linux kernel at that time), it might be worth reconsidering the decision.

Implementation

As discussed in 2018, the implementation will not be straightforward.

  • src/runtime/runtime2.go:
    • struct m: lockedg guintptr has to be changed to a slice
  • src/runtime/proc.go:
    • schedule, startlockedm, etc. have to be changed to force preemption of multiple g on a single m
@gopherbot gopherbot added this to the Proposal milestone Dec 25, 2024
@seankhliao
Copy link
Member

Given that this was previously rejected due to implementation complexity on the Go side, I don't see how that has changed significantly enough to warrant a reevaluation.

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Dec 25, 2024
@AkihiroSuda
Copy link
Contributor Author

Given that this was previously rejected due to implementation complexity on the Go side, I don't see how that has changed significantly enough to warrant a reevaluation.

The previous discussion in 2018 predates the introduction of asynchronous preemption (SIGURG on 10ms timeout) in Go 1.14 in 2020.
So it might be still worth reevaluating the complexity.

@ianlancetaylor
Copy link
Member

I don't understand how this could work in practice. If every goroutine is locked to the caller's thread, even the simplest channel communication seems likely to deadlock.

Note that asynchronous preemption did not affect the scheduler in a significant way. The scheduler already supported synchronous preemption, asynchronous preemption just added another preemption path.

@gnoack
Copy link
Contributor

gnoack commented Dec 31, 2024

Hi! I am a Landlock contributor in the Linux kernel and maintainer of the Go-Landlock library.

I also do not believe that this can be made to work - A Landlock sandbox that only applies to a subset of a processes' threads would just be as strong as the boundaries that the runtime can enforce between these (OS) threads, to keep nefarious threads to influence other threads' behavior.

However, threads influencing each others behavior or scheduling work for each other is a normal thing that (intentionally) happens in multi-threaded programs and there are no strong security boundaries between them that would keep them from messing with each other's data (e.g. through object aliasing).

Enforcing a Landlock policy on only one thread would make the entire code in userspace (Go runtime, all loaded program code) the "Trusted Computing Base" for that Landlock sandbox, and it seems unrealistic to harden that in a way so that threads can't influence each other.

TL;DR:

  • When we use Landlock for the full process, the kernel can give guarantees and reason on its own about the security of that.
  • If we used Landlock for individual threads only, we need to make security assumptions about how threads can interact within the process, which are unrealistic to make (and which fully go out of the window once a pooly written cgo dependency is linked).

Also see my remark on the original bug: landlock-lsm/go-landlock#32 (comment)


P.S.: The same reasoning applies to other programming languages as well, which is why "enforcing Landlock for the full process" is one of the first bugs we filed (landlock-lsm/linux#2). The current per-thread restriction is an unfortunate implementation artifact due to the way how the kernel "credential" swizzling is implemented, similar to the way setuid(2) is implemented under the hood.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants