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`
Nobody has claimed this yet.
- 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 fileKPXC.txt, 128 bytes, blank password
Steps to reproduce
- Connect the phone to the private network over WireGuard on cellular data.
- Configure a database on an SMB share using the built-in SMB provider, with a separate key file
in the same directory. - Clear the Keepass2Android cache so the
.kdbxis uncached (isCached = False). - 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
-
The stack trace has no
SmbConnection..ctorframe. Connect,Login, andTreeConnectall
succeeded; the throw comes from inside the read loop ofOpenFileForRead.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) ... -
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, sameMaxReadSizerequest
parameter, same 16-creditCreditCharge. The only variable is how many bytes actually come
back. -
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. -
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 -
A cached copy of the same database opens and decrypts fine, so this is not KDBX parsing.
-
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
-
Include the
NTStatusin 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 theTreeConnectstatus inSmbConnection..ctor,
which is currently not checked at all — a failed tree connect surfaces later as the
misleadingFailed to read to …. -
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
MaxWriteSizeon the write path, which has the identical single-call structure. -
Pass a configurable timeout to
SMB2Client.SMBLibraryoffers
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. -
Retry / resume rather than abort. The loop already tracks
bytesRead; on
STATUS_IO_TIMEOUTit 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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