Skip to content

Linux store throws TypeInitializationException (not DllNotFoundException) on every call when libsecret is missing, so consumers can't detect it and fall back #168

Description

@matt-edmondson

What's wrong

LinuxSecretServiceCredentialStore builds its schema handle in a static field of a nested class (Storage/LinuxSecretServiceCredentialStore.cs, ~line 157):

private static class Schema
{
    internal static readonly IntPtr Handle = NativeMethods.secret_schema_new(...);
}

On a machine without libsecret-1.so.0 (a headless server, SSH session, or container), the P/Invoke's DllNotFoundException is thrown inside the static initializer. .NET wraps it in a TypeInitializationException, and every later access to Schema throws that same wrapped exception again.

The class remarks (lines 18–19) tell consumers that the provider "will fail at the first operation; consumers should detect this and fall back to InMemoryCredentialStore". But what they receive is a TypeInitializationException whose message is "The type initializer for 'Schema' threw an exception.", not the DllNotFoundException or CredentialStoreException a caller would reasonably catch.

Repro

In a Linux container with no libsecret (ldconfig -p | grep libsecret is empty):

var store = CredentialStoreFactory.CreateDefault("x");
store.TryLoad(...); // System.TypeInitializationException -> DllNotFoundException: Unable to load shared library 'libsecret-1.so.0'
store.TryLoad(...); // same TypeInitializationException again

Why it matters

Consumers following the documented "detect and fall back" advice miss this case. For example, OAICLI's Auth.IsMissingSecretStore matches PlatformNotSupportedException or DllNotFoundException or EntryPointNotFoundException, so on exactly the headless-Linux setup that fallback was written for, users get Error: The type initializer for 'Schema' threw an exception. and exit code 255 (tracked on the OAICLI side separately).

Suggested fix

  • Create the schema lazily inside a method (e.g. a Lazy<IntPtr> or a guarded static getter), not in a type initializer.
  • Catch DllNotFoundException / EntryPointNotFoundException there and rethrow as CredentialStoreException (or PlatformNotSupportedException) with a message naming libsecret, so there is one documented exception type to catch.
  • Update the class remarks to name the exception consumers should catch.

Acceptance criteria

  • Without libsecret, the first and every later operation throw the documented exception type, never a TypeInitializationException.
  • A test covers the missing-library path, at least by exercising the translation logic.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    readyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions