Writing

Software, technology, sysadmin war stories, and more. Feed
Wednesday, May 19, 2021

gmock and gtest on OpenBSD 6.9 hate each other

Here's a fun one. I've been messing around with an OpenBSD 6.9 image to see just how well my existing code works on a non-Linux environment. Once in a while, I find something unusual and interesting, like a difference in what is supported, and discover a "Linuxism" (or really a "glibcism" in most cases) as a result.

Tonight, however, was not one of those. It was something else brought to us by the folks at Google who put together gtest and gmock, two C++ libraries for testing and mocking libraries, respectively.

The OpenBSD system has this available via their ports system, so you can just "pkg_add gtest" and you'll get gtest+gmock 1.8.0p3 on your machine, ready to go. Trouble is, it's not quite right in the head if you do dynamic linking and put both into your binary. Sometimes, it'll crash with a delightful message about a double-free. If you then only link against gtest (and not also gmock), it won't crash.

Seriously, check this out:

openbsd:~/prog/wtf$ cat dummy.cc
#include <gtest/gtest.h>
 
int main(int argc, char** argv) {
  testing::InitGoogleTest(&argc, argv);
 
  return 0;
}

So, this is a stupid little program that initializes the test framework but doesn't actually run any tests. Now, here's the fun part: just by linking gmock to it, it will sometimes decide to die when you run it.

I wrote something stupid to demonstrate this by compiling and linking it twice: once with both libraries, and once with just gtest:

openbsd:~/prog/wtf$ cat build
#!/bin/sh
 
/usr/bin/clang++ -I/usr/local/include dummy.cc -rdynamic \
    -L/usr/local/lib         -lgtest -lpthread  -o dummy_alone
 
/usr/bin/clang++ -I/usr/local/include dummy.cc -rdynamic \
    -L/usr/local/lib -lgmock -lgtest -lpthread  -o dummy_with_gmock

So, if I run this script, I get "dummy_alone" and "dummy_with_gmock". I can run that first one all day long and nothing will crash.

However, when it's time to run the second one...

openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
openbsd:~/prog/wtf$ ./dummy_with_gmock
dummy_with_gmock(68750) in free(): double free 0x8ccbe46f080
Abort trap (core dumped)

Aha! You see! It blew up. Since this is OpenBSD, I can switch on some fun stuff with MALLOC_OPTIONS and get it to spit out a few more interesting details:

openbsd:~/prog/wtf$ export MALLOC_OPTIONS=C
openbsd:~/prog/wtf$ ./dummy_with_gmock
dummy_with_gmock(55602) in free(): chunk canary corrupted 0xea31e17aee0 
0x18@0x18 (double free?)
Abort trap (core dumped)

If you dig around through the googletest archive of issues and commit history, you will eventually discover the fix which is called "Fix ODR violation of gtest global variables for non-Windows platforms".

For those fortunate enough to not have tripped over this before, ODR means "One Definition Rule", and it basically means you can't have duplicate definitions of things in your program.

This makes sense, right? You can't expect stuff to work if it doesn't know which one you're talking about. But, how is this even possible, you ask? Won't you get an error if you define the same thing twice?

Well, you will, and you won't. Let's say you try to test this with something simple like this:

int main() {
  int a;
  int a;
  return 0;
}

If you try to compile that, it will bomb, as you would hope.

double.cc:3:7: error: redefinition of 'a'
  int a;
      ^
double.cc:2:7: note: previous definition is here
  int a;
      ^
1 error generated.

But, that's in the same source file. What if you create it in disjoint source files, make different objects, then turn them into different libraries, and then yank all of them into the same program at runtime? Guess what, you'll totally have a conflict... and nothing will stop you. That's what happened here.

It's interesting to look at the fix here, particularly the parts which were removed for non-Windows builds. It used to build gmock from gmock-all.cc and gtest-all.cc. You can't see it here, but it also built gtest from gtest-all.cc.

So, you have [gtest = gtest-all.cc] and [gmock = gmock-all.cc + gtest-all.cc], and then you jam them together, and ... BOOM.

This is something I see a lot in this world and I tend to think of it as a "big ball of mud" build. This is where people take multiple source files (foo.cc, bar.cc, ...) and turn them into a single object file. This tends to allow all kinds of nasty cross-file dependencies, and then yes, ODR violations if you wind up adding the same source file back in from a different route.

My own personal rule on this is pretty simple: foo.o comes from foo.cc and possibly foo.h. bar.o comes from bar.cc and maybe bar.h. At no point do I compile more than one .cc source file into the same object file.

The fix, unsurprisingly, tells gmock to depend on gtest, which just makes sense. It's way better than just shoveling everything together and hoping it works out, because that just doesn't happen.

If you're on OpenBSD and this is biting you, I guess the fix would be to patch the port to push it up to at least 1.8.1 which has this fixed upstream. Or, you can just build it yourself and use that version instead. Either way, this will solve the problem of test code that does nothing and yet blows up in a really nasty and unpredictable fashion.