Skip to content

Root-folder identity failure escapes as an unhandled exception instead of IdentityUnsupported, stopping the host on kernels < 4.11 #815

Description

@Logan-Quest

Summary

On hosts with a kernel older than 4.11, STATX_BTIME is unavailable, so directory identity capture fails. The codebase already models this outcome, DirectoryObjectIdentityFailureKind.IdentityUnsupported, with a user-facing message, but the scan authorization path doesn't route through it. The PlatformNotSupportedException escapes into a BackgroundService, and under .NET's default StopHost behavior the entire host shuts down. The user sees an API 502 and a container restart loop instead of the intended "this storage location does not expose the directory identity Listenarr requires" message.

Environment

  • Listenarr ghcr.io/listenarrs/listenarr:canary, version 1.3.0
  • Synology DSM, kernel 4.4.302+; btrfs on /volume1/Music/Audiobooks bind-mounted to /audiobooks
  • Source references below are against canary f27c798

Mechanism

PinnedDirectoryCreation.NativeOperations.cs:223-247 requires birth time:

const uint requiredMask = 0x00000100 | 0x00000800; // STATX_INO | STATX_BTIME
if (Statx(handle.DangerousGetHandle().ToInt32(), string.Empty, 0x1000, requiredMask, out var information) != 0)
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}
if ((information.Mask & requiredMask) != requiredMask)
{
    throw new PlatformNotSupportedException(
        "The filesystem does not expose complete directory generation identity.");
}

statx landed in Linux 4.11. The interop targets the glibc wrapper (NativeInterop.cs:310), which on older kernels falls back to fstatat and returns success with STATX_BTIME absent, so the mask check at 235 throws on every call. DSM 7.x ships kernel 4.4 which is below 4.11.

DirectoryObjectIdentityResolver.cs:112 already classifies this correctly:

PlatformNotSupportedException => DirectoryObjectIdentityFailureKind.IdentityUnsupported,

and RootFolderStorageHealthResolver.cs:227-228 renders a clear message for it. But the scan path (ScanPathAuthorizationService.cs:288-321) doesn't go through the resolver, it surfaces the generic "The configured scan root no longer identifies its enrolled physical generation", which misdirects users into re-adding the root folder. That never helps: enrollment and validation share the capture path, so it fails at capture, not comparison.

Introduced in #717 (NativeOperations.cs added there; the requiredMask lines are new in that diff).

Reproduce

  1. Run Listenarr on a host with kernel < 4.11 (e.g., Synology DSM 7.x).
  2. Bind-mount a directory to /audiobooks and add it as a root folder, this appears to succeed.
  3. Trigger a library scan and the scan fails, host stops, container restarts.

Diagnostics

$ uname -r
4.4.302+

$ docker exec listenarr stat -f -c 'fstype=%T' /audiobooks
fstype=btrfs

Run as the container's actual runtime user (PUID 1030 / PGID 100), not root, to rule out permissions:

$ docker exec -u 1030:100 listenarr stat -c 'dev=%d ino=%i birth=%W' /audiobooks
dev=46 ino=526181 birth=0

$ docker exec -u 1030:100 listenarr sh -c 'ls -la /audiobooks | head -3'
total 0
drwxrwxrwx 1 1026      users  1794 Aug 12 13:01 .
drwxr-xr-x 1 root      root    236 Aug 12 12:57 ..
drwxrwxrwx 1 listenarr users    28 Oct 19  2025 Alan Bradley

The runtime user can open, stat, and traverse the directory, so this is not an access-permission failure. birth=0 is reported regardless of caller, which is expected: fstatat cannot supply a creation timestamp, so no combination of ownership or mode will satisfy STATX_BTIME on this kernel.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions