PhilippC / PhilippC/keepass2android

SMB: large database read fails with generic `Failed to read from file`; hard-coded 5 s SMBLibrary response timeout on a single 1 MB `ReadFile`

Open
#3,283 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
C#
Stars
6.2k
Forks
478
Avg merge
1h 4m
Merged PRs (30d)
2

Description

Checks
  • I have read the FAQ section, searched the open issues, and still think this is a new bug.
Describe the bug you encountered:

Written by a LLM

Keepass2Android_log.txt
Keepass2Android_log2.txt
Keepass2Android_log3.txt

Summary

Keepass2Android 1.15-r3 cannot open a 492 KB KeePass database from a TrueNAS SMB share over a
low-throughput link (WireGuard over cellular, measured at ~230 kbit/s). It fails while updating
the remote cache, and the error is reduced to System.Exception: Failed to read from file.

The failure is deterministic, not intermittent: SmbFileStorage.OpenFileForRead requests the
entire file in a single SMB2 READ and SMBLibrary gives that one request a hard, non-extending
5-second deadline. Any database that cannot cross the wire in 5 seconds is permanently
unopenable over SMB — on this link, anything over roughly 500 KB.

The same connection, in the same session, successfully reads the 128-byte key file from the same
share. Only the large file fails. File Manager+ transfers the complete .kdbx from the same
share over the same tunnel without issue.

Reading the code, this looks like SmbFileStorage.OpenFileForRead issuing a single
MaxReadSize (1 MB) SMB2 READ
and giving up on SMBLibrary's hard-coded 5 second response
timeout
, then discarding the NTStatus that would have said so.

Environment

  • Keepass2Android: 1.15-r3 (current release), Google Play build
  • Android 17, Pixel 7 Pro
  • SMB server: TrueNAS (Samba), SMB2/3
  • Network: phone on Verizon cellular → WireGuard → private address of the TrueNAS host
  • Database: Passwords.kdbx, 492,405 bytes, key file KPXC.txt, 128 bytes, blank password

Steps to reproduce

  1. Connect the phone to the private network over WireGuard on cellular data.
  2. Configure a database on an SMB share using the built-in SMB provider, with a separate key file
    in the same directory.
  3. Clear the Keepass2Android cache so the .kdbx is uncached (isCached = False).
  4. Select the database, supply the key file, and unlock.

Actual behavior

The app returns to the previous screen. The log shows:

2:15:29:242 -- smb://<server>/Storage/.../Passwords.kdbx isCached = False   (Thread 30)
2:15:29:242 -- CFS: OpenWhenNoLocalChanges                                  (Thread 30)
...
2:15:30:878 -- smb://<server>/Storage/.../KPXC.txt localVersionHash = E55264FA...
2:15:33:785 -- CFS: Files in Sync                                           (Thread 32)   <-- key file OK
...
2:15:45:191 -- smb://<server>/Storage/.../Passwords.kdbx isCached = False   (Thread 30)
2:15:45:197 -- System.Exception: Failed to read from file
   at Kp2aBusinessLogic.Io.SmbFileStorage.OpenFileForRead(IOConnectionInfo ioc)
   at keepass2android.Io.OfflineSwitchableFileStorage.OpenFileForRead(IOConnectionInfo ioc)
   at keepass2android.Io.CachingFileStorage.UpdateCacheFromRemote(IOConnectionInfo ioc, String cachedFilePath)
   at keepass2android.Io.CachingFileStorage.OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, String cachedFilePath)
   at keepass2android.Io.CachingFileStorage.OpenFileForRead(IOConnectionInfo ioc)
   at keepass2android.PasswordActivity.PreloadDbFile()

Evidence that this is a read-path problem, not connect/auth

  1. The stack trace has no SmbConnection..ctor frame. Connect, Login, and TreeConnect all
    succeeded; the throw comes from inside the read loop of OpenFileForRead.

    For contrast, the same log contains a genuine connection failure when the LAN address was tried
    from off-LAN, and it looks different — note the extra frame and different message:

    2:31:08:621 -- System.Exception: Failed to connect to SMB server <lan-server>
       at Kp2aBusinessLogic.Io.SmbFileStorage.SmbConnection..ctor(SmbConnectionInfo info)
       at Kp2aBusinessLogic.Io.SmbFileStorage.OpenFileForRead(IOConnectionInfo ioc)
       ...
    
  2. The 128-byte key file is read successfully from the same share moments before the failure
    (CFS: Files in Sync). Same host, same credentials, same tunnel, same MaxReadSize request
    parameter, same 16-credit CreditCharge. The only variable is how many bytes actually come
    back.

  3. Measured throughput on this link makes the read arithmetically impossible. Copying the same
    492,405-byte database from the same share, over the same tunnel, on the same cellular
    connection, with File Manager+ takes ~17 seconds — about 29 KB/s, or ~230 kbit/s. Meeting
    the 5 s response deadline would require ~800 kbit/s sustained. The read is therefore not
    flaky on this link; it cannot succeed at all.

  4. Time-to-failure is long and variable, which is consistent with a fixed 5 s read deadline
    preceded by a variable-latency setup phase (TCP connect, negotiate, NTLM session setup, tree
    connect, CreateFile — roughly 8–10 round trips before the read clock starts). Note every
    observation is ≥ 5 s. Five reproductions:

    Log Read started Exception Elapsed
    log1 2:15:29.242 2:15:45.197 16.0 s
    log1 2:15:57.355 2:16:14.602 17.2 s
    log2 2:29:33.299 2:29:46.350 13.1 s
    log2 2:31:35.266 2:31:51.609 16.3 s
    log3 2:54:14.504 2:54:21.815 7.3 s
  5. A cached copy of the same database opens and decrypts fine, so this is not KDBX parsing.

  6. Lowering the WireGuard MTU does not help. The tunnel MTU was reduced from 1420 to 1280
    (the IPv6 minimum, below which no path can require further fragmentation) and the failure
    reproduced unchanged. That rules out a path-MTU black hole or fragmentation stall as the
    trigger and leaves plain throughput: 492 KB simply does not arrive within the 5 s response
    deadline on this link. Note the smaller MTU marginally increases per-byte overhead, so it
    cannot have masked a fix. (Measured: the File Manager+ copy above was slower at MTU 1280
    than at 1420, as expected from the extra per-packet overhead.)

Probable cause (from SmbFileStorage.cs @ master)

SmbConnection constructs the client with no timeout argument:

public readonly SMB2Client Client = new SMB2Client();

SMBLibrary 1.5.4 (the pinned version) defaults that constructor to
DefaultResponseTimeoutInMilliseconds = 5000, and there is no setting exposed anywhere in K2A to
raise it.

OpenFileForRead then asks for the entire negotiated maximum in one call:

status = conn.FileStore.ReadFile(out var data, fileHandle, bytesRead, (int)conn.Client.MaxReadSize);
if (status != NTStatus.STATUS_SUCCESS && status != NTStatus.STATUS_END_OF_FILE)
{
    throw new Exception("Failed to read from file");   // <-- status discarded
}

Against Samba, MaxReadSize negotiates to 1,048,576, so the whole 492 KB database is requested as
a single SMB2 READ with CreditCharge = 16, and the entire payload must arrive within 5
seconds or SMB2FileStore.ReadFile returns:

return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT;

Critically, WaitForCommand's stopwatch is started once and is never reset by incoming data:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
while (stopwatch.ElapsedMilliseconds < m_responseTimeoutInMilliseconds && ...)
{
    ...
    m_incomingQueueEventHandle.WaitOne(100);
}
return null;   // -> STATUS_IO_TIMEOUT

So the deadline is 5 s of wall-clock from issuing the READ to having the complete response
received and parsed. A transfer that is progressing normally but slowly is indistinguishable from
a dead connection, and partial progress buys nothing.

492 KB in 5 s requires a sustained ~800 kbit/s. This link measures ~230 kbit/s (see evidence 3),
so the single 1 MB read has no possibility of completing in time — this is a deterministic
failure on any link slower than ~800 kbit/s, not an intermittent one. The 128-byte key file
always makes the deadline; the database never can.
K2A then throws away the STATUS_IO_TIMEOUT / STATUS_INVALID_SMB distinction, which is why the
log has nothing actionable in it.

This also explains why the failure is invisible to the user as anything but "it didn't work", and
why other SMB clients on the same phone and tunnel succeed: they chunk their reads.

Suggested fixes

  1. Include the NTStatus in the exception message. One line, and it makes every future SMB
    report diagnosable:

    throw new Exception($"Failed to read from file: {status}");
    

    The same applies to the other throws in this file, which all discard status:
    Failed to open file (CreateFile), Failed to write to file (WriteFile),
    Failed to query details for …, and the TreeConnect status in SmbConnection..ctor,
    which is currently not checked at all — a failed tree connect surfaces later as the
    misleading Failed to read to ….

  2. Chunk reads to a fixed, smaller size (e.g. 64 KB) instead of MaxReadSize. This is the
    fix that actually restores function, and it is effective independently of the timeout value:
    at 64 KB per read the link only needs ~105 kbit/s to meet the same 5 s deadline, versus
    ~800 kbit/s for a 1 MB read. On the ~230 kbit/s link measured here each 64 KB chunk completes
    in ~2.2 s, comfortably inside the budget, and the database opens. Same reasoning applies to
    MaxWriteSize on the write path, which has the identical single-call structure.

  3. Pass a configurable timeout to SMB2Client. SMBLibrary offers
    SMB2Client(int responseTimeoutInMilliseconds). Issue #3025 / PR #2900 added a configurable
    network timeout, but it does not appear to reach SMBLibrary's per-response timeout, which stays
    at the 5 s default.

  4. Retry / resume rather than abort. The loop already tracks bytesRead; on
    STATUS_IO_TIMEOUT it could retry the same offset a bounded number of times instead of
    discarding the whole partial download.

Separate finding: SMB credentials are still logged in plaintext in 1.15-r3

Issue #3127 reported plaintext WebDAV credentials in the log, and it was closed as fixed in 1.15.
The SMB key-file path is still logged with the password in it. From a 1.15-r3 log:

PasswordActivity: key file type KeyFile!smb%3A%2F%2F%255C<username>%3A<PASSWORD IN CLEARTEXT>%40<host>%2FStorage%2F...%2FKPXC.txt!N!!

The IOConnectionInfo for the key file is written out with credentials intact — only URL-encoded,
which is not redaction. SmbConnectionInfo already has a GetPathWithoutCredentials() helper;
this logging path does not use it.

Impact is the same as #3127: anyone who does what this report asks them to do — enable the debug
log and share it — discloses their share password. I only noticed before uploading. The attached
logs have been redacted manually.

Attached

Keepass2Android_log.txt, Keepass2Android_log2.txt, Keepass2Android_log3.txt — three
reproductions, credentials and hostnames redacted.

Related issues

  • #3041 (open) — SMB against a Fritz!Box. Different failure (Failed to login to SMB as …, empty
    share listing); likely unrelated, but it shares the "no usable error detail" problem, which
    fix (1) above would also help with.
  • #3117 (open) — timeout saving a large database over WebDAV. Same class of size-dependent
    timeout failure on a different storage backend.
  • #3025 (closed) — configurable network timeout.
  • #3127 (closed) — plaintext WebDAV credentials in log; see the section above.
Describe what you expected to happen:

Have Keepass2Android unlock kdbx file while on wireguard using a low bandwidth connection.

What version of Keepass2Android are you using?

1.15-r3

Which version of Android are you on?

17

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The affected entry point is SmbFileStorage.OpenFileForRead in SmbFileStorage.cs; begin by tracing its SMB2 ReadFile loop and the SMB2Client timeout configuration described in the report. Compare the proposed chunking, timeout, retry, status-reporting, and credential-redaction changes, then verify completion against the low-bandwidth read failure and plaintext logging case.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, csharp
Domain
mobile-dev, networking, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.