Floating-point exception traps crash native plugins in Double Commander
The MediaInfo content plugin shows image and video dimensions in a Double Commander column. It worked in tests — then crashed Double Commander with an "Access violation" dialog the instant a folder of old camera JPEGs scrolled into view. The same code reading the same files in a standalone process never crashed. That "only inside the host" signature is the whole story.
The symptom
Browsing a directory of ~340 Canon JPEGs, Double Commander threw an unhandled EAccessViolation. The stack trace pointed entirely into Apple system libraries:
Unhandled exception: EAccessViolation: Access violation
$... in /usr/lib/system/libsystem_m.dylib
$... in /System/Library/CoreServices/RawCamera.bundle/.../RawCamera
$... in /System/Library/CoreServices/RawCamera.bundle/.../RawCamera
...
$... in /System/Library/Frameworks/ImageIO.framework/.../ImageIO
So: ImageIO, while copying image properties (it parses EXIF MakerNotes), routed into Apple's RawCamera decoder, which did some math in libsystem_m — and that faulted. But only inside Double Commander.
Isolating it
Running the exact same CGImageSourceCopyPropertiesAtIndex over every one of those files in a small standalone tool: zero crashes. The files weren't corrupt. The crash needed something the host was doing.
The root cause: FPC enables FP-exception traps
Double Commander is built with Free Pascal (FPC), whose runtime enables floating-point exception traps (invalid operation, divide-by-zero, overflow). In a normal C/Cocoa process those exceptions are masked — operations that produce NaN or infinity just flow through silently. Apple's frameworks are written assuming that masked default. RawCamera's MakerNote math hits one of those normally-harmless conditions; under FPC's unmasked environment it raises a hardware trap, which the FPC runtime reports as an access violation.
This is why the standalone tool was fine (traps masked) and the host crashed (traps enabled). It can be reproduced deterministically by toggling the FPU control register the way FPC does. On arm64 that's FPCR bits 8–10:
uint64_t fpcr;
__asm__ volatile("mrs %0, fpcr" : "=r"(fpcr));
fpcr |= (1u<<8) | (1u<<9) | (1u<<10); // IOE | DZE | OFE (trap on invalid/div0/overflow)
__asm__ volatile("msr fpcr, %0" :: "r"(fpcr));
| Condition | Result over the 339 JPEGs |
|---|---|
| Normal process (traps masked) | reads all fine |
| FP traps enabled (like FPC / Double Commander) | crashes with a signal |
| Traps enabled, but masked around the ImageIO call | reads all fine |
The fix
Save the host's floating-point environment, switch to the default (masked) environment for the duration of the framework call, then restore exactly what the host had. <fenv.h> makes this portable across arm64 and x86_64:
#import <fenv.h>
fenv_t hostEnv;
fegetenv(&hostEnv); // remember the host's FP environment (traps enabled)
fesetenv(FE_DFL_ENV); // mask FP exceptions for the framework call
@try {
info = ParseImage(url); // ImageIO / AVFoundation / CGPDF run here, safely
} @finally {
fesetenv(&hostEnv); // restore the host's environment exactly as found
}
The @try/@finally guarantees the host environment is restored even if parsing throws. With this in place, the plugin reads the entire problem folder — and any RAW or camera-JPEG folder — without crashing the host.
Takeaways
- The FP environment is part of the host contract. A Free Pascal host enables exception traps that most native code assumes are masked.
- Mask around system-framework calls and restore.
fegetenv/fesetenv(FE_DFL_ENV)/ restore is the portable pattern. - Reproduce the host condition deterministically. Toggling
FPCRturned an intermittent "only in the app" crash into a one-line, repeatable test.