Discussion: Hack the Planet!

Execute the code to understand the output and gain insights into the workings of the system stack during function calls.

Run the code

Now, it’s time to execute the code and observe the output.

Press + to interact
#include <iostream>
int getUserId() { return 1337; }
void restrictedTask1()
{
int id = getUserId();
if (id == 1337) { std::cout << "did task 1\n"; }
}
void restrictedTask2()
{
int id;
if (id == 1337) { std::cout << "did task 2\n"; }
}
int main()
{
restrictedTask1();
restrictedTask2();
}

Understanding the output

The id variable in the restrictedTask2()function has not been initialized and has an indeterminate value. To use its value is an undefined behavior. Anything can happen when a program runs into undefined behavior; the C++ standard makes no guarantees. Even the part of the program that happened before we took the value of id is undefined!

However, if we run this program, it will probably print both did task 1 and did task 2, at least if we compile without optimizations. So the value 1337 magically teleported from restrictedTask1() to restrictedTask2()! How could this happen?

Function call stack

Most systems use a stack for local variables. The restrictedTask1() has a local variable id, which it sets aside space for in its stack frame. As it happens, the restrictedTask2() has the same number and types of local variables (one int), so its stack frame layout will be identical to restrictedTask1().

When in main(), the stack will have grown to a certain point—see the leftmost illustration in the following figure. We then call restrictedTask1(), the stack grows, restrictedTask1() sets aside space in its stack frame for id, and initializes id to 1337. Then, control returns to main() and the stack shrinks again.

Press + to interact
Stack during both function calls
Stack during both function calls

Next, we call the restrictedTask2(), the stack grows again, and restrictedTask2() sets aside space in its stack frame for id but doesn’t initialize it. However, restrictedTask2()’s stack frame ends up in the exact same place as restrictedTask1()’s stack frame. The stack isn’t cleared between function calls, so the contents of restrictedTask2()’s stack frame will be exactly as restrictedTask1() left it, including the value 1337 in the position of the local variable id. This causes id to be 1337 in both functions.

If we turn on optimizations, though, restrictedTask1() and restrictedTask2() are likely to be inlined into main(), and the stack is not used for these calls. We can try this below by compiling it with -O2 (GCC/Clang).

Press + to interact
#include <iostream>
int getUserId() { return 1337; }
void restrictedTask1()
{
int id = getUserId();
if (id == 1337) { std::cout << "did task 1\n"; }
}
void restrictedTask2()
{
int id;
if (id == 1337) { std::cout << "did task 2\n"; }
}
int main()
{
restrictedTask1();
restrictedTask2();
}

On the x86_64 Linux machine, GCC prints did task 1 with -O2, whereas Clang segfaults. Try to run it locally and see what happens on your machine.

Avoiding uninitialized variables

To avoid nasal demons, security holes like this one, and garbage data in general, we should always initialize our variables before using them. It’s easy to slip up, but help is to be had! First of all, always compile with warnings. GCC, Clang, and MSVC will warn about this particular case, as will tools like Clang-Tidy. But that’s only because it’s straightforward to prove that id is uninitialized when it’s used. Often, we can’t know this until runtime. This is where sanitizers come in.

Different sanitizers exist for different purposes, but all monitor our code at runtime in various ways to detect problems that can’t be detected at compile-time. One such sanitizer is MemorySanitizer, which is designed to catch usages of uninitialized memory. If we run this program with MemorySanitizer (pass-fsanitize=memory as a compiler option to Clang), it’ll print something like this:

==1416660==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x5603d43af5eb in restrictedTask2() main.cpp:14:9
#1 0x5603d43af64d in main main.cpp:19:5
Warning by MemorySanitizer

It tells us that we’re using uninitialized memory and gives us a stack trace to where it happened.

Sanitizers can make our program much slower, so we typically want separate build configurations with sanitizers. A sanitizer can only detect an issue if that issue actually occurs at runtime, though, so make sure to run as much of your test suite as possible with this build.

Recommendations

Here are some recommendations to improve code quality in C++ projects:

  • Always enable warnings: Using -Wall -Wextra -Wpedantic is a good start on GCC/Clang, and /W4 on MSVC.

  • Use linting tools: If the IDE supports integrations with linting tools like clang-tidy, turn them on. They can sometimes report issues like this one before we even compile the code.

  • Treat warnings as errors: Enable warnings as errors, at least in continuous integration (CI) jobs, so you don’t overlook any warnings.

  • Set up sanitizer builds: You should set up sanitizer builds for as many sanitizers as you can and run the tests with these builds.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.