November 11th, 2025
like1 reaction

Behind the scenes on how Windows 95 application compatibility patched broken programs

Whenever possible, Windows 95 made application compatibility tweaks through things like compatibility flags that alter the behavior of the system for any program the flag was applied to. Using compatibility flags allows the fix to be generalized: If one program has a problem, there’s a good chance that another program will also have that same problem. So you can apply the flag to the second program and take advantage of the same fix.

On very rare occasions, the problem is too deeply embedded in the program, and the only reasonable option is to patch it. Out of safety, the Windows 95 team got written permission from the vendor whenever they needed to patch a program. The consultation included detailed information on what the problem was and how it was going to be patched. In exchange, the team requested information from the vendor on what versions of their product are affected (and if they could send those versions for analysis), as well as a promise to fix the problem in their next version, because the next version won’t have the benefit of the patch.

The patches themselves were kept in the registry under HKLM\System\CurrentControlSet\Control\SessionManager\AppPatches\〈ModuleName〉\〈Detection string〉. When a 16-bit module is loaded and its target Windows version is less than 4.0, the kernel looks through all the detection strings and tries them one by one to see if any of them triggers.

The detection string is decoded into bytes. The first byte represents the match algorithm, and the rest are parameters to the algorithm.

Type 01: Matching the NE header using 8-bit offsets. The value of is the 8-bit offset into the header, and the xx values are the bytes to match.

01 nn of xx xx … xx 00
    nn bytes Terminator
  Repeat as needed

Type 02: Matching the NE header using 16-bit offsets. The value offs is the 16-bit offset (little-endian) into the header, and the xx values are the bytes to match.

02 nn offs xx xx … xx 00
    nn bytes Terminator
  Repeat as needed

Type 03: Matching the file contents using 16-bit offsets. The value offs is the 16-bit offset (little-endian) into the file, and the xx values are the bytes to match.

03 nn offs xx xx … xx 00
    nn bytes Terminator
  Repeat as needed

Type 04: Matching the file contents using 24-bit offsets. The value offset is the 24-bit offset (little-endian) into the file, and the xx values are the bytes to match.

04 nn offset xx xx … xx 00
    nn bytes Terminator
  Repeat as needed

Type 05: Matching the file contents using 32-bit offsets. The value offset32 is the 32-bit offset (little-endian) into the file, and the xx values are the bytes to match.

05 nn offset32 xx xx … xx 00
    nn bytes Terminator
  Repeat as needed

Type 06: Matching the 16-bit file size. The value size is the 16-bit file size (little-endian).

06 size

Type 07: Matching the 24-bit file size. The value size24 is the 24-bit file size (little-endian).

07 size24

Type 08: Matching the 32-bit file size. The value filesize is the 32-bit file size (little-endian).

08 filesize

Finally, there is the “combo” detector. This allows you to combine multiple detectors (which must all be satisfied).

Type FF: Combo detector. Each xx block is one of the other detector types (starting with the type byte and ending with the null terminator).

FF nn xx xx … xx 00
    nn bytes Terminator
  Repeat as needed

For example, one of the detectors that comes with Windows 95 is ff0601023e0a03000306f05c00. This breaks down as

FF Combo detector
06 First detector is 6 bytes long
01 Type 1: Match NE header with 1-byte offsets
02 Match two bytes
3e At offset 0x3E in the NE header (expected Windows version)
0a 03 Bytes 0a, 03, indicating Windows version 3.1
00 Terminator
03 Second detector is 3 bytes long
06 Type 6: Match 16-bit file size
f0 5c File size 0x5cf0
00 No more detectors

In practice, you tend to see a lot of file size matches, because any change to a program is highly like to alter the file size. Conversely, you are unlikely to see many file contents matches because those incur additional I/O and are therefore more expensive.

If a match is found, the subkeys indicate the segments to patch, and the values of those subkeys are binary data providing the patch to apply. The names of the values are not significant, but traditionally “Add” patches are named Add and “Change” patches are named Change. If there is more than one Add or Replace patch, tradition dictates that they are given numeric suffixes to distinguish them.

Type 01: Change bytes. The sz is the total size of the patch value. The offs is a 16-bit (little-endian) offset into the segment. The xx values are the bytes expected to be there, and the yy values are the bytes that they will be changed to.

01 sz offs nn xx xx … xx yy yy … yy
    nn bytes nn bytes

Type 02: Append bytes. The sz is the total size of the patch value. The offs is a 16-bit offset (little-endian) to where the bytes should be added. (The offset must be greater than or equal to the actual segment size, and the segment will be grown to accommodate the extra bytes.) The xx values are the bytes to add.

02 sz offs nn xx xx … xx
    nn bytes

For example, the detector above comes with this patch: 0109700002ff76eb15. This breaks down as follows:

01 Change bytes
09 Total size of this entry is 9 bytes
7000 Segment offset is 0x0070
02 Change two bytes
ff 76 Original bytes are ff 76
eb 15 Change them to eb 15

I chose this example because it’s one of the patches I wrote. It fixes a bug in a sound card driver that corrupts the upper 16 bits of extended 32-bit registers in a hardware interrupt handler. The corruption happens in a debug logging function, so the patch replaces a section of the logging function with eb 15, which is the encoding of an unconditional jump forward by 0x15 bytes. This skips over the section that corrupts the registers and resumes execution at a harmless point later in that function.

The corruption happens because the driver calls a function which corrupts the upper 16 bits of extended 32-bit registers, as is permitted by normal 16-bit code. However, hardware interrupt handlers operate under stricter conditions than normal 16-bit code, and the function in question is not documented as safe to call from a hardware interrupt. This code was always broken, but they mostly got away with it prior to Windows 95.

Before this change, the driver corrupted registers during a hardware interrupts, resulting in unpredictable behavior in the code that was interrupted. (See also: Space aliens.)

Now, with this change, the logging never executes either, but the only place the message gets logged to is the debug terminal, so the only people who see these messages are developers. If the sound card vendor wants to see these messages on their debug terminal, they can fix their bug.

Topics
History

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.

1 comment

Sort by :
  • word merchant 1 hour ago

    That’s a very neat and impressive approach. Hopefully the application compatibility team had enough people on it.

    Were there ever situations where (and I’m not asking for names), an application/driver/whatever did something so evil which was presumably fine (for small values of “fine”) in laxer times, that patching it would’ve reduced its functionality to such an extent, that denying the load altogether would’ve been the most sensible approach for a stable system? Not that you’d be allowed to do this, I get that…