August 26th, 2026
mind blown1 reaction

In the product end game, every change carries significant risk, episode 2

A colleague related this story of a bug that arose at the very end of the Word 97 release cycle.

Testing had identified a ship-stopping bug in a somewhat common code path. The bad news is that the bug was sporadic. The good news is that the test team had come up with a script that could trigger the crash in the lab with fairly good reliability. The bad news is that the bug went away when you used the debugger.

The development team was very anxious, because a bug that appears only sporadically in the lab is going to occur regularly in the wild. But how do you debug a problem that resists debugging?

There was talk of using an in-circuit emulator (ICE), which is basically a separate computer that ran a CPU emulator. The ICE had a cable that plugged into the target machine’s CPU socket, and that was the mechanism by which the ICE could emulate the operation of a CPU: by physically reproducing the electrical signals that a real CPU would generate. Since the CPU was emulated, you could use the ICE to set breakpoints on things that happen inside the CPU itself, like “Break when the value 42 is written to this memory location when the CPU has interrupts disabled.” (Though in this case, it was just for setting breakpoints and inspecting memory from outside the system.) To most software developers, in-circuit emulators existed only in myth, and the possibility of acquiring one and using it was met with the excitement of a five-year-old boy who learns that he might get to ride on a fire truck.

The developers who were leading the investigation beamed with joy when they identified that most of the crashing systems came from the same manufacturer, and they were all manufactured before a specific date. At this point, it wasn’t too long before a CPU errata was found that was consistent with the manufacturing dates of the affected systems.

Now, the compiler they were using had issued an update to avoid the offending code sequence, but the team had locked their toolset before that update became available, and upgrading the compiler while in escrow¹ is not a good idea, because who knows what new bugs would be introduced by switching to a new compiler.²

The team wrote a tool to scour their binaries to look for any occurrences of a code sequence that would trigger the CPU erratum.² They found around 150 instances of the troublesome code sequence, but one of the requirements for the CPU bug was that the sequence span a page boundary, and only one of those 150 incidents crossed a page boundary.

And their testers found it.

To avoid introducing any new problems as a side effect of the fix, the team opted to do a binary patch to the binary to insert a nop into the offending code sequence. This was enough to avoid the CPU erratum without risking regression to any other code.

Bonus reading: Related story.

¹ Supplemental reading: Escrow vs. release candidate.

² There is a small risk that the compiler will have a bug that is triggered by the product source code, but there is a much bigger risk that that there are pre-existing bugs in the product source code that would be exposed by the new compiler. For example, an uninitialized variable bug could be masked by the fact that the old compiler’s choice of memory layout means that the previous use of the memory was for a pointer that was never null, but the new compiler lays out local variables differently, and now the previous use of the memory was for an integer that is sometimes zero.

Sound familiar?

Topics

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

7 comments

Sort by :
  • Bwmat

    Ah, that makes sense, was thinking you’d want to not shift the code after the inserted nop(s) though?

  • Bwmat 2 days ago

    I’m curious what the instruction sequence was that you could just nop it out without breaking the function it was in?

    • Me Gusta · Edited

      The post doesn't indicate that an instruction sequence was removed. The post indicates that a nop instruction was inserted. As a small example with nothing to do with the post itself. Suppose you have the instruction sequence:

      <code>

      You could insert a nop in several locations:

      <code>

      As one example. This doesn't do anything major, but it can change timings or position of the code in memory, and this can be enough to bypass a problem in the CPU or microcode.

      But in the case in the post. The description indicates that the instruction sequence spanned a page boundary. So placing extra bytes in the...

      Read more
      • Travis Hayes

        If the solution was to patch the binary by inserting a NOP, it would be a recipe for pain, too. Adjusting the address of everything after that point in the binary? Every RET, every jump table or lookup… ouch.

        Seems like if they could narrow down the problem source, you could insert a #pragma assembler block with the NOP into the source and recompile; check the resulting binary for page boundary changes, readjust the source again, rinse and repeat; until the page boundary condition went away.

  • Joshua Hudson 3 days ago

    I must agree with Raymond. Upgrading the compiler late in the release cycle is dangerous. I have managed to depend on unspecified behavior in C# before.

  • Jernej Simončič 3 days ago

    Are you sure you hadn’t written about this problem before? I know I’ve read about this exact problem (and solution) somewhere years ago.