Skip to content

Commit f245ffb

Browse files
authored
fix: reject stale sessions without retaining past responses (#1282)
1 parent d547e01 commit f245ffb

2 files changed

Lines changed: 32 additions & 14 deletions

File tree

‎WebDriverAgentLib/Routing/FBHTTPServer.m‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import "FBLogger.h"
1414
#import "FBResponsePayload.h"
1515
#import "FBTCPSocket.h"
16+
#import "FBSession.h"
1617

1718
static NSData *FBCRLFCRLFData(void)
1819
{
@@ -131,10 +132,6 @@ @interface FBHTTPServer () <FBTCPSocketDelegate>
131132
// standalone or not (except DELETE /session itself - see -dispatchMethod:). See
132133
// -abandonPendingRequestsForSessionID:. Guarded by @synchronized(self.pendingSessionRequests).
133134
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableSet<FBPendingRequest *> *> *pendingSessionRequests;
134-
// Already-abandoned sessions mapped to the response they were abandoned with, so a request parsed
135-
// after that point is answered at once instead of queueing for a session that is gone. Kept for
136-
// the server's lifetime; ids are UUIDs. Guarded by @synchronized(self.pendingSessionRequests).
137-
@property (nonatomic, strong) NSMutableDictionary<NSString *, RouteResponse *> *abandonedSessionResponses;
138135
// When each connection started waiting for its current request. The reaper closes connections
139136
// whose entry outlives FBIncompleteRequestTimeout; idle keep-alive connections have no entry and
140137
// are exempt. Guarded by @synchronized(self.connectionBuffers).
@@ -163,7 +160,6 @@ - (instancetype)init
163160
_connectionsAwaitingResponse = [NSMutableSet set];
164161
_standaloneWaiters = [NSMutableDictionary dictionary];
165162
_pendingSessionRequests = [NSMutableDictionary dictionary];
166-
_abandonedSessionResponses = [NSMutableDictionary dictionary];
167163
_incompleteRequestStarts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
168164
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
169165
}
@@ -739,9 +735,15 @@ - (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery
739735
- (nullable RouteResponse *)trackPendingRequest:(FBPendingRequest *)pendingRequest forSessionID:(NSString *)sessionID
740736
{
741737
@synchronized (self.pendingSessionRequests) {
742-
RouteResponse *abandonedResponse = self.abandonedSessionResponses[sessionID];
743-
if (nil != abandonedResponse) {
744-
return abandonedResponse;
738+
// Checking the current session under this lock closes the admission/teardown
739+
// race without retaining a tombstone and a full response for every past UUID.
740+
// -kill clears the active session before posting its abandonment notification.
741+
if (nil == [FBSession sessionWithIdentifier:sessionID]) {
742+
RouteResponse *response = [RouteResponse new];
743+
[FBResponseWithStatus([FBCommandStatus noSuchDriverErrorWithMessage:@"Session does not exist"
744+
traceback:nil]) dispatchWithResponse:response];
745+
[self applyDefaultHeadersToResponse:response];
746+
return response;
745747
}
746748
NSMutableSet<FBPendingRequest *> *pendingRequests = self.pendingSessionRequests[sessionID];
747749
if (nil == pendingRequests) {
@@ -776,8 +778,6 @@ - (void)abandonPendingRequestsForSessionID:(NSString *)sessionID withResponse:(R
776778
@synchronized (self.pendingSessionRequests) {
777779
pendingRequests = [self.pendingSessionRequests[sessionID] copy];
778780
[self.pendingSessionRequests removeObjectForKey:sessionID];
779-
// Recorded before the lock is dropped, so requests admitted from here on are rejected.
780-
self.abandonedSessionResponses[sessionID] = response;
781781
}
782782
for (FBPendingRequest *pendingRequest in pendingRequests) {
783783
[self writeResponse:response toClient:pendingRequest.client];

‎WebDriverAgentTests/UnitTests/FBHTTPServerSessionTests.m‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import <sys/socket.h>
1414

1515
#import "FBHTTPServer.h"
16+
#import "FBSession-Private.h"
1617

1718
static atomic_int gSessionProbeHits;
1819

@@ -23,10 +24,26 @@ @interface FBHTTPServerSessionTests : XCTestCase
2324

2425
@implementation FBHTTPServerSessionTests
2526

27+
- (void)testAbandonmentDoesNotRetainResponsesAfterPendingRequestsDrain
28+
{
29+
__weak RouteResponse *weakResponse;
30+
@autoreleasepool {
31+
RouteResponse *response = [RouteResponse new];
32+
[response respondWithString:@"deleted"];
33+
weakResponse = response;
34+
for (NSUInteger index = 0; index < 1000; index++) {
35+
[self.server abandonPendingRequestsForSessionID:[NSString stringWithFormat:@"old-%lu", (unsigned long)index]
36+
withResponse:response];
37+
}
38+
}
39+
XCTAssertNil(weakResponse);
40+
}
41+
2642
- (void)setUp
2743
{
2844
[super setUp];
2945
atomic_store(&gSessionProbeHits, 0);
46+
[FBSession initWithApplication:nil].identifier = @"live-session";
3047
self.server = [FBHTTPServer new];
3148
[self.server get:@"/session/:sessionID/probe" withBlock:^(RouteRequest *request, RouteResponse *response) {
3249
atomic_fetch_add(&gSessionProbeHits, 1);
@@ -42,6 +59,7 @@ - (void)tearDown
4259
{
4360
[self.server stop:NO];
4461
self.server = nil;
62+
[FBSession.activeSession kill];
4563
[super tearDown];
4664
}
4765

@@ -93,14 +111,14 @@ - (void)testRequestForAlreadyAbandonedSessionIsRejectedImmediately
93111

94112
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[@"GET /session/dead-session/probe HTTP/1.1\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]
95113
timeout:5.0];
96-
XCTAssertTrue([response containsString:@"session-was-deleted"], @"%@", response);
114+
XCTAssertTrue([response containsString:@"invalid session id"], @"%@", response);
97115
XCTAssertEqual(atomic_load(&gSessionProbeHits), 0, @"the route must not run for a deleted session");
98116
}
99117

100118
- (void)testAbandonedSessionIsRememberedAfterManyLaterAbandonments
101119
{
102-
// Abandoned ids are kept for the server's lifetime; evicting them would let a stale request
103-
// queue on a possibly wedged route queue again, which is the hang this rejection prevents.
120+
// A stale UUID must still be rejected after arbitrarily many other sessions,
121+
// without needing to remember every deleted UUID.
104122
RouteResponse *abandonedResponse = [RouteResponse new];
105123
[abandonedResponse respondWithString:@"session-was-deleted"];
106124
[self.server abandonPendingRequestsForSessionID:@"dead-session" withResponse:abandonedResponse];
@@ -113,7 +131,7 @@ - (void)testAbandonedSessionIsRememberedAfterManyLaterAbandonments
113131

114132
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[@"GET /session/dead-session/probe HTTP/1.1\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]
115133
timeout:5.0];
116-
XCTAssertTrue([response containsString:@"session-was-deleted"], @"%@", response);
134+
XCTAssertTrue([response containsString:@"invalid session id"], @"%@", response);
117135
XCTAssertEqual(atomic_load(&gSessionProbeHits), 0, @"the route must not run for a deleted session");
118136
}
119137

0 commit comments

Comments
 (0)