Skip to content

Bind fstat$INODE64 on macOS x64 - #855

Open
krejko wants to merge 1 commit into
Listenarrs:canaryfrom
nexalapp:fix/macos-fstat-inode64
Open

Bind fstat$INODE64 on macOS x64#855
krejko wants to merge 1 commit into
Listenarrs:canaryfrom
nexalapp:fix/macos-fstat-inode64

Conversation

@krejko

@krejko krejko commented Aug 20, 2026

Copy link
Copy Markdown

Summary

On macOS x64 the library scan finds zero files in a populated root and reports success. Every file is discarded as non-regular:

SCANDBG root=/Users/…/audiobooks-test candidates=0 tracked=0 enumeratedDirs=22
LinkSkipped msg=[Non-regular files are not scanned.] path=/…/[Minority Report 5] The Eyes Have It …mp3
LinkSkipped msg=[Non-regular files are not scanned.] path=/…/[Chronicles of Narnia 0] … .m4b
… (14 of 14 files)

The cause is the raw fstat P/Invoke in PinnedDirectoryCreation.NativeInterop:

[DllImport("libc", EntryPoint = "fstat", SetLastError = true)]
private static extern int FStatMac(int fileDescriptor, out MacStatInformation information);

macOS ships two incompatible fstat ABIs. On x86_64 the bare symbol is the pre-10.5 variant whose struct predates the 64-bit inode layout, so parsing the result as MacStatInformation reads st_mode from the wrong offset. C never hits this because <sys/stat.h> redirects fstatfstat$INODE64; a P/Invoke binds the legacy symbol directly.

HandleIsRegularFile then evaluates garbage and every regular file is rejected.

Evidence

Same file, same modern struct layout, both ABIs:

Arch Symbol mode type regular? size
arm64 fstat 0x81c0 0x8000 true 375267
arm64 fstat$INODE64 symbol not present
x86_64 fstat 0x25c3 0x2000 false 0
x86_64 fstat$INODE64 0x81c0 0x8000 true 375267

0x2000 is S_IFCHR — a character device. The legacy struct puts other fields where st_mode is expected.

Why directories still enumerate (22 of them above) while files vanish: directory identity compares captured-vs-current values, and consistently wrong values still compare equal. Files fail an absolute == 0x8000 check, so they all drop out — which is why the scan looks healthy and simply returns nothing.

Changes

Fixed

  • Dispatch the macOS fstat binding on process architecture: fstat$INODE64 on x64, the bare fstat on arm64.

arm64 must keep the unsuffixed symbol — fstat$INODE64 does not exist there and binding it throws EntryPointNotFoundException (verified above).

Testing

The ABI comparison above was produced by calling both symbols through ctypes with the modern struct, under arch -arm64 and arch -x86_64 on the same machine and file.

dotnet build clean. The infrastructure project compiles with no new warnings.

Not runtime-verified on x64. I develop on Apple Silicon, and exercising this path requires running the whole host under Rosetta. The symbol-level evidence is direct, but I would value a check from anyone on an Intel Mac before merge.

Notes

Scoped strictly to the symbol binding — the struct layout, call sites and every other native path are untouched. arm64 behaviour is unchanged (the bare symbol was already correct there), so this only affects the x64 build, which is the one CI publishes for macOS.

🤖 Generated with Claude Code

macOS exposes two incompatible fstat ABIs. On x86_64 the bare "fstat" symbol is
the pre-10.5 variant whose struct predates the 64-bit inode layout, so reading
the result as MacStatInformation takes st_mode from the wrong offset. C code
never hits this because <sys/stat.h> redirects fstat to fstat$INODE64, but a raw
P/Invoke binds the legacy symbol.

The practical effect is that HandleIsRegularFile misreports every regular file,
so ScanFileDiscovery discards all of them:

    LinkSkipped msg=[Non-regular files are not scanned.] path=/.../book.m4b

An unmatched scan over a populated root then completes successfully with zero
candidates, because directory identity only compares captured-vs-current values
(consistent garbage still matches) while files fail an absolute check.

Verified against the same file on both ABIs, parsing the modern struct:

    arm64    fstat          mode=0x81c0 type=0x8000 regular=True  size=375267
    arm64    fstat$INODE64  symbol not present
    x86_64   fstat          mode=0x25c3 type=0x2000 regular=False size=0
    x86_64   fstat$INODE64  mode=0x81c0 type=0x8000 regular=True  size=375267

Dispatch on process architecture: x64 needs the $INODE64 suffix, arm64 must keep
the bare symbol because the suffixed one does not exist there and binding it
throws EntryPointNotFoundException.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@krejko
krejko requested a review from a team August 20, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant