Skip to content

fix(files): file registration records the proc-fd link size (64 bytes) instead of the real file length - #901

Merged
therobbiedavis merged 3 commits into
Listenarrs:canaryfrom
kevinheneveld:fix/proc-fd-file-size
Oct 9, 2026
Merged

therobbiedavis merged 3 commits into
Listenarrs:canaryfrom
kevinheneveld:fix/proc-fd-file-size

Conversation

@kevinheneveld

@kevinheneveld kevinheneveld commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

The bug

Since #717, AudiobookFileService reads the registered file's size from the lease's metadata path (new FileInfo(metadataPath).Length). On Linux that path is a /proc/self/fd/<n> magic link, and FileSystemInfo stats the link inode itself — proc magic symlinks report a constant st_size of 64. Result: every file registered through a pinned lease persists Size = 64, regardless of the real file.

Live evidence from my instance (v1.3.0): 361 of 27,912 AudiobookFiles rows have Size = 64 exactly — every one created after the deploy that brought in #717 (sources: download, LibraryScan, MetadataRescan). The UI shows "0 KB" wherever sizes render (the duplicate-ASIN comparison table is where I caught it).

The fix

Open the metadata path and take the stream length. Opening follows the magic link to the exact pinned object the lease holds open, so the size is read from the real file without reintroducing the rename race the metadata path exists to avoid. Applied at both write sites:

  • AudiobookFileService.cs — initial registration (Size = null when the open fails, as before)
  • AudiobookFileService.PhysicalGeneration.cs — generation-replacement snapshots (falls back to the previously recorded size)

Notes

  • ExtractMetadataAsync's cache key uses LastWriteTimeUtc of the same metadata path, which has the same lstat behavior on Linux (the link's mtime, not the file's). It only weakens cache invalidation — the key still includes the physical object identity — so I left it out of scope here; happy to fold it in if you'd like.
  • Existing rows. Nothing in the tree repaired a row already at Size = 64: a library scan only refreshes a file whose physical generation changed, and the metadata rescan only picked files missing duration/format/sample rate. A missing size, or one equal to the 64-byte sentinel, now counts as missing metadata, so the existing rescan job picks the row up and the metadata-only refresh (Fix weak-storage imports and recovery #993's RefreshMetadataAsync) re-reads the length through the same lease the metadata comes from and writes it with the other metadata fields. Identity and ownership stay untouched. No new startup phase — an earlier revision of this PR put the repair in the startup identity reconciliation, which Fix weak-storage imports and recovery #993 has since scoped to pathname identity only, so it was re-expressed on this path when rebasing.

Tests

  • AudioFileServiceTests (Linux): initial registration and a physical-generation refresh through a real PinnedAudiobookFileRegistrationLease, each first asserting the lease's MetadataPath is a /proc/ link whose FileInfo.Length disagrees with the real length. Both fail with the fix reverted (Actual: 64).
  • AudiobookFileServiceMetadataRefreshTests: a row carrying the sentinel ends the metadata-only refresh with the file's real length, ownership and physical evidence untouched.
  • EfAudiobookFileRepositoryTests: sentinel-size and null-size rows with otherwise complete metadata are rescan candidates; a correctly sized row is not.

🤖 Generated with Claude Code

@kevinheneveld
kevinheneveld requested a review from a team August 25, 2026 21:45
kevinheneveld added a commit to kevinheneveld/Listenarr that referenced this pull request Aug 26, 2026
…fd link (PR Listenarrs#901)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
m4bard added a commit to m4bard/Listenarr that referenced this pull request Aug 26, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.
m4bard added a commit to m4bard/Listenarr that referenced this pull request Aug 27, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Aug 28, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Aug 28, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Aug 31, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Aug 31, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 1, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 1, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 1, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 1, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 3, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 3, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 3, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 4, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 4, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 9, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 10, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 2af63b9)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 29, 2026
…ts beside Listenarrs#901

No behaviour change. The gate and its reasoning move from EnsureAudiobookFileCoreAsync
into QualifiesAsAudioContent in a new AudiobookFileService.ContentGate.cs, following the
six partials this class already uses. The call site is four lines.

Why it matters beyond tidiness. BackendArchitectureTests.ActiveProductionSourceFiles_RemainFocused
fails any production file over 500 lines. AudiobookFileService.cs is 469 on canary, so there
are 31 lines of headroom and two open PRs are spending them:

    canary                   469
    Listenarrs#901 alone               494   passes
    this branch before       487   passes
    both together            512   FAILS

Measured by merging this branch onto Listenarrs#901 head 58adddd: 3141 passed, 130 skipped, and that
one architecture test failed. Each PR passes the guard alone and the pair does not, so whoever
merged second would have hit a failure they could not see from their own branch.

This branch's footprint in the shared file drops from 18 lines to 5, which puts the combination
at 499. Suite unchanged at 3142 passed / 0 failed / 130 skipped, and the application project
rebuilds at zero warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e68f6db)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 29, 2026
…t a two-file double

The carried-forward test (dc84254, from our own narrower fix) modeled the
lease's MetadataPath as a second, independent 64-byte file standing in for a
/proc/<pid>/fd/<n> magic link. That is faithful to a fix that reads size
through the lease's own OpenMetadataReadStream(), which is what our fix did.
Listenarrs#901 does not call that: TryGetRegisteredFileLength opens MetadataPath
itself. A real magic link, opened directly, resolves to the pinned file's
real bytes; only lstat'ing it (FileInfo.Length, the original bug) reports a
constant 64. A second, unrelated file at MetadataPath cannot happen with a
real lease, so the two-file double doesn't exercise what Listenarrs#901 actually does,
and both of its behavioural tests still failed (64 instead of the real
length) with Listenarrs#901's fix applied -- not because the bug persists, but because
the double isn't the bug.

Replaced with two tests built on the production
PinnedAudiobookFileRegistrationLease.Open(), already used elsewhere in this
file, so MetadataPath is a real /proc/<pid>/fd/<n> link on this Linux box:
one for EnsureAudiobookFileAsync (the original AudiobookFileService.cs site)
and one for RefreshPhysicalGenerationAsync (the PhysicalGeneration.cs site
Listenarrs#901 also touches, which had no dedicated size test before). Measured
fail-first on canary-m4bard without Listenarrs#901 (both report 64, matching the real
FileInfo/lstat bug on this box), and pass with Listenarrs#901's fix applied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f279b5f)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Sep 29, 2026
…ests

Review nit on item 236 (Listenarrs#901): both new tests already assert MetadataPath
looks like a real /proc/<pid>/fd/<n> link, but nothing checked that lstat
(FileInfo.Length) on that path actually disagrees with the real file, the
way the removed DivergentDescriptorFixture control did. Without that, a
runtime change that made lstat follow the link -- so the 64-byte bug simply
stopped reproducing -- could make either test pass without
TryGetRegisteredFileLength doing anything, since both stat and open would
already agree.

Added the same control inline: Assert.NotEqual between the real byte count
and new FileInfo(MetadataPath).Length, right after the /proc path shape
assertion and before the registration/refresh call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6e3e188)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 0d4e045)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…ts beside Listenarrs#901

No behaviour change. The gate and its reasoning move from EnsureAudiobookFileCoreAsync
into QualifiesAsAudioContent in a new AudiobookFileService.ContentGate.cs, following the
six partials this class already uses. The call site is four lines.

Why it matters beyond tidiness. BackendArchitectureTests.ActiveProductionSourceFiles_RemainFocused
fails any production file over 500 lines. AudiobookFileService.cs is 469 on canary, so there
are 31 lines of headroom and two open PRs are spending them:

    canary                   469
    Listenarrs#901 alone               494   passes
    this branch before       487   passes
    both together            512   FAILS

Measured by merging this branch onto Listenarrs#901 head 58adddd: 3141 passed, 130 skipped, and that
one architecture test failed. Each PR passes the guard alone and the pair does not, so whoever
merged second would have hit a failure they could not see from their own branch.

This branch's footprint in the shared file drops from 18 lines to 5, which puts the combination
at 499. Suite unchanged at 3142 passed / 0 failed / 130 skipped, and the application project
rebuilds at zero warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e68f6db)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…t a two-file double

