Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detecting access to out-of-scope variables

Code like this is undefined behavior, because it accesses a local variable that's no longer in scope (whose lifetime has ended).

int main() {
    int *a;
    {
        int b = 42;
        a = &b;
    }
    printf("%d", *a); // UB!
    return 0;
}

My question: Are there good techniques for automatically detecting bugs like this? It seems like it ought to be detectable (mark portions of stack space as unusable when variables go out of scope, then complain if that space is accessed), but Valgrind 3.10, Clang 4's AddressSanitizer and UndefinedBehaviorSanitizer, and GCC 6's AddressSanitizer and UndefinedBehaviorSanitizer all don't complain.

like image 772
Josh Kelley Avatar asked Jun 22 '17 18:06

Josh Kelley


People also ask

Can a variable be accessed outside of its scope?

No, a local variable cannot be accessed outside its scope. Some languages allow for closures, which are accessible outside the scope yet can modify variables local to the scope, but the raw variable is inaccessible.

What happens when a variable runs out of scope?

Nothing physical happens. A typical implementation will allocate enough space in the program stack to store all variables at the deepest level of block nesting in the current function. This space is typically allocated in the stack in one shot at the function startup and released back at the function exit.

What happens when a variable goes out of scope C++?

The cases when object goes out of scope, The program goes out of the scope of a function. The program ends. The block initializing local variables of object goes out of scope.


2 Answers

Without special compiler support, non-intrusive memory debuggers such as Valgrind can detect access to stack frames that have gone out of scope, but not to scopes within functions. This is because compilers (typically) allocate all the memory for a stack frame in a single pass*. Therefore to detect access to out of scope variables within the same function, we need specific compiler instrumentation to "poison" variables that have gone out of scope but whose enclosing frame is still valid.

The technique used by the ubsan AddressSanitizer, available in recent versions of clang and gcc, is to replace stack access with access to specially allocated memory:

In order to implemented quarantine for the stack memory we need to promote stack to heap. [...] __asan_stack_malloc(real_stack, frame_size) allocates a fake frame (frame_size bytes) from a thread-local heap-like structure (fake stack). Every fake frame comes unpoisoned and then the redzones are poisoned in the instrumented function code. __asan_stack_free(fake_stack, real_stack, frame_size) poisons the entire fake frame and deallocates it.

Example of usage and output:

$ g++ -std=c++11 a.cpp -fsanitize=address && env ASAN_OPTIONS='detect_stack_use_after_return=1' ./a.out 
ERROR: AddressSanitizer: stack-use-after-scope on address 0x7fd0e8300020 at pc 0x000000400c1b bp 0x7fff5b45ecf0 sp 0x7fff5b45ece8
READ of size 4 at 0x7fd0e8300020 thread T0
    #0 0x400c1a in main (a.out+0x400c1a)
    #1 0x7fd0ebe18d5c in __libc_start_main (/lib64/libc.so.6+0x1ed5c)
    #2 0x400a48  (a.out+0x400a48)

Address 0x7fd0e8300020 is located in stack of thread T0 at offset 32 in frame
    #0 0x400b26 in main (a.out+0x400b26)

  This frame has 1 object(s):
    [32, 36) 'b' <== Memory access at offset 32 is inside this variable

Note that because it is expensive it must be requested both at compile time (-fsanitize=address) and at run time (ASAN_OPTIONS='detect_stack_use_after_return=1'). Regarding minimum versions; it works with gcc 7.1.0 and clang trunk, but apparently not any released versions of clang, so if you want to use a released compiler you'll have to use gcc.


* Consider that these two functions compile (e.g. by gcc at -O0) to identical machine code, so there is no way** for a non-intrusive memory debugger to tell the difference between them:

int f() {
    int* a;
    {
        int b = 42;
        a = &b;
    }
    return *a;
}

int g() {
    int* a;
    int b = 42;
    a = &b;
    return *a;
}

** Strictly speaking, if debug symbols are available a debugger could track variables going in and out of scope. But generally if you have debug symbols available you have the source code, so can recompile the program with instrumentation.

like image 176
ecatmur Avatar answered Nov 01 '22 15:11

ecatmur


Yes. Lint is designed for this. We use it a lot in embedded systems and automotive systems. You can use the online demo to test out how well it would work for you. In your specific case, its rule MISRA:2012:18.6.

Sample Run


FlexeLint for C/C++ (Unix) Vers. 9.00L, Copyright Gimpel Software 1985-2014
--- Module: misra3.c (C)
        _
     1  int main() {
misra3.c  1  Note 970:  Use of modifier or type 'int' outside of a typedef [MISRA 2012 Directive 4.6, advisory]
misra3.c  1  Note 9075:  external symbol 'main(void)' defined without a prior declaration [MISRA 2012 Rule 8.4, required]
            _
     2      int *a;
misra3.c  2  Note 970:  Use of modifier or type 'int' outside of a typedef [MISRA 2012 Directive 4.6, advisory]
     3      {
                _
     4          int b = 42;
misra3.c  4  Note 970:  Use of modifier or type 'int' outside of a typedef [MISRA 2012 Directive 4.6, advisory]
                      _
     5          a = &b;
misra3.c  5  Info 733:  Assigning address of auto variable 'b' to outer scope symbol 'a' [MISRA 2012 Rule 18.6, required]
     6      }
            _
     7      printf("%d", *a); // UB!
misra3.c  7  Info 718:  Symbol 'printf' undeclared, assumed to return int [MISRA 2012 Rule 17.3, mandatory]
misra3.c  7  Warning 586:  function 'printf' is deprecated. [MISRA 2012 Rule 21.6, required]
misra3.c  7  Info 746:  call to function 'printf()' not made in the presence of a prototype
     8      return 0;
                    _
     9  }

misra3.c  9  Info 783:  Line does not end with new-line
misra3.c  9  Note 954:  Pointer variable 'a' (line 2) could be declared as pointing to const [MISRA 2012 Rule 8.13, advisory]

/// Start of Pass 2 ///

--- Module: misra3.c (C)
     1  int main() {
     2      int *a;
     3      {
     4          int b = 42;
     5          a = &b;
     6      }
     7      printf("%d", *a); // UB!
     8      return 0;
     9  }

--- Global Wrap-up

Warning 526:  Symbol 'printf()' (line 7, file misra3.c) not defined
Warning 628:  no argument information provided for function 'printf()' (line 7, file misra3.c)
like image 4
Cloud Avatar answered Nov 01 '22 14:11

Cloud