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

C++: Return statement inside Guard Block #15001

Open
tardigrade-9 opened this issue Dec 5, 2023 · 5 comments
Open

C++: Return statement inside Guard Block #15001

tardigrade-9 opened this issue Dec 5, 2023 · 5 comments

Comments

@tardigrade-9
Copy link

tardigrade-9 commented Dec 5, 2023

Is the dataFlow guarded if the condition has return statement?
Here's the code

#include <iostream>
#include <cstring>

bool test_func(const char* str1, const char* str2) {
    return strcmp(str1, str2) == 0;
}

struct test_struct {
    int a;
    int b;
};

int func(int num){
    struct test_struct *test = NULL;
    if(num > 0) {
        test = (test_struct *)calloc(num, sizeof(*test));
        if(!test) {
            return 1;
        }
    }

    test[0].a = 1;
    test[0].b = 2;
    return 0;
}
int main() {
    func(2);
    func(0);
    return 0;
}

Here's the query

/**
 * @kind path-problem
 */

 import cpp
 import semmle.code.cpp.dataflow.new.DataFlow
 import semmle.code.cpp.controlflow.IRGuards
 import Flow::PathGraph
 
 /**
  * Holds if `g` is a guard that ensures that `e` is not null when `g` evaluates to `branch`
  */
 predicate isNotNullCheck(IRGuardCondition g, Expr e, boolean branch) {
   g.comparesEq(any(Instruction instr | instr.getUnconvertedResultExpression() = e).getAUse(),
     any(ConstantValueInstruction const | const.getValue() = "0").getAUse(), 0, false, branch)
 }
 

 module UAFConfig implements DataFlow::ConfigSig {
   predicate isSource(DataFlow::Node source) {
    exists(Expr e | e = source.asExpr() | e.(NullValue).getValue().toInt() = 0)
 
  }
 
   predicate isSink(DataFlow::Node sink) {
     dereferenced(sink.asExpr())
   }
 
   predicate isBarrier(DataFlow::Node node) {
     node = DataFlow::BarrierGuard<isNotNullCheck/3>::getABarrierNode()
   }
 }
 
 module Flow = DataFlow::Global<UAFConfig>;
 
 from Flow::PathNode source, Flow::PathNode sink
 where Flow::flowPath(source, sink)
 select sink, source, sink, "Null ptr deref: $@ and $@.", source, "source", sink, "deref"

Output:
1.test=NULL, -> test[0].a , this is TP
2. calloc() , -> test[0].a , this is FP because when calloc return null, test[0] is unreachable..
two more results for test[1], which is same as above..

Is this because Guard Condition can't see the return statement inside the conditional block?

@smowton smowton changed the title C++:Return statement inside Guard Block C++: Return statement inside Guard Block Dec 5, 2023
@MathiasVP
Copy link
Contributor

MathiasVP commented Dec 5, 2023

Hi @tardigrade-9,

The problem isn't actually the return. Rather, the problem is that the barrier guards library doesn't support this pattern:

if(condition()) {
  x = source();
  if(!safe(x)) {
    return;
  }
}
sink(x);

where we want to put a barrier on code guarded by safe(x). I hope you can see that this is a generalization of your setup where !safe(x) is the !test check, source() is your call to calloc, sink is the dereference of test, and condition() is num > 0.

The BarrireGuard module creates a barrier on everything that is guarded by safe(x). However, sink(x) isn't actually guarded by safe(x) since safe(x) only guards the case where condition() holds.

We do have some ideas for how we could make this work. But for now there's no good way to handle this FP. I'll add your example to our internal issue that tracks this.

@tardigrade-9
Copy link
Author

Thanks @MathiasVP for clarification.

@tardigrade-9
Copy link
Author

tardigrade-9 commented Dec 9, 2023

@MathiasVP I modified the functions to have condition on test

int func2(int num,test_struct *test){
    if(!test) {
        return 1;
    }
    if(num > 0) {
        test = (test_struct *)calloc(num, sizeof(*test));
        if(!test) {
            return 1;
        }
    }
    test[0].a = 1;
    test[0].b = 2;
    return 0;
}

I get path something like
func2(2,NULL=source);
test[0] as sink
Screenshot 2023-12-08 at 7 46 17 PM

@MathiasVP
Copy link
Contributor

Hi @tardigrade-9,

I'm not sure what you mean. Your modified snippet still fits the general pattern that I said we couldn't handle (where condition() is num > 0, source() is (test_struct *)calloc(num, sizeof(*test)), !safe(x) is !test, and sink(x) is test[0]).

Could you clarify what you meant in your last post?

@tardigrade-9
Copy link
Author

Sorry for misunderstanding. The pattern you mentioned is

if(condition()) {
  x = source();
  if(!safe(x)) {
    return;
  }
}
sink(x);

But if you see source is not within the condition block. Source is functioncall func2(2,NULL(=source)). And I've added condition on x which follows below pattern

if(!safe(x)) {
 return ;
}

if(condition()) {
  x = new_source();
  if(!safe(x)) {
    return;
  }
}
sink(x);

And the path I'm getting is source as func2 parameter, to sink x. But previously the source was new_source(). I hope this is clear. Thanks!

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

No branches or pull requests

2 participants