The carried-forward test (dc84254, from our own narrower fix) modeled the
lease's MetadataPath as a second, independent 64-byte file standing in for a
/proc/<pid>/fd/<n> magic link. That is faithful to a fix that reads size
through the lease's own OpenMetadataReadStream(), which is what our fix did.
Listenarrs#901 does not call that: TryGetRegisteredFileLength opens MetadataPath
itself. A real magic link, opened directly, resolves to the pinned file's
real bytes; only lstat'ing it (FileInfo.Length, the original bug) reports a
constant 64. A second, unrelated file at MetadataPath cannot happen with a
real lease, so the two-file double doesn't exercise what Listenarrs#901 actually does,
and both of its behavioural tests still failed (64 instead of the real
length) with Listenarrs#901's fix applied -- not because the bug persists, but because
the double isn't the bug.

Replaced with two tests built on the production
PinnedAudiobookFileRegistrationLease.Open(), already used elsewhere in this
file, so MetadataPath is a real /proc/<pid>/fd/<n> link on this Linux box:
one for EnsureAudiobookFileAsync (the original AudiobookFileService.cs site)
and one for RefreshPhysicalGenerationAsync (the PhysicalGeneration.cs site
Listenarrs#901 also touches, which had no dedicated size test before). Measured
fail-first on canary-m4bard without Listenarrs#901 (both report 64, matching the real
FileInfo/lstat bug on this box), and pass with Listenarrs#901's fix applied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f279b5f)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…ests

Review nit on item 236 (Listenarrs#901): both new tests already assert MetadataPath
looks like a real /proc/<pid>/fd/<n> link, but nothing checked that lstat
(FileInfo.Length) on that path actually disagrees with the real file, the
way the removed DivergentDescriptorFixture control did. Without that, a
runtime change that made lstat follow the link -- so the 64-byte bug simply
stopped reproducing -- could make either test pass without
TryGetRegisteredFileLength doing anything, since both stat and open would
already agree.

Added the same control inline: Assert.NotEqual between the real byte count
and new FileInfo(MetadataPath).Length, right after the /proc path shape
assertion and before the registration/refresh call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6e3e188)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 0d4e045)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…ts beside Listenarrs#901

No behaviour change. The gate and its reasoning move from EnsureAudiobookFileCoreAsync
into QualifiesAsAudioContent in a new AudiobookFileService.ContentGate.cs, following the
six partials this class already uses. The call site is four lines.

Why it matters beyond tidiness. BackendArchitectureTests.ActiveProductionSourceFiles_RemainFocused
fails any production file over 500 lines. AudiobookFileService.cs is 469 on canary, so there
are 31 lines of headroom and two open PRs are spending them:

    canary                   469
    Listenarrs#901 alone               494   passes
    this branch before       487   passes
    both together            512   FAILS

Measured by merging this branch onto Listenarrs#901 head 58adddd: 3141 passed, 130 skipped, and that
one architecture test failed. Each PR passes the guard alone and the pair does not, so whoever
merged second would have hit a failure they could not see from their own branch.

This branch's footprint in the shared file drops from 18 lines to 5, which puts the combination
at 499. Suite unchanged at 3142 passed / 0 failed / 130 skipped, and the application project
rebuilds at zero warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e68f6db)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…t a two-file double

The carried-forward test (dc84254, from our own narrower fix) modeled the
lease's MetadataPath as a second, independent 64-byte file standing in for a
/proc/<pid>/fd/<n> magic link. That is faithful to a fix that reads size
through the lease's own OpenMetadataReadStream(), which is what our fix did.
Listenarrs#901 does not call that: TryGetRegisteredFileLength opens MetadataPath
itself. A real magic link, opened directly, resolves to the pinned file's
real bytes; only lstat'ing it (FileInfo.Length, the original bug) reports a
constant 64. A second, unrelated file at MetadataPath cannot happen with a
real lease, so the two-file double doesn't exercise what Listenarrs#901 actually does,
and both of its behavioural tests still failed (64 instead of the real
length) with Listenarrs#901's fix applied -- not because the bug persists, but because
the double isn't the bug.

Replaced with two tests built on the production
PinnedAudiobookFileRegistrationLease.Open(), already used elsewhere in this
file, so MetadataPath is a real /proc/<pid>/fd/<n> link on this Linux box:
one for EnsureAudiobookFileAsync (the original AudiobookFileService.cs site)
and one for RefreshPhysicalGenerationAsync (the PhysicalGeneration.cs site
Listenarrs#901 also touches, which had no dedicated size test before). Measured
fail-first on canary-m4bard without Listenarrs#901 (both report 64, matching the real
FileInfo/lstat bug on this box), and pass with Listenarrs#901's fix applied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f279b5f)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 1, 2026
…ests

Review nit on item 236 (Listenarrs#901): both new tests already assert MetadataPath
looks like a real /proc/<pid>/fd/<n> link, but nothing checked that lstat
(FileInfo.Length) on that path actually disagrees with the real file, the
way the removed DivergentDescriptorFixture control did. Without that, a
runtime change that made lstat follow the link -- so the 64-byte bug simply
stopped reproducing -- could make either test pass without
TryGetRegisteredFileLength doing anything, since both stat and open would
already agree.

Added the same control inline: Assert.NotEqual between the real byte count
and new FileInfo(MetadataPath).Length, right after the /proc path shape
assertion and before the registration/refresh call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6e3e188)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 2, 2026
…rrs#901

This PR fixed two things behind the same /proc descriptor link: the
probe's extension guard rejecting the candidate, and FileInfo reporting
the link's own 64 bytes instead of the file's length.

Listenarrs#901 fixes the size half, and fixes it better. It opens the metadata
path and takes the stream length unconditionally. The version here read
the length through the lease's generation-bound stream but fell back to
FileInfo on the metadata path when the lease did not expose one, which
on Linux is the original 64-byte bug again. That half also had no test
of its own here; both test files in this branch cover the scan boundary.

So the size change is dropped and the two shared files go back to
canary. What remains is the half Listenarrs#901 does not touch: the scan passed
the lease's metadata path as both the byte source and the media
identity, and on Linux that path is an extensionless /proc link, so the
audio-extension guard rejected the candidate before ffprobe ever ran.
Passing the candidate as the identity alongside the metadata path as the
byte source keeps the guard working on the real filename.

The two PRs no longer touch a file in common.

(cherry picked from commit 0d4e045)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 2, 2026
…ts beside Listenarrs#901

No behaviour change. The gate and its reasoning move from EnsureAudiobookFileCoreAsync
into QualifiesAsAudioContent in a new AudiobookFileService.ContentGate.cs, following the
six partials this class already uses. The call site is four lines.

Why it matters beyond tidiness. BackendArchitectureTests.ActiveProductionSourceFiles_RemainFocused
fails any production file over 500 lines. AudiobookFileService.cs is 469 on canary, so there
are 31 lines of headroom and two open PRs are spending them:

    canary                   469
    Listenarrs#901 alone               494   passes
    this branch before       487   passes
    both together            512   FAILS

Measured by merging this branch onto Listenarrs#901 head 58adddd: 3141 passed, 130 skipped, and that
one architecture test failed. Each PR passes the guard alone and the pair does not, so whoever
merged second would have hit a failure they could not see from their own branch.

This branch's footprint in the shared file drops from 18 lines to 5, which puts the combination
at 499. Suite unchanged at 3142 passed / 0 failed / 130 skipped, and the application project
rebuilds at zero warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e68f6db)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 2, 2026
…t a two-file double

The carried-forward test (dc84254, from our own narrower fix) modeled the
lease's MetadataPath as a second, independent 64-byte file standing in for a
/proc/<pid>/fd/<n> magic link. That is faithful to a fix that reads size
through the lease's own OpenMetadataReadStream(), which is what our fix did.
Listenarrs#901 does not call that: TryGetRegisteredFileLength opens MetadataPath
itself. A real magic link, opened directly, resolves to the pinned file's
real bytes; only lstat'ing it (FileInfo.Length, the original bug) reports a
constant 64. A second, unrelated file at MetadataPath cannot happen with a
real lease, so the two-file double doesn't exercise what Listenarrs#901 actually does,
and both of its behavioural tests still failed (64 instead of the real
length) with Listenarrs#901's fix applied -- not because the bug persists, but because
the double isn't the bug.

Replaced with two tests built on the production
PinnedAudiobookFileRegistrationLease.Open(), already used elsewhere in this
file, so MetadataPath is a real /proc/<pid>/fd/<n> link on this Linux box:
one for EnsureAudiobookFileAsync (the original AudiobookFileService.cs site)
and one for RefreshPhysicalGenerationAsync (the PhysicalGeneration.cs site
Listenarrs#901 also touches, which had no dedicated size test before). Measured
fail-first on canary-m4bard without Listenarrs#901 (both report 64, matching the real
FileInfo/lstat bug on this box), and pass with Listenarrs#901's fix applied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f279b5f)
m4bard added a commit to m4bard/Listenarr that referenced this pull request Oct 2, 2026
…ests

Review nit on item 236 (Listenarrs#901): both new tests already assert MetadataPath
looks like a real /proc/<pid>/fd/<n> link, but nothing checked that lstat
(FileInfo.Length) on that path actually disagrees with the real file, the
way the removed DivergentDescriptorFixture control did. Without that, a
runtime change that made lstat follow the link -- so the 64-byte bug simply
stopped reproducing -- could make either test pass without
TryGetRegisteredFileLength doing anything, since both stat and open would
already agree.

Added the same control inline: Assert.NotEqual between the real byte count
and new FileInfo(MetadataPath).Length, right after the /proc path shape
assertion and before the registration/refresh call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6e3e188)
@m4bard

m4bard commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

I ran part of this today against our own fork's build, carrying this PR's commit alongside other unrelated fork patches, not a stock canary instance. What I have below is evidence of how real the underlying bug is, plus a test-double finding, not confirmation that the fix itself works end to end on a live install. More on why further down. As far as I can tell the diagnosis is specific and the fix matches it: reading the metadata path instead of lstat'ing it is exactly what's needed once you know a /proc/<pid>/fd/<n> magic link always reports a constant 64-byte lstat size, regardless of the real file behind it.

On the backfill question the PR body already flags: rows written before this fix was in place currently carry Size = 64 on this install, a large fraction of registered audio files, which is consistent with the diagnosis at real scale rather than just the small repro in the PR description. I didn't get to test whether re-registering one of those rows corrects it, so I can't say anything either way about that part.

I wasn't able to finish the other half of the check this pass, whether a freshly scanned file gets the correct size going forward. That's blocked by something unrelated to this PR on this particular install, not a problem with the fix itself.

One thing on the test side: we wrote our own regression test for this on our branch, and the first version modeled the lease's MetadataPath as a second, ordinary file standing in for the /proc/<pid>/fd/<n> link rather than a real one. That double doesn't reproduce the actual lstat-on-a-magic-link behavior, since a plain file doesn't carry the constant 64-byte lstat quirk a proc magic link does, so the test was passing for the wrong reason. We replaced it with a version that opens a genuine /proc/<pid>/fd/<n> link to a real file, plus an assertion that lstat on that link actually disagrees with the real file's length, so the test proves its own setup reproduces the quirk before it asserts anything about the fix. I don't see a test in this PR's diff; happy to share ours if it's useful, no pressure either way.

Unless I'm missing something, this looks solid. Does the backfill behavior for existing rows match what you'd expect, or is there a migration or re-scan path already planned for those?

Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.

@kevinheneveld
kevinheneveld force-pushed the fix/proc-fd-file-size branch from 58adddd to 5fbd36c Compare October 6, 2026 00:21
@kevinheneveld

Copy link
Copy Markdown
Contributor Author

