Skip to content

Commit a024b83

Browse files
authored
Fix hang in SftpClient.UploadFile upon error (#1643)
* Fix deadlock in SftpClient.UploadFile upon error * Make RequestWrite deterministic wrt. exception handling * add regression test; fix race * x
1 parent 4fcf604 commit a024b83

File tree

4 files changed

+335
-41
lines changed

4 files changed

+335
-41
lines changed

src/Renci.SshNet/Sftp/Responses/SftpStatusResponse.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public SftpStatusResponse(uint protocolVersion)
1212
{
1313
}
1414

15-
public StatusCodes StatusCode { get; private set; }
15+
public StatusCodes StatusCode { get; set; }
1616

1717
public string ErrorMessage { get; private set; }
1818

@@ -39,5 +39,12 @@ protected override void LoadData()
3939
Language = ReadString(Ascii);
4040
}
4141
}
42+
43+
protected override void SaveData()
44+
{
45+
base.SaveData();
46+
47+
Write((uint)StatusCode);
48+
}
4249
}
4350
}

src/Renci.SshNet/Sftp/SftpSession.cs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Globalization;
45
using System.Text;
56
using System.Threading;
@@ -914,6 +915,8 @@ public void RequestWrite(byte[] handle,
914915
AutoResetEvent wait,
915916
Action<SftpStatusResponse> writeCompleted = null)
916917
{
918+
Debug.Assert((wait is null) != (writeCompleted is null), "Should have one parameter or the other.");
919+
917920
SshException exception = null;
918921

919922
var request = new SftpWriteRequest(ProtocolVersion,
@@ -925,22 +928,27 @@ public void RequestWrite(byte[] handle,
925928
length,
926929
response =>
927930
{
928-
writeCompleted?.Invoke(response);
929-
930-
exception = GetSftpException(response);
931-
wait?.SetIgnoringObjectDisposed();
931+
if (writeCompleted is not null)
932+
{
933+
writeCompleted.Invoke(response);
934+
}
935+
else
936+
{
937+
exception = GetSftpException(response);
938+
wait.SetIgnoringObjectDisposed();
939+
}
932940
});
933941

934942
SendRequest(request);
935943

936944
if (wait is not null)
937945
{
938946
WaitOnHandle(wait, OperationTimeout);
939-
}
940947

941-
if (exception is not null)
942-
{
943-
throw exception;
948+
if (exception is not null)
949+
{
950+
throw exception;
951+
}
944952
}
945953
}
946954

@@ -2272,7 +2280,7 @@ public uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle)
22722280
return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields;
22732281
}
22742282

2275-
private static SshException GetSftpException(SftpStatusResponse response)
2283+
internal static SshException GetSftpException(SftpStatusResponse response)
22762284
{
22772285
#pragma warning disable IDE0010 // Add missing cases
22782286
switch (response.StatusCode)

src/Renci.SshNet/SftpClient.cs

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#nullable enable
22
using System;
33
using System.Collections.Generic;
4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Globalization;
67
using System.IO;
78
using System.Net;
89
using System.Runtime.CompilerServices;
10+
using System.Runtime.ExceptionServices;
911
using System.Text;
1012
using System.Threading;
1113
using System.Threading.Tasks;
@@ -2456,56 +2458,82 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo
24562458
// create buffer of optimal length
24572459
var buffer = new byte[_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle)];
24582460

2459-
var bytesRead = input.Read(buffer, 0, buffer.Length);
2461+
int bytesRead;
24602462
var expectedResponses = 0;
2461-
var responseReceivedWaitHandle = new AutoResetEvent(initialState: false);
24622463

2463-
do
2464+
// We will send out all the write requests without waiting for each response.
2465+
// Afterwards, we may wait on this handle until all responses are received
2466+
// or an error has occured.
2467+
using var mres = new ManualResetEventSlim(initialState: false);
2468+
2469+
ExceptionDispatchInfo? exception = null;
2470+
2471+
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) != 0)
24642472
{
2465-
// Cancel upload
24662473
if (asyncResult is not null && asyncResult.IsUploadCanceled)
24672474
{
24682475
break;
24692476
}
24702477

2471-
if (bytesRead > 0)
2478+
exception?.Throw();
2479+
2480+
var writtenBytes = offset + (ulong)bytesRead;
2481+
2482+
_ = Interlocked.Increment(ref expectedResponses);
2483+
mres.Reset();
2484+
2485+
_sftpSession.RequestWrite(handle, offset, buffer, offset: 0, bytesRead, wait: null, s =>
24722486
{
2473-
var writtenBytes = offset + (ulong)bytesRead;
2487+
var setHandle = false;
2488+
2489+
try
2490+
{
2491+
if (Sftp.SftpSession.GetSftpException(s) is Exception ex)
2492+
{
2493+
exception = ExceptionDispatchInfo.Capture(ex);
2494+
}
24742495

2475-
_sftpSession.RequestWrite(handle, offset, buffer, offset: 0, bytesRead, wait: null, s =>
2496+
if (exception is not null)
24762497
{
2477-
if (s.StatusCode == StatusCodes.Ok)
2478-
{
2479-
_ = Interlocked.Decrement(ref expectedResponses);
2480-
_ = responseReceivedWaitHandle.Set();
2498+
setHandle = true;
2499+
return;
2500+
}
24812501

2482-
asyncResult?.Update(writtenBytes);
2502+
Debug.Assert(s.StatusCode == StatusCodes.Ok);
24832503

2484-
// Call callback to report number of bytes written
2485-
if (uploadCallback is not null)
2486-
{
2487-
// Execute callback on different thread
2488-
ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes));
2489-
}
2490-
}
2491-
});
2504+
asyncResult?.Update(writtenBytes);
2505+
2506+
// Call callback to report number of bytes written
2507+
if (uploadCallback is not null)
2508+
{
2509+
// Execute callback on different thread
2510+
ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes));
2511+
}
2512+
}
2513+
finally
2514+
{
2515+
if (Interlocked.Decrement(ref expectedResponses) == 0 || setHandle)
2516+
{
2517+
mres.Set();
2518+
}
2519+
}
2520+
});
24922521

2493-
_ = Interlocked.Increment(ref expectedResponses);
2522+
offset += (ulong)bytesRead;
2523+
}
24942524

2495-
offset += (ulong)bytesRead;
2525+
// Make sure the read of exception cannot be executed ahead of
2526+
// the read of expectedResponses so that we do not miss an
2527+
// exception.
24962528

2497-
bytesRead = input.Read(buffer, 0, buffer.Length);
2498-
}
2499-
else if (expectedResponses > 0)
2500-
{
2501-
// Wait for expectedResponses to change
2502-
_sftpSession.WaitOnHandle(responseReceivedWaitHandle, _operationTimeout);
2503-
}
2529+
if (Volatile.Read(ref expectedResponses) != 0)
2530+
{
2531+
_sftpSession.WaitOnHandle(mres.WaitHandle, _operationTimeout);
25042532
}
2505-
while (expectedResponses > 0 || bytesRead > 0);
2533+
2534+
exception?.Throw();
25062535

25072536
_sftpSession.RequestClose(handle);
2508-
responseReceivedWaitHandle.Dispose();
25092537
}
25102538

25112539
private async Task InternalUploadFileAsync(Stream input, string path, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)