Message ID | 20250206105435.2159977-1-memxor@gmail.com (mailing list archive) |
---|---|
Headers | show |
Series | Resilient Queued Spin Lock | expand |
On Thu, Feb 06, 2025 at 02:54:08AM -0800, Kumar Kartikeya Dwivedi wrote: > Additionally, eBPF programs attached to different parts of the kernel > can introduce new control flow into the kernel, which increases the > likelihood of deadlocks in code not written to handle reentrancy. There > have been multiple syzbot reports surfacing deadlocks in internal kernel > code due to the diverse ways in which eBPF programs can be attached to > different parts of the kernel. By switching the BPF subsystem’s lock > usage to rqspinlock, all of these issues can be mitigated at runtime. Only if the called stuff is using this new lock. IIRC we've had a number of cases where eBPF was used to tie together 'normal' kernel functions in a way that wasn't sound. You can't help there. As an example, eBPF calling strncpy_from_user(), which ends up in fault injection and badness happens -- this has been since fixed, but still.
On Thu, Feb 06, 2025 at 02:54:08AM -0800, Kumar Kartikeya Dwivedi wrote: > Deadlock Detection > ~~~~~~~~~~~~~~~~~~ > We handle two cases of deadlocks: AA deadlocks (attempts to acquire the > same lock again), and ABBA deadlocks (attempts to acquire two locks in > the opposite order from two distinct threads). Variants of ABBA > deadlocks may be encountered with more than two locks being held in the > incorrect order. These are not diagnosed explicitly, as they reduce to > ABBA deadlocks. > > Deadlock detection is triggered immediately when beginning the waiting > loop of a lock slow path. > > While timeouts ensure that any waiting loops in the locking slow path > terminate and return to the caller, it can be excessively long in some > situations. While the default timeout is short (0.5s), a stall for this > duration inside the kernel can set off alerts for latency-critical > services with strict SLOs. Ideally, the kernel should recover from an > undesired state of the lock as soon as possible. > > A multi-step strategy is used to recover the kernel from waiting loops > in the locking algorithm which may fail to terminate in a bounded amount > of time. > > * Each CPU maintains a table of held locks. Entries are inserted and > removed upon entry into lock, and exit from unlock, respectively. > * Deadlock detection for AA locks is thus simple: we have an AA > deadlock if we find a held lock entry for the lock we’re attempting > to acquire on the same CPU. > * During deadlock detection for ABBA, we search through the tables of > all other CPUs to find situations where we are holding a lock the > remote CPU is attempting to acquire, and they are holding a lock we > are attempting to acquire. Upon encountering such a condition, we > report an ABBA deadlock. > * We divide the duration between entry time point into the waiting loop > and the timeout time point into intervals of 1 ms, and perform > deadlock detection until timeout happens. Upon entry into the slow > path, and then completion of each 1 ms interval, we perform detection > of both AA and ABBA deadlocks. In the event that deadlock detection > yields a positive result, the recovery happens sooner than the > timeout. Otherwise, it happens as a last resort upon completion of > the timeout. > > Timeouts > ~~~~~~~~ > Timeouts act as final line of defense against stalls for waiting loops. > The ‘ktime_get_mono_fast_ns’ function is used to poll for the current > time, and it is compared to the timestamp indicating the end time in the > waiter loop. Each waiting loop is instrumented to check an extra > condition using a macro. Internally, the macro implementation amortizes > the checking of the timeout to avoid sampling the clock in every > iteration. Precisely, the timeout checks are invoked every 64k > iterations. > > Recovery > ~~~~~~~~ I'm probably bad at reading, but I failed to find anything that explained how you recover from a deadlock. Do you force unload the BPF program?
On Thu, Feb 06, 2025 at 02:54:08AM -0800, Kumar Kartikeya Dwivedi wrote: > Changelog: > ---------- > v1 -> v2 > v1: https://lore.kernel.org/bpf/20250107140004.2732830-1-memxor@gmail.com > > * Address nits from Waiman and Peter > * Fix arm64 WFE bug pointed out by Peter. What's the state of that smp_cond_relaxed_timeout() patch-set? That still seems like what you're needing, right?
On Mon, Feb 10, 2025 at 10:38:41AM +0100, Peter Zijlstra wrote: > On Thu, Feb 06, 2025 at 02:54:08AM -0800, Kumar Kartikeya Dwivedi wrote: > > > > Deadlock Detection > > ~~~~~~~~~~~~~~~~~~ > > We handle two cases of deadlocks: AA deadlocks (attempts to acquire the > > same lock again), and ABBA deadlocks (attempts to acquire two locks in > > the opposite order from two distinct threads). Variants of ABBA > > deadlocks may be encountered with more than two locks being held in the > > incorrect order. These are not diagnosed explicitly, as they reduce to > > ABBA deadlocks. > > > > Deadlock detection is triggered immediately when beginning the waiting > > loop of a lock slow path. > > > > While timeouts ensure that any waiting loops in the locking slow path > > terminate and return to the caller, it can be excessively long in some > > situations. While the default timeout is short (0.5s), a stall for this > > duration inside the kernel can set off alerts for latency-critical > > services with strict SLOs. Ideally, the kernel should recover from an > > undesired state of the lock as soon as possible. > > > > A multi-step strategy is used to recover the kernel from waiting loops > > in the locking algorithm which may fail to terminate in a bounded amount > > of time. > > > > * Each CPU maintains a table of held locks. Entries are inserted and > > removed upon entry into lock, and exit from unlock, respectively. > > * Deadlock detection for AA locks is thus simple: we have an AA > > deadlock if we find a held lock entry for the lock we’re attempting > > to acquire on the same CPU. > > * During deadlock detection for ABBA, we search through the tables of > > all other CPUs to find situations where we are holding a lock the > > remote CPU is attempting to acquire, and they are holding a lock we > > are attempting to acquire. Upon encountering such a condition, we > > report an ABBA deadlock. > > * We divide the duration between entry time point into the waiting loop > > and the timeout time point into intervals of 1 ms, and perform > > deadlock detection until timeout happens. Upon entry into the slow > > path, and then completion of each 1 ms interval, we perform detection > > of both AA and ABBA deadlocks. In the event that deadlock detection > > yields a positive result, the recovery happens sooner than the > > timeout. Otherwise, it happens as a last resort upon completion of > > the timeout. > > > > Timeouts > > ~~~~~~~~ > > Timeouts act as final line of defense against stalls for waiting loops. > > The ‘ktime_get_mono_fast_ns’ function is used to poll for the current > > time, and it is compared to the timestamp indicating the end time in the > > waiter loop. Each waiting loop is instrumented to check an extra > > condition using a macro. Internally, the macro implementation amortizes > > the checking of the timeout to avoid sampling the clock in every > > iteration. Precisely, the timeout checks are invoked every 64k > > iterations. > > > > Recovery > > ~~~~~~~~ > > I'm probably bad at reading, but I failed to find anything that > explained how you recover from a deadlock. > > Do you force unload the BPF program? Even the simple AB-BA case, CPU0 CPU1 lock-A lock-B lock-B lock-A <- just having a random lock op return -ETIMO doesn't actually solve anything. Suppose CPU1's lock-A will time out; it will have to unwind and release lock-B before CPU0 can make progress. Worse, if CPU1 isn't quick enough to unwind and release B, then CPU0's lock-B will also time out. At which point they'll both try again and you're stuck in the same place, no? Given you *have* to unwind to make progress; why not move the entire thing to a wound-wait style lock? Then you also get rid of the whole timeout mess.
Peter Zijlstra <peterz@infradead.org> writes: > On Thu, Feb 06, 2025 at 02:54:08AM -0800, Kumar Kartikeya Dwivedi wrote: >> Changelog: >> ---------- >> v1 -> v2 >> v1: https://lore.kernel.org/bpf/20250107140004.2732830-1-memxor@gmail.com >> >> * Address nits from Waiman and Peter >> * Fix arm64 WFE bug pointed out by Peter. > > What's the state of that smp_cond_relaxed_timeout() patch-set? Just waiting for review comments: https://lore.kernel.org/lkml/20250203214911.898276-1-ankur.a.arora@oracle.com/ -- ankur
On Mon, Feb 10, 2025 at 2:49 AM Peter Zijlstra <peterz@infradead.org> wrote: > > > > Do you force unload the BPF program? Not yet. As you can imagine, cancelling bpf program is much harder than sending sigkill to the user space process. The prog needs to safely free all the resources it holds. This work was ongoing for a couple years now with numerous discussions. Many steps in-between are being considered as well. Including detaching misbehaving prog, but there is always a counter argument. > Even the simple AB-BA case, > > CPU0 CPU1 > lock-A lock-B > lock-B lock-A <- > > just having a random lock op return -ETIMO doesn't actually solve > anything. Suppose CPU1's lock-A will time out; it will have to unwind > and release lock-B before CPU0 can make progress. > > Worse, if CPU1 isn't quick enough to unwind and release B, then CPU0's > lock-B will also time out. > > At which point they'll both try again and you're stuck in the same > place, no? Not really. You're missing that deadlock is not a normal case. As soon as we have cancellation logic working we will be "sigkilling" prog where deadlock was detected. In this patch the verifier guarantees that the prog must check the return value from bpf_res_spin_lock(). The prog cannot keep re-trying. The only thing it can do is to exit. Failing to grab res_spin_lock() is not a normal condition. The prog has to implement a fallback path for it, but it has the look and feel of normal spin_lock and algorithms are written assuming that the lock will be taken. If res_spin_lock errors, it's a bug in the prog or the prog was invoked from an unexpected context. Same thing for patches 19,20,21 where we're addressing years of accumulated tech debt in the bpf core parts, like bpf hashmap. Once res_spin_lock() fails in kernel/bpf/hashtab.c the bpf_map_update_elem() will return EBUSY (just like it does now when it detects re-entrance on bucket lock). This is no retry. If res_spin_lock fails in bpf hashmap it's 99% case of syzbot doing "clever" attaching of bpf progs to bpf internals and trying hard to break things. > Given you *have* to unwind to make progress; why not move the entire > thing to a wound-wait style lock? Then you also get rid of the whole > timeout mess. We looked at things like ww_mutex_lock, but they don't fit. wound-wait is for databases where deadlock is normal and expected. The transaction has to be aborted and retried. res_spin_lock is different. It's kinda safe spin_lock that doesn't brick the kernel. To be a drop in replacement it has to perform at the same speed as spin_lock. Hence the massive benchmarking effort that you see in the cover letter. That's also the reason to keep it 4 bytes. We don't want to increase it to 8 or whatever unless it's absolutely necessary. In the other email you say: > And it seems to me this thing might benefit somewhat significantly from > adding this little extra bit. referring to optimization that 8 byte res_spin_lock can potentially do O(1) ABBA deadlock detection instead of O(NR_CPUS). That was a conscious trade-off. Deadlocks are not normal. If it takes a bit longer to detect it's fine. The res_spin_lock is optimized to proceed as normal qspinlock.
On Mon, Feb 10, 2025 at 08:37:06PM -0800, Alexei Starovoitov wrote: > On Mon, Feb 10, 2025 at 2:49 AM Peter Zijlstra <peterz@infradead.org> wrote: > > > > > > Do you force unload the BPF program? > > Not yet. As you can imagine, cancelling bpf program is much > harder than sending sigkill to the user space process. So you are killing the user program? Because it wasn't at all clear what if anything is done when this failure case is tripped. > The prog needs to safely free all the resources it holds. > This work was ongoing for a couple years now with numerous discussions. Well, for you maybe, I'm new here. This is only the second submission, and really only the first one I got to mostly read. > > Even the simple AB-BA case, > > > > CPU0 CPU1 > > lock-A lock-B > > lock-B lock-A <- > > > > just having a random lock op return -ETIMO doesn't actually solve > > anything. Suppose CPU1's lock-A will time out; it will have to unwind > > and release lock-B before CPU0 can make progress. > > > > Worse, if CPU1 isn't quick enough to unwind and release B, then CPU0's > > lock-B will also time out. > > > > At which point they'll both try again and you're stuck in the same > > place, no? > > Not really. You're missing that deadlock is not a normal case. Well, if this is unpriv user programs, you should most definitely consider them the normal case. Must assume user space is malicious. > As soon as we have cancellation logic working we will be "sigkilling" > prog where deadlock was detected. Ah, so that's the plan, but not yet included here? This means that every BPF program invocation must be 'cancellable'? What if kernel thread is hitting tracepoint or somesuch? So much details not clear to me and not explained either :/ > In this patch the verifier guarantees that the prog must check > the return value from bpf_res_spin_lock(). Yeah, but so what? It can check and still not do the right thing. Only checking the return value is consumed somehow doesn't really help much. > The prog cannot keep re-trying. > The only thing it can do is to exit. Right, but it might have already modified things, how are you going to recover from that? > Failing to grab res_spin_lock() is not a normal condition. If you're going to be exposing this to unpriv, I really do think you should assume it to be the normal case. > The prog has to implement a fallback path for it, But verifier must verify it is sane fallback, how can it do that? > > Given you *have* to unwind to make progress; why not move the entire > > thing to a wound-wait style lock? Then you also get rid of the whole > > timeout mess. > > We looked at things like ww_mutex_lock, but they don't fit. > wound-wait is for databases where deadlock is normal and expected. > The transaction has to be aborted and retried. Right, which to me sounds exactly like what you want for unpriv. Have the program structured such that it must acquire all locks before it does a modification / store -- and have the verifier enforce this. Then any lock failure can be handled by the bpf core, not the program itself. Core can unlock all previously acquired locks, and core can either re-attempt the program or 'skip' it after N failures. It does mean the bpf core needs to track the acquired locks -- which you already do, except it becomes mandatory, prog cannot acquire more than ~32 locks. > res_spin_lock is different. It's kinda safe spin_lock that doesn't > brick the kernel. Well, 1/2 second is pretty much bricked imo. > That was a conscious trade-off. Deadlocks are not normal. I really do think you should assume they are normal, unpriv and all that.
On Tue, Feb 11, 2025 at 2:44 AM Peter Zijlstra <peterz@infradead.org> wrote: > > On Mon, Feb 10, 2025 at 08:37:06PM -0800, Alexei Starovoitov wrote: > > On Mon, Feb 10, 2025 at 2:49 AM Peter Zijlstra <peterz@infradead.org> wrote: > > > > > > > > Do you force unload the BPF program? > > > > Not yet. As you can imagine, cancelling bpf program is much > > harder than sending sigkill to the user space process. > > So you are killing the user program? Because it wasn't at all clear what > if anything is done when this failure case is tripped. No. We're not killing the user process. bpf progs often run when there is no owner process. They're just attached somewhere and doing things. Like XDP firewall will work just fine without any user space. > > The prog needs to safely free all the resources it holds. > > This work was ongoing for a couple years now with numerous discussions. > > Well, for you maybe, I'm new here. This is only the second submission, > and really only the first one I got to mostly read. > > > > Even the simple AB-BA case, > > > > > > CPU0 CPU1 > > > lock-A lock-B > > > lock-B lock-A <- > > > > > > just having a random lock op return -ETIMO doesn't actually solve > > > anything. Suppose CPU1's lock-A will time out; it will have to unwind > > > and release lock-B before CPU0 can make progress. > > > > > > Worse, if CPU1 isn't quick enough to unwind and release B, then CPU0's > > > lock-B will also time out. > > > > > > At which point they'll both try again and you're stuck in the same > > > place, no? > > > > Not really. You're missing that deadlock is not a normal case. > > Well, if this is unpriv user programs, you should most definitely > consider them the normal case. Must assume user space is malicious. Ohh. No unpriv here. Since spectre was discovered unpriv bpf died. BPF_UNPRIV_DEFAULT_OFF=y was the default for distros and all hyperscalers for quite some time. > > As soon as we have cancellation logic working we will be "sigkilling" > > prog where deadlock was detected. > > Ah, so that's the plan, but not yet included here? This means that every > BPF program invocation must be 'cancellable'? What if kernel thread is > hitting tracepoint or somesuch? > > So much details not clear to me and not explained either :/ Yes. The plan is to "kill" bpf prog when it misbehaves. But this is orthogonal to this res_spin_lock set which is a building block. > Right, but it might have already modified things, how are you going to > recover from that? Tracking resources acquisition and release by the bpf prog is a normal verifier job. When bpf prog does bpf_rcu_read_lock() the verifier makes sure that all execution paths from there on have bpf_rcu_read_unlock() before program reaches the exit. Same thing with locks. If bpf_res_spin_lock() succeeds the verifier will make sure there is matching bpf_res_spin_unlock(). If some resource was acquired before bpf_res_spin_lock() and it returned -EDEADLK the verifier will not allow early return without releasing all acquired resources. > > Failing to grab res_spin_lock() is not a normal condition. > > If you're going to be exposing this to unpriv, I really do think you > should assume it to be the normal case. No unpriv for foreseeable future. > > The prog has to implement a fallback path for it, > > But verifier must verify it is sane fallback, how can it do that? > > > > Given you *have* to unwind to make progress; why not move the entire > > > thing to a wound-wait style lock? Then you also get rid of the whole > > > timeout mess. > > > > We looked at things like ww_mutex_lock, but they don't fit. > > wound-wait is for databases where deadlock is normal and expected. > > The transaction has to be aborted and retried. > > Right, which to me sounds exactly like what you want for unpriv. > > Have the program structured such that it must acquire all locks before > it does a modification / store -- and have the verifier enforce this. > Then any lock failure can be handled by the bpf core, not the program > itself. Core can unlock all previously acquired locks, and core can > either re-attempt the program or 'skip' it after N failures. We definitely don't want to bpf core to keep track of acquired resources. That just doesn't scale. There could be rcu_read_locks, all kinds of refcounted objects, locks taken, and so on. The verifier makes sure that the program does the release no matter what the execution path. That's how it scales. On my devserver I have 152 bpf programs running. All of them keep acquiring and releasing resources (locks, sockets, memory) million times a second. The verifier checks that each prog is doing its job individually. > It does mean the bpf core needs to track the acquired locks -- which you > already do, We don't. The bpf infra does static checks only. The core doesn't track objects at run-time. The only exceptions are map elements. bpf prog might store an acquired object in a map. Only in that case bpf infra will free that object when it frees the whole map. But that doesn't apply to short lived things like RCU CS and locks. Those cannot last long. They must complete within single execution of the prog. > > That was a conscious trade-off. Deadlocks are not normal. > > I really do think you should assume they are normal, unpriv and all > that. No unpriv and no, we don't want deadlocks to be considered normal by bpf users. They need to hear "fix your broken prog" message loud and clear. Patch 14 splat is a step in that direction. Currently it's only for in-kernel res_spin_lock() usage (like in bpf hashtab). Eventually we will deliver the message to users without polluting dmesg. Still debating the actual mechanism.
On Tue, Feb 11, 2025 at 10:33:00AM -0800, Alexei Starovoitov wrote: > Ohh. No unpriv here. > Since spectre was discovered unpriv bpf died. > BPF_UNPRIV_DEFAULT_OFF=y was the default for distros and > all hyperscalers for quite some time. Ah, okay. Time to remove the option then? > > So much details not clear to me and not explained either :/ > > Yes. The plan is to "kill" bpf prog when it misbehaves. > But this is orthogonal to this res_spin_lock set which is > a building block. > > > Right, but it might have already modified things, how are you going to > > recover from that? > > Tracking resources acquisition and release by the bpf prog > is a normal verifier job. > When bpf prog does bpf_rcu_read_lock() the verifier makes sure > that all execution paths from there on have bpf_rcu_read_unlock() > before program reaches the exit. > Same thing with locks. Ah, okay, this wasn't stated anywhere. This is rather crucial information. > If bpf_res_spin_lock() succeeds the verifier will make sure > there is matching bpf_res_spin_unlock(). > If some resource was acquired before bpf_res_spin_lock() and > it returned -EDEADLK the verifier will not allow early return > without releasing all acquired resources. Good. > > Have the program structured such that it must acquire all locks before > > it does a modification / store -- and have the verifier enforce this. > > Then any lock failure can be handled by the bpf core, not the program > > itself. Core can unlock all previously acquired locks, and core can > > either re-attempt the program or 'skip' it after N failures. > > We definitely don't want to bpf core to keep track of acquired resources. > That just doesn't scale. > There could be rcu_read_locks, all kinds of refcounted objects, > locks taken, and so on. > The verifier makes sure that the program does the release no matter > what the execution path. > That's how it scales. > On my devserver I have 152 bpf programs running. > All of them keep acquiring and releasing resources (locks, sockets, > memory) million times a second. > The verifier checks that each prog is doing its job individually. Well, this patch set tracks the held lock stack -- which is required in order to do the deadlock thing after all. > > It does mean the bpf core needs to track the acquired locks -- which you > > already do, > > We don't. This patch set does exactly that. Is required for deadlock analysis. > The bpf infra does static checks only. > The core doesn't track objects at run-time. > The only exceptions are map elements. > bpf prog might store an acquired object in a map. > Only in that case bpf infra will free that object when it frees > the whole map. > But that doesn't apply to short lived things like RCU CS and > locks. Those cannot last long. They must complete within single > execution of the prog. Right. Held lock stack is like that. > > > That was a conscious trade-off. Deadlocks are not normal. > > > > I really do think you should assume they are normal, unpriv and all > > that. > > No unpriv and no, we don't want deadlocks to be considered normal > by bpf users. They need to hear "fix your broken prog" message loud > and clear. Patch 14 splat is a step in that direction. > Currently it's only for in-kernel res_spin_lock() usage > (like in bpf hashtab). Eventually we will deliver the message to users > without polluting dmesg. Still debating the actual mechanism. OK; how is the user supposed to handle locking two hash buckets? Does the BPF prog create some global lock to serialize the multi bucket case? Anyway, I wonder. Since the verifier tracks all this, it can determine lock order for the prog. Can't it do what lockdep does and maintain lock order graph of all loaded BPF programs? This is load-time overhead, rather than runtime.
On Thu, Feb 13, 2025 at 1:59 AM Peter Zijlstra <peterz@infradead.org> wrote: > > On Tue, Feb 11, 2025 at 10:33:00AM -0800, Alexei Starovoitov wrote: > > > Ohh. No unpriv here. > > Since spectre was discovered unpriv bpf died. > > BPF_UNPRIV_DEFAULT_OFF=y was the default for distros and > > all hyperscalers for quite some time. > > Ah, okay. Time to remove the option then? Good point. Indeed. Will accept the patch if anyone has cycles to prep it, test it. > > > So much details not clear to me and not explained either :/ > > > > Yes. The plan is to "kill" bpf prog when it misbehaves. > > But this is orthogonal to this res_spin_lock set which is > > a building block. > > > > > Right, but it might have already modified things, how are you going to > > > recover from that? > > > > Tracking resources acquisition and release by the bpf prog > > is a normal verifier job. > > When bpf prog does bpf_rcu_read_lock() the verifier makes sure > > that all execution paths from there on have bpf_rcu_read_unlock() > > before program reaches the exit. > > Same thing with locks. > > Ah, okay, this wasn't stated anywhere. This is rather crucial > information. This is kinda verifier 101. I don't think it needs to be in the log. > > We definitely don't want to bpf core to keep track of acquired resources. > > That just doesn't scale. > > There could be rcu_read_locks, all kinds of refcounted objects, > > locks taken, and so on. > > The verifier makes sure that the program does the release no matter > > what the execution path. > > That's how it scales. > > On my devserver I have 152 bpf programs running. > > All of them keep acquiring and releasing resources (locks, sockets, > > memory) million times a second. > > The verifier checks that each prog is doing its job individually. > > Well, this patch set tracks the held lock stack -- which is required in > order to do the deadlock thing after all. Right, but the held lock set is per-cpu global and not exhaustive. It cannot detect 3-lock circles _by design_. We rely on timeout for extreme cases. > > The bpf infra does static checks only. > > The core doesn't track objects at run-time. > > The only exceptions are map elements. > > bpf prog might store an acquired object in a map. > > Only in that case bpf infra will free that object when it frees > > the whole map. > > But that doesn't apply to short lived things like RCU CS and > > locks. Those cannot last long. They must complete within single > > execution of the prog. > > Right. Held lock stack is like that. They're not equivalent and not used for correctness. See patch 26 and res_spin_lock_test_held_lock_max() selftest that was added specifically to overwhelm: +struct rqspinlock_held { + int cnt; + void *locks[RES_NR_HELD]; +}; It's an impossible case in reality, but the res_spin_lock code should be prepared for extreme cases like that. Just like existing qspinlock has 4 percpu qnodes and test-and-set fallback in case "if (unlikely(idx >= MAX_NODES))" line qspinlock.c:413. Can it happen in practice ? Probably never. But the code has to be ready to handle it. > > > > That was a conscious trade-off. Deadlocks are not normal. > > > > > > I really do think you should assume they are normal, unpriv and all > > > that. > > > > No unpriv and no, we don't want deadlocks to be considered normal > > by bpf users. They need to hear "fix your broken prog" message loud > > and clear. Patch 14 splat is a step in that direction. > > Currently it's only for in-kernel res_spin_lock() usage > > (like in bpf hashtab). Eventually we will deliver the message to users > > without polluting dmesg. Still debating the actual mechanism. > > OK; how is the user supposed to handle locking two hash buckets? Does > the BPF prog create some global lock to serialize the multi bucket case? Not following. Are you talking about patch 19 where we convert per-bucket raw_spinlock_t in bpf hashmap to rqspinlock_t ? Only one bucket lock is held at a time by map update code, but due to reentrance and crazy kprobes in the wrong places two bucket locks of a single map can be held on the same cpu. bpf_prog_A -> bpf_map_update -> res_spin_lock(bucket_A) -> kprobe or tracepoint -> bpf_prob_B -> bpf_map_update -> res_spin_lock(bucket_B) and that's why we currently have: if (__this_cpu_inc_return(*(htab->map_locked[hash])) ... return -EBUSY; .. workaround to prevent the most obvious AA deadlock, but it's not enough. People were able to hit ABBA. Note, raw_spin_lock today (and res_spin_lock after patch 19) is used by proper kernel code in kernel/bpf/hashtab.c. bpf prog just calls bpf_map_update() which is a normal helper call from the verifier point of view. It doesn't know whether there are locks inside or not. bpf_ktime_get_ns() helper is similar. The verifier knows that it's safe from NMI, but what kinds of locks inside it doesn't care. > Anyway, I wonder. Since the verifier tracks all this, it can determine > lock order for the prog. Can't it do what lockdep does and maintain lock > order graph of all loaded BPF programs? > > This is load-time overhead, rather than runtime. I wish it was possible. Locks are dynamic. They protect dynamically allocated objects, so the order cannot be statically verified. We pushed the limit of static analysis a lot. Maybe too much. For example, the verifier can statically validate the following code: struct node_data *n, *m, *o; struct bpf_rb_node *res, *res2; // here we allocate an object of type known to the verifier n = bpf_obj_new(typeof(*n)); if (!n) return 1; n->key = 41; n->data = 42; // here the verifier knows that glock spin_lock // protect rbtree groot bpf_spin_lock(&glock); // here it checks that the lock is held and type of // objects in rbtree matches the type of 'n' bpf_rbtree_add(&groot, &n->node, less); bpf_spin_unlock(&glock); and all kinds of other more complex stuff, but it is not enough to cover necessary algorithms. Here is an example from real code that shows why we cannot verify two held locks: struct bpf_vqueue { struct bpf_spin_lock lock; int credit; unsigned long long lasttime; unsigned int rate; }; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, ...); __type(key, int); __type(value, struct bpf_vqueue); } vqueue SEC(".maps"); q = bpf_map_lookup_elem(&vqueue, &key); if (!q) goto err; curtime = bpf_ktime_get_ns(); bpf_spin_lock(&q->lock); q->lasttime = curtime; q->credit -= ...; credit = q->credit; bpf_spin_unlock(&q->lock); the above is safe, but if there are two lookups: q1 = bpf_map_lookup_elem(&vqueue, &key1); q2 = bpf_map_lookup_elem(&vqueue, &key2); both will point to two different locks, and since the key is dynamic there is no way to know the order of q1->lock vs q2->lock. So we allow only one lock at a time with bare minimal operations while holding the lock, but it's not enough to do any meaningful work. The top feature request is to allow calls while holding locks (currently they're disallowed, like above bpf_ktime_get_ns() cannot be done while holding the lock) and allow grabbing more than one lock. That's what res_spin_lock() is achieving. Having said all that I think the discussion is diverging into all-thing-bpf instead of focusing on res_spin_lock. Just to make it clear... there is a patch 18: F: kernel/bpf/ F: kernel/trace/bpf_trace.c F: lib/buildid.c +F: arch/*/include/asm/rqspinlock.h +F: include/asm-generic/rqspinlock.h +F: kernel/locking/rqspinlock.c F: lib/test_bpf.c F: net/bpf/ that adds maintainer entries to BPF scope. We're not asking locking experts to maintain this new res_spin_lock. It's not a generic kernel infra. It will only be used by bpf infra and by bpf progs. We will maintain it and we will fix whatever bugs we introduce. We can place it in kernel/bpf/rqspinlock.c to make things more obvious, but kernel/locking/ feels a bit cleaner. We're not asking to review patches 14 and higher. They are presented for completeness. (patch 17 was out-of-order. It will be moved sooner. Sorry about that) But welcome feedback for patches 1-13. Like the way you spotted broken smp_cond_load_acquire() on arm64 due to WFE. That was a great catch. We really appreciate it.
new posting reminded me we had this thread... On Thu, Feb 13, 2025 at 06:37:05PM -0800, Alexei Starovoitov wrote: > > > When bpf prog does bpf_rcu_read_lock() the verifier makes sure > > > that all execution paths from there on have bpf_rcu_read_unlock() > > > before program reaches the exit. > > > Same thing with locks. > > > > Ah, okay, this wasn't stated anywhere. This is rather crucial > > information. > > This is kinda verifier 101. I don't think it needs to be in the log. Right, but I didn't take that class. I'm BPF n00b. Meanwhile you're asking me to review this :-/ > > OK; how is the user supposed to handle locking two hash buckets? Does > > the BPF prog create some global lock to serialize the multi bucket case? > > Not following. > Are you talking about patch 19 where we convert per-bucket > raw_spinlock_t in bpf hashmap to rqspinlock_t ? I'm not sure -- see the BPF n00b thing, I don't know how this is supposed to be used. Like really; I have absolutely 0 clues. Anyway; the situation I was thinking of was something along the lines of: you need data from 2 buckets, so you need to lock 2 buckets, but since hash-table, there is no sane order, so you need a 3rd lock to impose order. But also, see below, you've illustrated this exact case with q1,q2. > Only one bucket lock is held at a time by map update code, > but due to reentrance and crazy kprobes in the wrong places > two bucket locks of a single map can be held on the same cpu. > > bpf_prog_A -> bpf_map_update -> res_spin_lock(bucket_A) > -> kprobe or tracepoint > -> bpf_prob_B -> bpf_map_update -> res_spin_lock(bucket_B) > > and that's why we currently have: > if (__this_cpu_inc_return(*(htab->map_locked[hash])) ... > return -EBUSY; > > .. workaround to prevent the most obvious AA deadlock, > but it's not enough. > People were able to hit ABBA. Right, you can create arbitrary lock chain with this; chain length is limited by nesting-depth*nr-cpus or somesuch. > > Anyway, I wonder. Since the verifier tracks all this, it can determine > > lock order for the prog. Can't it do what lockdep does and maintain lock > > order graph of all loaded BPF programs? > > > > This is load-time overhead, rather than runtime. > > I wish it was possible. Locks are dynamic. They protect > dynamically allocated objects, so the order cannot be statically > verified. We pushed the limit of static analysis a lot. > Maybe too much. > For example, > the verifier can statically validate the following code: > struct node_data *n, *m, *o; > struct bpf_rb_node *res, *res2; > > // here we allocate an object of type known to the verifier > n = bpf_obj_new(typeof(*n)); > if (!n) > return 1; > n->key = 41; > n->data = 42; > > // here the verifier knows that glock spin_lock > // protect rbtree groot > bpf_spin_lock(&glock); > > // here it checks that the lock is held and type of > // objects in rbtree matches the type of 'n' > bpf_rbtree_add(&groot, &n->node, less); > bpf_spin_unlock(&glock); > > and all kinds of other more complex stuff, > but it is not enough to cover necessary algorithms. > > Here is an example from real code that shows > why we cannot verify two held locks: > > struct bpf_vqueue { > struct bpf_spin_lock lock; > int credit; > unsigned long long lasttime; > unsigned int rate; > }; > > struct { > __uint(type, BPF_MAP_TYPE_HASH); > __uint(max_entries, ...); > __type(key, int); > __type(value, struct bpf_vqueue); > } vqueue SEC(".maps"); > > q = bpf_map_lookup_elem(&vqueue, &key); > if (!q) > goto err; > curtime = bpf_ktime_get_ns(); > bpf_spin_lock(&q->lock); > q->lasttime = curtime; > q->credit -= ...; > credit = q->credit; > bpf_spin_unlock(&q->lock); > > the above is safe, but if there are two lookups: > > q1 = bpf_map_lookup_elem(&vqueue, &key1); > q2 = bpf_map_lookup_elem(&vqueue, &key2); > > both will point to two different locks, > and since the key is dynamic there is no way to know > the order of q1->lock vs q2->lock. I still feel like I'm missing things, but while they are two dynamic locks, they are both locks of vqueue object. What lockdep does is classify locks by initialization site (by default). Same can be done here, classify per dynamic object. So verifier can know the above is invalid. Both locks are same class, so treat as A-A order (trivial case is where q1 and q2 are in fact the same object since the keys hash the same). Now, going back to 3rd lock, if instead you write it like: bpf_spin_lock(&glock); q1 = bpf_map_lookup_elem(&vqueue, &key1); q2 = bpf_map_lookup_elem(&vqueue, &key2); ... bpf_spin_unlock(&glock); then (assuming q1 != q2) things are fine, since glock will serialize everybody taking two vqueue locks. And the above program snippet seems to imply maps are global state, so you can keep lock graph of maps, such that: bpf_map_lookup_elem(&map-A, &key-A); bpf_map_lookup_elem(&map-B, &key-B); vs bpf_map_lookup_elem(&map-B, &key-B); bpf_map_lookup_elem(&map-A, &key-A); trips AB-BA > So we allow only one lock at a time with > bare minimal operations while holding the lock, > but it's not enough to do any meaningful work. Yes, I can see that being a problem. > The top feature request is to allow calls > while holding locks (currently they're disallowed, > like above bpf_ktime_get_ns() cannot be done > while holding the lock) So bpf_ktime_get_ns() is a trivial example; it it always safe to call, you can simply whitelist it. > and allow grabbing more than one lock. > That's what res_spin_lock() is achieving. I am not at all sure how res_spin_lock is helping with the q1,q2 thing. That will trivially result in lock cycles. And you said any program that would trigger deadlock is invalid. Therefore the q1,q2 example from above is still invalid and res_spin_lock has not helped. > Having said all that I think the discussion is diverging into > all-thing-bpf instead of focusing on res_spin_lock. I disagree, all of this is needed to understand res_spin_lock. From the above, I'm not yet convinced you cannot extend the verifier with something lockdep like. > Just to make it clear... there is a patch 18: > > F: kernel/bpf/ > F: kernel/trace/bpf_trace.c > F: lib/buildid.c > +F: arch/*/include/asm/rqspinlock.h > +F: include/asm-generic/rqspinlock.h > +F: kernel/locking/rqspinlock.c > F: lib/test_bpf.c > F: net/bpf/ > > that adds maintainer entries to BPF scope. > > We're not asking locking experts to maintain this new res_spin_lock. > It's not a generic kernel infra. > It will only be used by bpf infra and by bpf progs. > We will maintain it and we will fix whatever bugs > we introduce. While that is appreciated, the whole kernel is subject to the worst case behaviour of this thing. As such, I feel I need to care.
On Tue, Mar 4, 2025 at 2:46 AM Peter Zijlstra <peterz@infradead.org> wrote: > > > Anyway; the situation I was thinking of was something along the lines > of: you need data from 2 buckets, so you need to lock 2 buckets, but > since hash-table, there is no sane order, so you need a 3rd lock to > impose order. Not quite. This is a typical request to allow locking two buckets and solution to that is run-time lock address check. > > q1 = bpf_map_lookup_elem(&vqueue, &key1); > > q2 = bpf_map_lookup_elem(&vqueue, &key2); > > > > both will point to two different locks, > > and since the key is dynamic there is no way to know > > the order of q1->lock vs q2->lock. > > I still feel like I'm missing things, but while they are two dynamic > locks, they are both locks of vqueue object. What lockdep does is > classify locks by initialization site (by default). Same can be done > here, classify per dynamic object. > > So verifier can know the above is invalid. Both locks are same class, so > treat as A-A order (trivial case is where q1 and q2 are in fact the same > object since the keys hash the same). Sounds like you're saying that the verifier should reject the case when two locks of the same class like q1->lock and q2->lock need to be taken ? But that is one of the use cases where people requested to allow multiple locks. The typical solution to this is to order locks by addresses at runtime. And nf_conntrack_double_lock() in net/netfilter/nf_conntrack_core.c does exactly that. if (lock1 < lock2) { spin_lock(lock1);spin_lock(lock2); } else { spin_lock(lock2);spin_lock(lock1); } > Now, going back to 3rd lock, if instead you write it like: > > bpf_spin_lock(&glock); > q1 = bpf_map_lookup_elem(&vqueue, &key1); > q2 = bpf_map_lookup_elem(&vqueue, &key2); > ... > bpf_spin_unlock(&glock); > > then (assuming q1 != q2) things are fine, since glock will serialize > everybody taking two vqueue locks. > > And the above program snippet seems to imply maps are global state, so Not quite. Some maps are global, but there are dynamic maps too. That's what map-in-map is for. > you can keep lock graph of maps, such that: > > bpf_map_lookup_elem(&map-A, &key-A); > bpf_map_lookup_elem(&map-B, &key-B); > > vs > > bpf_map_lookup_elem(&map-B, &key-B); > bpf_map_lookup_elem(&map-A, &key-A); > > trips AB-BA If everything was static and _keys_ known statically too, then yes, such analysis by the verifier would be possible. But both maps and keys are dynamic. Note, to make sure that the above example doesn't confuse people, bpf_map_lookup_elem() lookup itself is completely lockless. So nothing wrong with the above sequence as written. Only when: q1 = bpf_map_lookup_elem(&map-A, &key-A); q2 = bpf_map_lookup_elem(&map-B, &key-B); if (bpf_res_spin_lock(&q1->lock)) if (bpf_res_spin_lock(&q2->lock)) the deadlocks become a possibility. Both maps and keys are only known at run-time. So locking logic has to do run-time checks too. > I am not at all sure how res_spin_lock is helping with the q1,q2 thing. > That will trivially result in lock cycles. Right and AA or ABBA will be instantly detected at run-time. > And you said any program that would trigger deadlock is invalid. > Therefore the q1,q2 example from above is still invalid and > res_spin_lock has not helped. res_spin_lock will do its job and will prevent a deadlock. As we explained earlier such a program will be marked as broken and will be detached/stopped by the bpf infra. Also we're talking root privileges. None of this is allowed in unpriv. > > Just to make it clear... there is a patch 18: > > > > F: kernel/bpf/ > > F: kernel/trace/bpf_trace.c > > F: lib/buildid.c > > +F: arch/*/include/asm/rqspinlock.h > > +F: include/asm-generic/rqspinlock.h > > +F: kernel/locking/rqspinlock.c > > F: lib/test_bpf.c > > F: net/bpf/ > > > > that adds maintainer entries to BPF scope. > > > > We're not asking locking experts to maintain this new res_spin_lock. > > It's not a generic kernel infra. > > It will only be used by bpf infra and by bpf progs. > > We will maintain it and we will fix whatever bugs > > we introduce. > > While that is appreciated, the whole kernel is subject to the worst case > behaviour of this thing. As such, I feel I need to care. Not sure why you're trying to relitigate the years worth of discussions around locks in the bpf community. Static analysis of 2+ locks by the verifier is impossible. Full lock graph cycle detection lockdep-style is too slow in run-time. Hence res_spin_lock with AA, ABBA, and timeout as a last resort is our solution to real reported bugs. This res_spin_lock patchset fixes the following syzbot reports: https://lore.kernel.org/bpf/675302fd.050a0220.2477f.0004.GAE@google.com https://lore.kernel.org/bpf/000000000000b3e63e061eed3f6b@google.com https://lore.kernel.org/bpf/CAPPBnEa1_pZ6W24+WwtcNFvTUHTHO7KUmzEbOcMqxp+m2o15qQ@mail.gmail.com https://lore.kernel.org/bpf/CAPPBnEYm+9zduStsZaDnq93q1jPLqO-PiKX9jy0MuL8LCXmCrQ@mail.gmail.com https://lore.kernel.org/lkml/000000000000adb08b061413919e@google.com It fixes the real issues. Some of them have hacky workarounds, some are not fixed yet. More syzbot reports will be fixed in follow ups when we adopt res_spin_lock in other parts of bpf infra. Note, all of the above syzbot reports are _not_ using direct locks inside the bpf programs. All of them hit proper kernel spin_locks inside bpf infra (like inside map implementations and such). The verifier cannot do anything. syzbot generated programs are trivial. They do one bpf_map_update_elem() call or similar. It's a combination of attaching to tricky tracepoints like trace_contention_begin or deep inside bpf infra. We already have these workarounds: CFLAGS_REMOVE_percpu_freelist.o = $(CC_FLAGS_FTRACE) CFLAGS_REMOVE_bpf_lru_list.o = $(CC_FLAGS_FTRACE) CFLAGS_REMOVE_queue_stack_maps.o = $(CC_FLAGS_FTRACE) CFLAGS_REMOVE_lpm_trie.o = $(CC_FLAGS_FTRACE) CFLAGS_REMOVE_ringbuf.o = $(CC_FLAGS_FTRACE) to prevent recursion anywhere in these files, but it's helping only so much. So please take a look at patches 1-18 and help us make sure we implemented AA, ABBA, timeout logic without obvious bugs. I think we did, but extra review would be great. Thanks!