You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With Vapor 4.76.0 (and presumably all(?) earlier versions) it's quite easy to leak a NIO Channel and a file descriptor because Vapor essentially forgets the active HTTP request. This might be used as a denial-of-service vector if a route that will consistently error asynchronously can be found easily.
Problem in more detail
The issue is that in this code in HTTPServerRequestDecoder.swift
decides that when didError happens we are not going to call read (callRead: false) and when in .writing (which means streaming data to request.body.drain) it also sets the action to .nothing.
The result is that because we in error, we won't run any more user code, we are also not reading or closing.
This however means that the only way for us to get rid of that Channel/socket is if NIO itself can detect that the connection went away. That's not reliably possible because the remote might have just abandoned us, so no more packets will ever arrive. On macOS / iOS even in the face of a FIN/RST NIO won't be able to learn about this because it can only do so reliably if it's registered for reads (we aren't, not reading) or writes (we aren't, not writing).
Long story short: Vapor forgets about this request and NIO can't help it in all situations. Or I guess one could say that Vapor wants a kick from NIO (some read/write error or connection closure) but NIO can't necessarily do that because 1) the remote peer might just abandon us (without RST/FIN) and on Darwin NIO can't reliably detect RST/FIN if nothing's reading or writing.
Possible fix
It depends on what semantics Vapor wants. Probably it might be as easy as doing action: .close when .error whilst .writing. I don't think these errors are recoverable so closing it probably the best idea.
Not sure what exact semantics Vapor wants. If Vapor wants this to be recoverable, then it would need to set callRead: true but immediately dump any further body data received after entering .error.
In general callRead: false should probably not be used in terminal states like .error or .end. Instead, if receiving data in .error we should dump the data or just close the connection/stream.
Reproduction:
For this route
app.on(.POST,"error-later",":delay", body:.stream){ request inletdelayMS= request.parameters.get("delay", as:Int64.self)??2_000letdelay=TimeAmount.milliseconds(delayMS)print("received", request)varhaveWritten= false
returnResponse(status:.ok, version: request.version, body:.init(stream:{ bodyWriter in
request.body.drain{ chunk in
switch chunk {case.buffer(let buffer):
if !haveWritten {
haveWritten = true
print("received first \(buffer.readableBytes) bytes of the request. Scheduling write in \(delay).")return request.eventLoop.flatScheduleTask(in: delay){structE:Error{}return request.eventLoop.makeFailedFuture(E())}.futureResult
}else{print("subsequence \(buffer.readableBytes) bytes")return request.eventLoop.makeSucceededFuture(())}case.end:print("received end", request)return bodyWriter.write(.end)case.error(let error):print("received error", error, request)return bodyWriter.write(.end)}}}))}
As we can see, we have 10 file descriptors to sockets that are CLOSED. In the real world they may also appear as ESTABLISHED because if the remote peer just abandoned it, the kernel also doesn't know it's gone...
The text was updated successfully, but these errors were encountered:
Description
With Vapor 4.76.0 (and presumably all(?) earlier versions) it's quite easy to leak a NIO
Channel
and a file descriptor because Vapor essentially forgets the active HTTP request. This might be used as a denial-of-service vector if a route that will consistently error asynchronously can be found easily.Problem in more detail
The issue is that in this code in
HTTPServerRequestDecoder.swift
decides that when
didError
happens we are not going to callread
(callRead: false
) and when in.writing
(which means streaming data torequest.body.drain
) it also sets theaction
to.nothing
.The result is that because we in error, we won't run any more user code, we are also not reading or closing.
This however means that the only way for us to get rid of that
Channel
/socket is if NIO itself can detect that the connection went away. That's not reliably possible because the remote might have just abandoned us, so no more packets will ever arrive. On macOS / iOS even in the face of a FIN/RST NIO won't be able to learn about this because it can only do so reliably if it's registered for reads (we aren't, not reading) or writes (we aren't, not writing).Long story short: Vapor forgets about this request and NIO can't help it in all situations. Or I guess one could say that Vapor wants a kick from NIO (some read/write error or connection closure) but NIO can't necessarily do that because 1) the remote peer might just abandon us (without RST/FIN) and on Darwin NIO can't reliably detect RST/FIN if nothing's reading or writing.
Possible fix
It depends on what semantics Vapor wants. Probably it might be as easy as doing
action: .close
when.error
whilst.writing
. I don't think these errors are recoverable so closing it probably the best idea.Not sure what exact semantics Vapor wants. If Vapor wants this to be recoverable, then it would need to set
callRead: true
but immediately dump any further body data received after entering.error
.In general
callRead: false
should probably not be used in terminal states like.error
or.end
. Instead, if receiving data in.error
we should dump the data or just close the connection/stream.Reproduction:
For this route
run
maybe 10 times and after a minute or so run (replace
VaporHello
with your binary name)You'll likely see something like
As we can see, we have 10 file descriptors to sockets that are
CLOSED
. In the real world they may also appear asESTABLISHED
because if the remote peer just abandoned it, the kernel also doesn't know it's gone...The text was updated successfully, but these errors were encountered: