codeql icon indicating copy to clipboard operation
codeql copied to clipboard

Python: Add example of missing use-use flow

Open RasmusWL opened this issue 1 year ago • 0 comments
trafficstars

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:

  1.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, 0)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  2.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, 0)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  3.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, 0)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  4.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, 0)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  5.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, 0)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  6. ... (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)

  1.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, some_var)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  2.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, some_var)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  3.  def func(x):
         try:
             with Thing() as y:
                 y.foo(x, some_var)
                 while not x.attribute:
                     y.bar()
                     print(x)
         finally:
             pass
     
  4. ...

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

  1.  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
     
  2.  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
     
  3.  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
     
  4.  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
     
  5. ...

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.

RasmusWL avatar Mar 07 '24 13:03 RasmusWL