codeql
codeql copied to clipboard
Python: Add example of missing use-use flow
Here is an interesting example where we lose use-use flow. I could not minimize the example any further, that is: removing the try-finally blocks made things work again, and converting while -> if made things work as well.
def func(x):
try:
with Thing as y:
y.foo(x, some_var.some_attr)
while not x.attribute: # <-- missing flow
y.bar() # <-- missing flow
print(x)
finally:
pass
I noticed that by change some_var.some_attr to 0 the problem would also go away, and by changing to some_var we would only have missing flow for y but have proper flow for x...
I did spend some time looking into a fix in the SsaCompute.qll file, but did not reach a conclusion. While it shares some aspects with https://github.com/github/codeql/pull/10970 and https://github.com/github/codeql/pull/10998, it's also not entirely the same.
Basic Blocks
One of my observations was that the basic blocks change when changing the argument, for the simple case where everything works:
-
def func(x): try: with Thing() as y: y.foo(x, 0) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, 0) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, 0) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, 0) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, 0) while not x.attribute: y.bar() print(x) finally: pass - ... (and so on) ...
...
Variant some_var
The interesting bit is that if we do
- y.foo(x, 0)
+ y.foo(x, some_var)
we get a new basic-block (for the whole line for the y.foo call)
-
def func(x): try: with Thing() as y: y.foo(x, some_var) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, some_var) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, some_var) while not x.attribute: y.bar() print(x) finally: pass - ...
AND we lose the use-use edge from y.foo -> y.bar
Variant some_var.some_attr
and if we do
- y.foo(x, some_var)
+ y.foo(x, some_var.some_attr)
we get another basic-block for the attribute lookup
-
def func(x): try: with Thing() as y: y.foo(x, some_var.some_attr) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, some_var.some_attr) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, some_var.some_attr) while not x.attribute: y.bar() print(x) finally: pass -
def func(x): try: with Thing() as y: y.foo(x, some_var.some_attr) while not x.attribute: y.bar() print(x) finally: pass - ...
we also lose the use-use edge from x (in y.foo call) to x.attribute
Idea
From doing quick-eval of the predicates in SsaCompute, I can see that we make the wrong conclusion around liveness.
I don't understand why we end up with a new basic-block for variant some_var, so if we ever want to get to the bottom of this (and we might simply start using shared SSA instead), figuring out why we get this new basic-bock would be a good next step.