Search code examples
c++memory-leaksvalgrind

Why does Valgrind report "uninitialized value" but not "invalid read" in my C++ vector access?


I am working on a C++ program that involves accessing elements in a std::vector . I encountered an issue when running my code with Valgrind, where it reports an "uninitialized value" error, but not an "invalid read" error, which I expected. Here is a simplified version of my code:

#include <vector>
#include <iostream>
#define DATA_SIZE 10

struct Data {
  Data(double aa, double bb) : a(aa), b(bb) {}
  double a {0.0};
  double b {0.0};
};

class Test {
  public:
    Test() {
      data_.clear();
      data_.reserve(DATA_SIZE * 2);
      for (int i = 0; i < DATA_SIZE; ++i) {
          data_.push_back(Data(i, i));
      }
    }
    double Read() {
      const int index = DATA_SIZE;
      const auto& data = data_[index];
      double res = data.a + data.b;
      std::cout << res;
      return res;
    }
  private:
    std::vector<Data> data_;
};

int main() {
  Test t;
  t.Read();
}

Machine: Linux ubuntu 18.04 with g++ 7.5 and valgrind 1:3.13.0-2ubuntu2.3

Compile

g++ -std=c++17 -O2 -g test.cpp -o test

Run

valgrind --tool=memcheck --leak-check=full --expensive-definedness-checks=yes --track-origins=yes ./test

Result

Valgrind reported lots of Conditional jump or move depends on uninitialised value(s) on operator<<(std::cout << res;).

Questions:

I expected Valgrind to report an "invalid read" since the access is out of bounds, but it only reports an "uninitialized value" error. Why does Valgrind report "uninitialized value" instead of "invalid read"?

More information:

Interestingly, if I change
data_.reserve(DATA_SIZE * 2); to data_.reserve(DATA_SIZE); , Valgrind reports an 'invalid read'(as expected). I feel like the issue lies in the size of the vector reserves, but I'm not quite sure why/what that is.


Solution

  • Memcheck works at a much lower level than the high level source code. It doesn't know anything about std::vector. All that it sees are allocations (with operator new) and reads and writes (and also system calls). So when you write data_.reserve(DATA_SIZE * 2); memcheck will redirect the underlying call to operator new and record that 320 bytes were allocated with the default alignment. Internally it will allocate shadow memory marked as uninitialized.

    As you loop through

    for (int i = 0; i < DATA_SIZE; ++i) {
              data_.push_back(Data(i, i));
          }
    

    the bottom 160 bytes get marked as initialized. Then when you read

          const int index = DATA_SIZE;
          const auto& data = data_[index];
    

    that's still within the bounds of the 320 byte block, so it is accessible. But the shadow memory indicates that it is uninitialized. No error yet though, memcheck allows copying uninitialized memory.

    It's when you get to your cout that you get the error. During the conversion from double to some kind of string there will be several conditions (is it negative? is the exponent negative? is the exponent bigger than 1e6 and so on). Those condition checks on uninitialized values will trigger errors. (Those might not be the exact places where errors trigger, it's just an example).