[geeks] Buffer Cache, mmap, and Undefined behavior

Jonathan Patschke jp at celestrion.net
Fri Apr 28 07:06:14 CDT 2017


This week I fixed a bug of my own creation, but if you'd explained it to
me beforehand, I'd never have believed it.

At the dayjob, we run HP-UX 10.20 in support of some lab equipment whose
software only runs on old HP-UX.  10.20 has some "interesting" bugs[0],
misfeatures, and artifacts surrounding its virtual memory system, but this
one was new to me.

The sorts of tests we do these days involve several large (~100MB) files
that specify the particulars of the test, of which I need to parse out
about 200K of control/drive/compare data.  Performance being a
cost-per-second concern when we go to production, my usual route is to
generate stuff as early as possible and in a way that can be mmap()ed into
the test directly (or, at most, with a fixup pass to change file offsets
to pointers).

Early this week, I got a bug report telling me that one of the newer tests
would fail every part if that test hadn't been run recently, but forcing a
re-generation of my summary file would get it to work correctly "for a
while."  My first instinct was that I had a bug that was corrupting the
summary files.  Frustratingly, they were identical (apart from the
timestamp).

So I thought, maybe there was a race-condition loading the index, or I had
some stack variable that was only incidentally initialized, or a bug in my
std::shared_ptr wrapper around mmap.  None of that bore fruit, either.

Then I wrote a tool to burst open the summaries and dump out their
contents.  The (identical) files gave different results--the same
different results we had in the test environment!

It'd be easy from there, right?  I'd made the same mistake twice, so it
should be obvious!  I made the dump tool additionally give the address of
each thing in the file; the last element was past-the-end by one word.
But, if it was past the end of the file, why did the read sometimes give
the correct (and far from random) value?  And, if it was outside of the
mmap()ed area, why did it not page fault?

Simple: my file size was not a multiple of the page size, and mmap() maps
whole pages, so there was page space left over after the backing file.  My
generator, test program, and dumper all used mmap(), and all ran on the
same machine.  Because that page stayed "hot" for weeks or months in the
automated-test environment, it was never paged out, and because of the
semantics[1] of mmap(), multiple processes could see that same data.

Once the page goot evicted from a combination of LRU and memory pressure,
that last value degraded to a 0 (from the blank replacement page),
instructing my test code to overwrite a different part of the comparison
memory, causing the device test to fail.

I fixed my bug, but I almost want to see if the disk block has that last
past-the-file integer.


[0] Example: a regular user running Berkeley DB 2.x can cause handles to leak
     in such a way as to require a reboot before even root can do any
     useful work.  That was a fun assembly-line halt to explain.
[1] Largely implementation-specific, but on HP-UX, equivalent private maps
     of the same backing file will use the last shared writethrough copy
     still in memory, regardless of what's "supposed" to be on disk.
-- 
Jonathan Patschke
Austin, TX
USA


More information about the geeks mailing list