Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Record log_byte client reports (#8186)

### Fixes

- Fix EXC_BAD_ACCESS in SentryNetworkTracker caused by repeated reads of the volatile `NSURLSessionTask.currentRequest` property (#8058)

## 9.19.1

### Fixes
Expand Down
47 changes: 25 additions & 22 deletions Sources/Sentry/SentryNetworkTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask
return;
}

NSURL *url = [[sessionTask currentRequest] URL];
// Snapshot currentRequest once — the property is volatile and can return a freed
// object if the task completes on another thread between repeated accesses.
NSURLRequest *currentRequest = sessionTask.currentRequest;
Comment thread
itaybre marked this conversation as resolved.
NSURL *url = currentRequest.URL;
Comment thread
itaybre marked this conversation as resolved.

if (url == nil) {
return;
Expand Down Expand Up @@ -167,14 +170,13 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask
id<SentrySpan> _Nullable currentSpan = [SentrySDKInternal.currentHub.scope span];
if (currentSpan != nil) {
span = currentSpan;
netSpan = [span startChildWithOperation:SentrySpanOperationNetworkRequestOperation
description:[NSString stringWithFormat:@"%@ %@",
sessionTask.currentRequest.HTTPMethod,
safeUrl.sanitizedUrl]];
netSpan =
[span startChildWithOperation:SentrySpanOperationNetworkRequestOperation
description:[NSString stringWithFormat:@"%@ %@",
currentRequest.HTTPMethod, safeUrl.sanitizedUrl]];
netSpan.origin = SentryTraceOriginAutoHttpNSURLSession;

[netSpan setDataValue:sessionTask.currentRequest.HTTPMethod
forKey:@"http.request.method"];
[netSpan setDataValue:currentRequest.HTTPMethod forKey:@"http.request.method"];
[netSpan setDataValue:safeUrl.sanitizedUrl forKey:@"url"];
[netSpan setDataValue:@"fetch" forKey:@"type"];

Expand Down Expand Up @@ -257,7 +259,8 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas
return;
}

NSURL *url = [[sessionTask currentRequest] URL];
NSURLRequest *currentRequest = sessionTask.currentRequest;
NSURL *url = currentRequest.URL;

if (url == nil) {
return;
Expand Down Expand Up @@ -285,9 +288,9 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas
// - suspended → canceling (task cancelled while suspended)
if (sessionTask.state == NSURLSessionTaskStateRunning
|| sessionTask.state == NSURLSessionTaskStateSuspended) {
[self captureFailedRequests:sessionTask];
[self captureFailedRequests:sessionTask currentRequest:currentRequest];

[self addBreadcrumbForSessionTask:sessionTask];
[self addBreadcrumbForSessionTask:sessionTask currentRequest:currentRequest];

NSInteger responseStatusCode = [self urlResponseStatusCode:sessionTask.response];

Expand All @@ -307,15 +310,15 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas
}

- (void)captureFailedRequests:(NSURLSessionTask *)sessionTask
currentRequest:(NSURLRequest *)currentRequest
{
if (!self.isCaptureFailedRequestsEnabled) {
SENTRY_LOG_DEBUG(
@"captureFailedRequestsEnabled is disabled, not capturing HTTP Client errors.");
return;
}

// if request or response are null, we can't raise the event
if (sessionTask.currentRequest == nil || sessionTask.response == nil) {
if (currentRequest == nil || sessionTask.response == nil) {
SENTRY_LOG_DEBUG(@"Request or Response are null, not capturing HTTP Client errors.");
return;
}
Expand All @@ -327,7 +330,6 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask
return;
}
NSHTTPURLResponse *myResponse = (NSHTTPURLResponse *)sessionTask.response;
NSURLRequest *myRequest = sessionTask.currentRequest;
NSNumber *responseStatusCode = @(myResponse.statusCode);

if (![self containsStatusCode:myResponse.statusCode]) {
Expand All @@ -337,7 +339,7 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask
}

if (![SentryTracePropagation
isTargetMatch:SENTRY_UNWRAP_NULLABLE(NSURL, myRequest.URL)
isTargetMatch:SENTRY_UNWRAP_NULLABLE(NSURL, currentRequest.URL)
withTargets:SentrySDKInternal.options.failedRequestTargets ?: @[]]) {
SENTRY_LOG_DEBUG(
@"Request url isn't within the request targets, not capturing HTTP Client errors.");
Expand Down Expand Up @@ -372,16 +374,16 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask

SentryRequest *request = [[SentryRequest alloc] init];

UrlSanitized *url = [[UrlSanitized alloc]
initWithURL:SENTRY_UNWRAP_NULLABLE(NSURL, [[sessionTask currentRequest] URL])];
UrlSanitized *url =
[[UrlSanitized alloc] initWithURL:SENTRY_UNWRAP_NULLABLE(NSURL, currentRequest.URL)];

request.url = url.sanitizedUrl;
request.method = myRequest.HTTPMethod;
request.method = currentRequest.HTTPMethod;
request.fragment = url.fragment;
request.queryString = url.query;
request.bodySize = [NSNumber numberWithLongLong:sessionTask.countOfBytesSent];
if (nil != myRequest.allHTTPHeaderFields) {
NSDictionary<NSString *, NSString *> *headers = myRequest.allHTTPHeaderFields.copy;
if (nil != currentRequest.allHTTPHeaderFields) {
NSDictionary<NSString *, NSString *> *headers = currentRequest.allHTTPHeaderFields.copy;
request.headers = [HTTPHeaderSanitizer sanitizeHeaders:headers];
}

Expand Down Expand Up @@ -428,6 +430,7 @@ - (BOOL)containsStatusCode:(NSInteger)statusCode
}

- (void)addBreadcrumbForSessionTask:(NSURLSessionTask *)sessionTask
currentRequest:(NSURLRequest *)currentRequest
{
if (!self.isNetworkBreadcrumbEnabled) {
return;
Expand All @@ -449,13 +452,13 @@ - (void)addBreadcrumbForSessionTask:(NSURLSessionTask *)sessionTask
SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] initWithLevel:breadcrumbLevel
category:@"http"];

UrlSanitized *urlComponents = [[UrlSanitized alloc]
initWithURL:SENTRY_UNWRAP_NULLABLE(NSURL, sessionTask.currentRequest.URL)];
UrlSanitized *urlComponents =
[[UrlSanitized alloc] initWithURL:SENTRY_UNWRAP_NULLABLE(NSURL, currentRequest.URL)];

breadcrumb.type = @"http";
NSMutableDictionary<NSString *, id> *breadcrumbData = [[NSMutableDictionary alloc] init];
breadcrumbData[@"url"] = urlComponents.sanitizedUrl;
breadcrumbData[@"method"] = sessionTask.currentRequest.HTTPMethod;
breadcrumbData[@"method"] = currentRequest.HTTPMethod;
breadcrumbData[@"request_start"] = requestStart;
breadcrumbData[@"request_body_size"] =
[NSNumber numberWithLongLong:sessionTask.countOfBytesSent];
Expand Down
37 changes: 21 additions & 16 deletions Sources/Sentry/SentryTimeToDisplayTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,32 +89,37 @@ - (BOOL)startForTracer:(SentryTracer *)tracer
[tracer setFinishCallback:^(SentryTracer *_tracer) {
[SentryDependencyContainer.sharedInstance.framesTracker removeListener:self];

// The tracer finishes when the screen is fully displayed. Therefore, we must also finish
// the TTID span.
if (self.initialDisplaySpan.isFinished == NO) {
[self.initialDisplaySpan finish];
// Strongify the weak span references so they can't become dangling
// mid-callback if the tracer's children are released concurrently.
id<SentrySpan> initialSpan = self.initialDisplaySpan;
id<SentrySpan> fullSpan = self.fullDisplaySpan;

if (initialSpan != nil && initialSpan.isFinished == NO) {
[initialSpan finish];
}

// If the start time of the tracer changes, which is the case for app start transactions, we
// also need to adapt the start time of our spans.
self.initialDisplaySpan.startTimestamp = _tracer.startTimestamp;
[self addTimeToDisplayMeasurement:self.initialDisplaySpan name:@"time_to_initial_display"];
if (initialSpan != nil) {
initialSpan.startTimestamp = _tracer.startTimestamp;
[self addTimeToDisplayMeasurement:initialSpan name:@"time_to_initial_display"];
}

if (self.fullDisplaySpan == nil) {
if (fullSpan == nil) {
return;
}

self.fullDisplaySpan.startTimestamp = _tracer.startTimestamp;
[self addTimeToDisplayMeasurement:self.fullDisplaySpan name:@"time_to_full_display"];
fullSpan.startTimestamp = _tracer.startTimestamp;
[self addTimeToDisplayMeasurement:fullSpan name:@"time_to_full_display"];

if (self.fullDisplaySpan.status != kSentrySpanStatusDeadlineExceeded) {
if (fullSpan.status != kSentrySpanStatusDeadlineExceeded) {
return;
}

self.fullDisplaySpan.timestamp = self.initialDisplaySpan.timestamp;
self.fullDisplaySpan.spanDescription = [NSString
stringWithFormat:@"%@ - Deadline Exceeded", self.fullDisplaySpan.spanDescription];
[self addTimeToDisplayMeasurement:self.fullDisplaySpan name:@"time_to_full_display"];
if (initialSpan != nil) {
fullSpan.timestamp = initialSpan.timestamp;
}
fullSpan.spanDescription =
[NSString stringWithFormat:@"%@ - Deadline Exceeded", fullSpan.spanDescription];
[self addTimeToDisplayMeasurement:fullSpan name:@"time_to_full_display"];
}];

return YES;
Expand Down
43 changes: 17 additions & 26 deletions Sources/Sentry/SentryTracePropagation.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,40 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage
tracePropagationTargets:(NSArray *_Nullable)tracePropagationTargets
toRequest:(NSURLSessionTask *)sessionTask
{
if (![SentryTracePropagation sessionTaskRequiresPropagation:sessionTask
tracePropagationTargets:tracePropagationTargets]) {
SENTRY_LOG_DEBUG(@"Not adding trace_id and baggage headers for %@",
sessionTask.currentRequest.URL.absoluteString);
// Snapshot currentRequest once — the property is volatile and can become a zombie
// between repeated accesses if the task completes on another thread.
NSURLRequest *request = sessionTask.currentRequest;
Comment thread
itaybre marked this conversation as resolved.
if (request == nil) {
return;
}

if (![SentryTracePropagation isTargetMatch:SENTRY_UNWRAP_NULLABLE(NSURL, request.URL)
withTargets:tracePropagationTargets ?: @[]]) {
SENTRY_LOG_DEBUG(
@"Not adding trace_id and baggage headers for %@", request.URL.absoluteString);
Comment thread
itaybre marked this conversation as resolved.
return;
}
NSString *baggageHeader = @"";

if (baggage != nil) {
NSString *_Nullable rawHeader = SENTRY_UNWRAP_NULLABLE(
NSString, sessionTask.currentRequest.allHTTPHeaderFields[SENTRY_BAGGAGE_HEADER]);
NSString *_Nullable rawHeader
= SENTRY_UNWRAP_NULLABLE(NSString, request.allHTTPHeaderFields[SENTRY_BAGGAGE_HEADER]);
NSDictionary *originalBaggage = [SentryBaggageSerialization decode:rawHeader ?: @""];
if (originalBaggage[@"sentry-trace_id"] == nil) {
baggageHeader = [baggage toHTTPHeaderWithOriginalBaggage:originalBaggage];
}
}

// First we check if the current request is mutable, so we could easily add a new
// header. Otherwise we try to change the current request for a new one with the extra
// header.
if ([sessionTask.currentRequest isKindOfClass:[NSMutableURLRequest class]]) {
NSMutableURLRequest *currentRequest = (NSMutableURLRequest *)sessionTask.currentRequest;
[SentryTracePropagation addHeaderFieldsToRequest:currentRequest
if ([request isKindOfClass:[NSMutableURLRequest class]]) {
NSMutableURLRequest *mutableRequest = (NSMutableURLRequest *)request;
[SentryTracePropagation addHeaderFieldsToRequest:mutableRequest
traceHeader:traceHeader
baggageHeader:baggageHeader
propagateTraceparent:propagateTraceparent];
} else {
// Even though NSURLSessionTask doesn't have 'setCurrentRequest', some subclasses
// do. For those subclasses we replace the currentRequest with a mutable one with
// the additional trace header. Since NSURLSessionTask is a public class and can be
// overridden, we believe this is not considered a private api.
SEL setCurrentRequestSelector = NSSelectorFromString(@"setCurrentRequest:");
if ([sessionTask respondsToSelector:setCurrentRequestSelector]) {
NSMutableURLRequest *newRequest = [sessionTask.currentRequest mutableCopy];
NSMutableURLRequest *newRequest = [request mutableCopy];
[SentryTracePropagation addHeaderFieldsToRequest:newRequest
traceHeader:traceHeader
baggageHeader:baggageHeader
Expand All @@ -61,15 +61,6 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage
}
}

+ (BOOL)sessionTaskRequiresPropagation:(NSURLSessionTask *)sessionTask
tracePropagationTargets:(NSArray *)tracePropagationTargets
{
return sessionTask.currentRequest != nil &&
[SentryTracePropagation
isTargetMatch:SENTRY_UNWRAP_NULLABLE(NSURL, sessionTask.currentRequest.URL)
withTargets:tracePropagationTargets];
}

+ (void)addHeaderFieldsToRequest:(NSMutableURLRequest *)request
traceHeader:(SentryTraceHeader *)traceHeader
baggageHeader:(NSString *)baggageHeader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,23 @@ class SentryNetworkTrackerTests: XCTestCase {
XCTAssertEqual(SentryNetworkTrackerTests.origin, span.origin)
}

func testSpanData_VolatileCurrentRequest_UsesSnapshot() throws {
var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl)
request.httpMethod = "GET"
let task = VolatileRequestTaskMock(request: request)
task.currentRequestAccessLimit = 1

let sut = fixture.getSut()
let transaction = startTransaction()
sut.urlSessionTaskResume(task)

let spans = Dynamic(transaction).children as [Span]?
let span = try XCTUnwrap(spans?.first)

XCTAssertEqual(span.spanDescription, "GET \(SentryNetworkTrackerTests.testUrl)")
XCTAssertEqual(span.data["http.request.method"] as? String, "GET")
}

func testStatusForTaskRunning() {
let sut = fixture.getSut()
let task = createDataTask()
Expand Down Expand Up @@ -1486,4 +1503,46 @@ class SentryNetworkTrackerTests: XCTestCase {
request.httpMethod = method
return URLSessionStreamTaskMock(request: request)
}

// MARK: - Concurrent resume + setState race (issue #8012)

func testResumeConcurrentWithSetState_DoesNotCrash() {
let sut = fixture.getSut()

let queue = DispatchQueue(label: "resume-setState-race", qos: .userInteractive, attributes: [.concurrent, .initiallyInactive])
let iterations = 500
let expectation = XCTestExpectation(description: "Concurrent resume and setState")
expectation.expectedFulfillmentCount = iterations * 2
expectation.assertForOverFulfill = true

for _ in 0..<iterations {
let task = createDataTask()
_ = startTransaction()

queue.async {
sut.urlSessionTaskResume(task)
expectation.fulfill()
}
queue.async {
task.state = .completed
sut.urlSessionTask(task, setState: .completed)
expectation.fulfill()
}
}

queue.activate()
wait(for: [expectation], timeout: 10)
}

func testResumeAfterTaskCompleted_DoesNotCrash() {
let sut = fixture.getSut()
let transaction = startTransaction()
let task = createDataTask()

task.state = .completed
sut.urlSessionTaskResume(task)

let spans = Dynamic(transaction).children as [Span]?
XCTAssertEqual(spans?.count ?? 0, 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ final class SentryTracePropagationTests: XCTestCase {
XCTAssertTrue(SentryTracePropagation.isTargetMatch(localhostURL, withTargets: targetsWithInvalidType))
}

// MARK: - currentRequest snapshot safety (issue #8012)

func testAddBaggageHeader_TaskWithNoCurrentRequest_DoesNotCrash() throws {
let emptyBaggage = Baggage()
let traceHeader = TraceHeader(
trace: SentryId(),
spanId: SpanId(),
sampled: .yes
)

let task = URLSessionDataTaskMock()

SentryTracePropagation.addBaggageHeader(
emptyBaggage,
traceHeader: traceHeader,
propagateTraceparent: true,
tracePropagationTargets: [try XCTUnwrap(NSRegularExpression(pattern: ".*"))],
toRequest: task
)

XCTAssertNil(task.currentRequest?.value(forHTTPHeaderField: "sentry-trace"))
}

private func createSessionTask(method: String = "GET") throws -> URLSessionDownloadTaskMock {
let url = try XCTUnwrap(URL(string: "https://www.domain.com/api?query=value&query2=value2#fragment"))
var request = URLRequest(url: url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,15 @@ static int64_t const DATA_BYTES_SENT = 652;

@end

/**
* A mock that returns nil from currentRequest after a configurable number of accesses.
* Simulates the real-world race where NSURLSessionTask.currentRequest becomes a dangling
* pointer (or nil) when the task completes on another thread between repeated reads.
*/
@interface VolatileRequestTaskMock : URLSessionDataTaskMock

@property (nonatomic) NSUInteger currentRequestAccessLimit;

@end

NS_ASSUME_NONNULL_END
Loading
Loading