Thanks for running it at scale, @m4bard — the Size = 64 fraction on your install is the same picture I had here (361 rows on mine, every one created after #717 landed).

Tests. You're right that the PR shipped without one, and your point about the stand-in is exactly the trap: a plain file can't carry the proc-link lstat quirk, so a test built that way passes for the wrong reason. I've pushed 5fbd36c0 with two Linux tests in AudioFileServiceTests that go through a real PinnedAudiobookFileRegistrationLease — initial registration, and a physical-generation refresh over a replaced file (the second write site in AudiobookFileService.PhysicalGeneration.cs). Each first asserts that the lease's MetadataPath is a /proc/ link and that FileInfo.Length on it disagrees with the file's real length, then asserts the persisted Size is the real length. With the fix reverted both fail with Actual: 64; with it, both pass (run in a Linux container against this branch). If yours covers a case these don't, I'd still take it.

Existing rows. To be precise about what the current tree does, since I checked rather than assumed: nothing repairs a Size = 64 row on its own. A library scan only refreshes a tracked file whose physical generation changed (AudiobookScanService.Reconciliation.cs), and the background metadata rescan only picks files missing duration/format/sample rate (GetMissingMetadataAsync), which these rows aren't. So rows written before the fix stay at 64 until the file is re-registered or replaced. On my instance I backfilled them with a one-off script from disk. The PR body's suggestion still stands: a startup reconciliation that re-reads Size for rows at exactly 64 whose path resolves would close this, and I'm happy to add it here or as a follow-up — that's the maintainer's call on whether to bundle a data repair with the fix.

Also rebased onto current canary (a630572e).

Disclosure: drafted with Claude Code at my direction; I read the cited code and the test output before posting.

@m4bard

m4bard commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for adding the tests, and for tracing what the scan and the metadata rescan actually do with these rows.

On your question: I compared ours against 5fbd36c0, and I don't think ours covers anything yours doesn't. They are close to the same two tests. Both hit the same two write sites through a real PinnedAudiobookFileRegistrationLease, and both check that lstat disagrees with the real length before asserting the size. The differences I can see are small. Ours checks the prefix /proc/{pid}/fd/ where yours checks /proc/, and ours replaces the file by delete-and-rewrite where yours renames over it; either way the replacement gets a new inode. Yours also asserts the predecessor's size and that the refresh kept the same row id, which ours doesn't. So I'd drop ours. That comparison comes from reading the two diffs; I haven't run yours.

On existing rows, your read of the scan matches mine. In AudiobookScanService.Reconciliation.cs at a630572e, the refresh only fires when the physical generation changed or the identity is missing (lines 146-170), so a row stuck at 64 stays at 64. In case it helps the maintainer weigh it, Sonarr and Readarr both handle the general case in their disk scan. Sonarr rewrites Size for any tracked file whose on-disk size differs (DiskScanService.cs lines 145-171 at 76c684e0), and Readarr does the same, together with the modified time (DiskScanService.cs lines 197-220 at 0b79d300). As I see it there are roughly three ways to go:

  • The targeted startup repair you described, for rows at exactly 64 whose path resolves. It's small and one-off, and it cleans up this bug's leftovers and nothing else.
  • A size check in the scan, the way Sonarr and Readarr do it. That would also catch future drift, but it changes a reconciliation path that is deliberately identity-gated today, so it's a bigger review.
  • The fix on its own, with any repair as a follow-up.

I don't have a strong view between them, and as you said, it's the maintainer's call.

One small heads-up: my #1022 (the import-and-scan bundle, which carries the change from #999) touches the lines next to yours in AudiobookFileService.cs, and the two conflict in one hunk (checked with git merge-tree against both PR heads). It's mechanical. If yours lands first, I'll rebase #1022. If #1022 goes in first, the resolution is to keep its QualifiesAsAudioContent check and drop the old var fi line.

Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.

@kevinheneveld

Copy link
Copy Markdown
Contributor Author

Follow-up on the backfill question: the repair is now in this PR (acc2f868).

The startup identity reconciliation (AudiobookFileIdentityReconciler) already pins every tracked file and checks its live generation against the row, so it now reads the byte length from that same pinned handle and persists it for rows whose stored size is the 64-byte sentinel or null. It does so only when the live object is the generation on record — a replaced file never lends its size to a row that still describes the old one; those stay with the existing mismatch handling. No extra opens: the handle was already there. One pass on the next start repairs them, and the count is logged with the reconciliation summary.

Test: ReconcileAsync_ProcLinkSentinelSize_IsRepairedOnlyFromTheVerifiedGeneration — sentinel row repaired, null-size row backfilled, correct row untouched, generation-mismatch row left alone. PR body updated to match.

Disclosure: drafted with Claude Code at my direction; I read the code and the test output before posting.

@therobbiedavis

Copy link
Copy Markdown
Collaborator

@kevinheneveld Can you please rebase this?

@therobbiedavis therobbiedavis added the patch patch version bump - backward compatible bug fixes label Oct 9, 2026
kevinheneveld and others added 3 commits October 9, 2026 08:37
…) instead of the real file length

