Re: "warning: context imbalance" in kernel/bpf/hashtab.c

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Sat, Nov 07, 2020 at 02:20:03PM -0800, Linus Torvalds wrote:
> On Sat, Nov 7, 2020 at 1:23 PM Luc Van Oostenryck
> <luc.vanoostenryck@xxxxxxxxx> wrote:
> >
> > Yes, but the branch simplifications depend on the simplifications
> > that can be made on the condition. For the moment there is a focus
> > on select because it often happens when the condition is simple but
> > for these context imbalance the general case is something like:
> >         stuff
> >         if (arbitrary condition)
> >                 take lock
> >         do other stuff
> >         if (same arbitrary condition)
> >                 release lock
> >
> > and the problem is (at least) twofold (but partially related):
> > 1) recognize that the second condition is the same as the first one
> > 2) avoid that partial optimizations of the first condition 'leak'
> >    into the common part because this often inhibits doing 1)
> >    [Sorry, if this is not very clear. I need to find some small
> >     examples to illustrate this].
> >
> > When the condition is very simple, it is converted into a select
> > but very often it's more complex, involves a memory access and/or
> > some function calls or some asm.
> 
> Note that if the condition is complex and involves memory etc, it's
> generally actually a bug in the kernel.
> 
> Because if it could possibly change between the lock taking and
> releasing, you are now broken.
> 
> So I wouldn't want to accept code like that _anyway_. Any complex
> conditional needs to be written as
> 
>      lock = complex_conditional;
>      if (lock)
>           take lock
>      do other stuff
>      if (lock)
>           release lock
> 
> and if sparse handles _that_ case well, then all is ok.
> 
> If sparse complains about the fragile "repeat complex conditional that
> couldn't be simplified to show that it was obviously identical", then
> that's actually a _good_ thing.
> 
> Because we want to complain about stuff where it's not obviously clear
> that the conditional is 100% the same.
> 
> Now, even in the above, if we'd want sparse to say that is ok, then
> we'd actually need to make the context tracking itself able to deal
> with "conditional on this pseudo", which it doesn't do right now.
> 
> So right now it requires that much simpler case where you actually end
> up not ever having that shared case in the middle at all, and the
> simplification really ends up showing that the real path is really
> 
>      error = complex_conditional;
>      if (error)
>           return error;
>      take lock
>      do other stuff
>      release lock
> 
> which apparently sparse now - with your simplification - can see that
> the code actually ends up doing .
> 
> Which is even better.
> 
> I'd much rather have the lock context tracking show that locking is
> simple, and that we don't have any code that sometimes is called
> locked, and sometimes is not.
> 
> Because even if you can prove - with some kind of conditional context
> tracking - that the context at least _nests_ correctly (ie you always
> properly unlock something you locked), that isn't actually the big
> problem. The big problem really is "why did that 'do-other-stuff'
> sometimes run without locking, and sometimes with locking?"

But isn't a lot of code written to explicitly support this, with an
argument or some other condition selecting between the locked mode
or the unlocked mode?


> So the lock context tracking is fairly inflexible on purpose. It's
> _meant_ to not just do "unlock matches a lock" matching. It's meant to
> also track "ok, there is no odd 'sometimes locked, sometimes not'
> code".

I agree with all this but I think the situation is not so clear cut.
I've made a few examples, oversimplified, totally made-up but
representative of the situations with the context tracking.

	void take_lock(void) __attribute__((context(x, 0, 1)));
	void drop_lock(void) __attribute__((context(x, 1, 0)));
	void minor(void);
	void major(void);
	int complex_cond(void);
	
This one is fine for Sparse
	static inline int cond_lock1(void)
	{
		if (complex_cond()) {
			take_lock();
			minor();
			return 1;
		}
		return 0;
	}
	static void ok1(void)
	{
		int cond = cond_lock1();
		if (cond) {
			major();
			drop_lock();
		}
	}
It produces the following:
	ok1:
		call.32     %r1 <- complex_cond
		cbr         %r1, .L1, .L5
	.L1:
		call        take_lock
		context     1
		call        minor
		# call      %r3 <- cond_lock1
		call        major
		call        drop_lock
		context     -1
		br          .L5
	.L5:
		ret
Good.

The next one corresponds to the situation Song Liu reported
	static inline int cond_lock2(void)
	{
		if (!complex_cond())
			return -1;
	
		take_lock();
		minor();
		return 0;
	}
	static int okish(void)
	{
		int cond = cond_lock2();
		if (cond)
			return 0;
		major();
		drop_lock();
		return 1;
	}

Sparse currently produces the following:
	okish:
		call.32     %r6 <- complex_cond
		select.32   %r8 <- %r6, $0, $0xffffffff
		cbr         %r6, .L8, .L9
	.L8:
		call        take_lock
		context     1
		call        minor
		br          .L9
	.L9:
		# call      %r8 <- cond_lock2
		seteq.32    %r11 <- %r8, $0
		cbr         %r6, .L11, .L12
	.L11:
		call        major
		call        drop_lock
		context     -1
		br          .L12
	.L12:
		ret.32      %r11

Sparse complains about a context imbalance because of L8/L9.
The seteq is not merged with the select but something can be done
about it or on the conditional branch that depends on it.
I'll look at it right away.

[But from what I remember when I analyzed some of these, it's in
 similar situations that things are messy, when cond_lock() is
 slightly more complex and partial simplifications begins to mix
 things from the surrounding blocks].


The one that really annoys me is this one. It's very simple and, as
far as I know, quite common in the kernel but kinda hopeless.
	static void ko(int cond)
	{
		if (cond)
			take_lock();
		major();
		if (cond)
			drop_lock();
	}

It produces the following:
	ko:
		cbr         %arg1, .L14, .L15
	.L14:
		call        take_lock
		context     1
		br          .L15
	.L15:
		call        major
		cbr         %arg1, .L16, .L17
	.L16:
		call        drop_lock
		context     -1
		br          .L17
	.L17:
		ret

There is no select, no seteq/setne, no complex condition, nothing.
It's an idealization but I think that a lot of code involving
conditional locks boils down to this and as I said is quite common.

What do we want to do about this? I don't think there is nothing that
can be done except warning or duplicating .L15, making it as if the
code would have been written as:
	static void ko(int cond)
	{
		if (cond) {
			take_lock();
			major();
			drop_lock();
		} else {
			major();
		}
	}
It's doable of course but ... well, I don't think we want to do this.

-- Luc



[Index of Archives]     [Newbies FAQ]     [LKML]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Trinity Fuzzer Tool]

  Powered by Linux