Since Listenarrs#717, file registration reads sizes from the lease's metadata path,
which on Linux is a /proc/self/fd magic link. FileSystemInfo.Length reads
the link inode itself (lstat), and proc magic symlinks report a constant
st_size of 64 — so every file registered through a pinned lease persists
Size = 64 regardless of the actual file. The UI then shows 0 KB wherever
sizes are rendered (duplicate comparison, library size totals, quality
scoring inputs).

Open the metadata path instead and take the stream length: opening follows
the magic link to the exact pinned object the lease holds, so the length
is read from the real file without reintroducing the rename race the
metadata path exists to avoid. Applies to both initial registration and
physical-generation replacement snapshots (the latter falls back to the
prior recorded size when the open fails).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he proc-fd link's

Two Linux tests exercise both write sites through a real
PinnedAudiobookFileRegistrationLease — initial registration and a
physical-generation refresh over a replaced file — and assert the
persisted Size is the file's byte length.

Each test first proves its own setup reproduces the quirk it guards
against: the lease's MetadataPath is a /proc/ magic link and
FileInfo.Length (an lstat) on it disagrees with the real length. A plain
file standing in for the link would not carry that behaviour, and the
test would pass for the wrong reason. Both fail with the fix reverted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-fd link

Rows registered on Linux before the fix carry Size = 64 — the lstat size
of the lease's /proc/self/fd link — with otherwise complete metadata, and
nothing corrected them: a library scan only refreshes a file whose
generation changed, and the metadata rescan only picked files missing
duration, format or sample rate.

A missing size, or one equal to that sentinel, now counts as missing
metadata, so the existing rescan job picks the row up and the
metadata-only refresh re-reads the length through the same lease the
metadata comes from (the pinned open, never an lstat of the metadata
path) and writes it with the other metadata fields. Identity and
ownership stay untouched, as before. No new startup phase: the repair
rides the path Listenarrs#993 made for exactly this kind of refresh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kevinheneveld
kevinheneveld force-pushed the fix/proc-fd-file-size branch from acc2f86 to 4727de8 Compare October 9, 2026 16:48
@kevinheneveld

Copy link
Copy Markdown
Contributor Author

Rebased onto canary at e3f6fd2a (v1.3.5).

The fix and its tests applied cleanly. The size-repair commit did not: #993 scoped the startup identity reconciliation to pathname identity only, so a repair riding its physical-generation check no longer had anything to ride. It is re-expressed on the path #993 added for exactly this kind of refresh: a missing size, or one equal to the 64-byte sentinel, now counts as missing metadata, so the existing rescan job picks the row up and RefreshMetadataAsync re-reads the length through the same lease the metadata comes from and writes it with the other metadata fields. Identity and ownership stay untouched. That adds a long? size parameter to IAudiobookFileRepository.RefreshMetadataAsync (one implementation, no other callers) and changes one assertion in AudiobookFileServiceMetadataRefreshTests — the refresh now writes the size rather than preserving it, which is the intent.

PR body updated to match. Happy to drop the third commit if you'd rather land the two-line fix on its own.

Disclosure: drafted with Claude Code at my direction; I read the cited code and the test output before posting.

@therobbiedavis therobbiedavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@therobbiedavis
therobbiedavis merged commit f3a419a into Listenarrs:canary Oct 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch patch version bump - backward compatible bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants