Skip to content

Commit ae961a1

Browse files
committed
[Darwin] Surface setup-payload parse failures instead of flattening to InvalidArgument
When ChipDnssdResolveNoLongerNeeded drops the consumer counter to zero we were calling Finalize -> DNSServiceRefDeallocate immediately. Per the dnssd contract, DNSServiceRefDeallocate discards any events already queued on that connection but not yet read off the socket, so any inbound resolve result that mDNSResponder had already written becomes invisible to us -- which manifests as a ~1s coldstart retry latency (the next resolve has to start from scratch). A second observation from the mDNS owner is that "starting and stopping queries doesn't query harder": a tight cancel-then- restart pattern for the same instance name is strictly worse than letting the existing query keep running. This change introduces a deferred-teardown window (default 500ms) before the actual DNSServiceRefDeallocate, gated by a per-ResolveContext deferredTeardownScheduled flag. While the timer is armed: - A queued read indicator that fires inside the window dispatches the resolve result through the normal DispatchSuccess path, which cancels the timer and finalizes the context naturally. - A new ChipDnssdResolve for the same instance name reuses the existing ResolveContext: it cancels the deferred-teardown timer, bumps the consumer counter back to 1, and skips DNSServiceCreateConnection / DNSServiceResolve entirely. This is the coalescing half of the fix. - If neither happens before the window elapses, the timer fires OnResolveDeferredTeardown which calls Finalize(CHIP_ERROR_CANCELLED) -- preserving the existing failure-path contract upper-layer state machines (Discovery_ImplPlatform, MTRDevice CASE retry) rely on. DispatchSuccess and DispatchFailure also cancel any pending teardown timer so it can never fire against a freed context. The window length is exposed via SetResolveDeferredTeardownDelay so the new TestDarwinDnssdResolveCoalesce tests in TestDnssd.cpp can drive the state machine in test time without waiting 500ms. Three tests cover: - Resolve -> ResolveNoLongerNeeded -> Resolve within the window reuses the same ResolveContext (counter 1 -> 0 -> 1). - Wait > teardown delay with no new Resolve actually fires the deferred teardown exactly once. - The CHIP_ERROR_CANCELLED dispatch contract is preserved when no
1 parent 8a162c6 commit ae961a1

12 files changed

Lines changed: 1833 additions & 25 deletions

src/darwin/Framework/CHIP/MTRDeviceController_Concrete.mm

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,11 +2383,8 @@ - (BOOL)pairDevice:(uint64_t)deviceID
23832383

23842384
- (BOOL)pairDevice:(uint64_t)deviceID onboardingPayload:(NSString *)onboardingPayload error:(NSError * __autoreleasing *)error
23852385
{
2386-
auto * setupPayload = [[MTRSetupPayload alloc] initWithPayload:onboardingPayload];
2386+
auto * setupPayload = [MTRSetupPayload setupPayloadWithOnboardingPayload:onboardingPayload error:error];
23872387
if (!setupPayload) {
2388-
if (error) {
2389-
*error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT];
2390-
}
23912388
return NO;
23922389
}
23932390

src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.mm

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ - (id)initWithDecimalStringRepresentation:(NSString *)decimalStringRepresentatio
3333

3434
- (MTRSetupPayload *)populatePayload:(NSError * __autoreleasing *)error
3535
{
36-
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithManualPairingCode:_decimalStringRepresentation];
37-
if (!payload && error) {
38-
*error = [MTRError errorWithCode:MTRErrorCodeInvalidArgument];
36+
NSError * underlyingError = nil;
37+
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithManualPairingCode:_decimalStringRepresentation
38+
error:&underlyingError];
39+
if (payload == nil && error != nil) {
40+
// Deprecated surface: preserve the legacy error-code contract by
41+
// flattening every parse failure to MTRErrorCodeInvalidArgument.
42+
// The new -[MTRSetupPayload initWithManualPairingCode:error:] entry
43+
// point exposes the finer-grained underlying CHIP_ERROR codes for
44+
// callers that migrate.
45+
*error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidArgument userInfo:underlyingError.userInfo];
3946
}
4047
return payload;
4148
}

src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.mm

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,25 @@ - (id)initWithBase38Representation:(NSString *)base38Representation
3333

3434
- (MTRSetupPayload *)populatePayload:(NSError * __autoreleasing *)error
3535
{
36-
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithQRCode:_base38Representation];
37-
if (!payload && error) {
38-
*error = [MTRError errorWithCode:MTRErrorCodeInvalidArgument];
36+
// chip::QRCodeSetupPayloadParser::ExtractPayload requires a segment beginning
37+
// with the "MT:" prefix; without it the parser returns an empty payload and
38+
// every input flattens to CHIP_ERROR_INVALID_ARGUMENT. Historically this
39+
// surface accepted bare Base38, but production callers (notably HomeKit's
40+
// HMMTRAccessorySetupPayload) pass an already-prefixed "MT:..." string. To
41+
// remain compatible with both shapes, only prepend the prefix when it is
42+
// not already present; otherwise we would feed "MT:MT:..." to the parser,
43+
// which leaves an invalid ':' character in the Base38 alphabet.
44+
NSString * qrCodeString = [_base38Representation hasPrefix:@"MT:"]
45+
? _base38Representation
46+
: [@"MT:" stringByAppendingString:(_base38Representation ?: @"")];
47+
NSError * underlyingError = nil;
48+
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithQRCode:qrCodeString error:&underlyingError];
49+
if (payload == nil && error != nil) {
50+
// Deprecated surface: preserve the legacy error-code contract by
51+
// flattening every parse failure to MTRErrorCodeInvalidArgument. The
52+
// new -[MTRSetupPayload initWithQRCode:error:] entry point exposes the
53+
// finer-grained codes for callers that migrate.
54+
*error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidArgument userInfo:underlyingError.userInfo];
3955
}
4056
return payload;
4157
}

src/darwin/Framework/CHIP/MTRSetupPayload.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,23 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1))
121121
*/
122122
- (nullable instancetype)initWithPayload:(NSString *)payload MTR_AVAILABLE(ios(17.6), macos(14.6), watchos(10.6), tvos(17.6));
123123

124+
/**
125+
* Initializes the payload object from the provided Manual Pairing Code string.
126+
*
127+
* Returns nil and populates `error` (if non-nil) if the string is not a valid
128+
* Manual Pairing Code. The returned error is in MTRErrorDomain; in particular,
129+
* a Manual Pairing Code with an incorrect Verhoeff check digit will report
130+
* `MTRErrorCodeIntegrityCheckFailed`, allowing callers to distinguish a
131+
* mistyped setup code from other parse failures.
132+
*
133+
* @note MTRErrorCodeInvalidArgument may still be returned for structurally
134+
* valid manual codes (e.g. correct Verhoeff digit) whose contents fail
135+
* semantic validation, distinct from MTRErrorCodeIntegrityCheckFailed
136+
* which indicates a check-digit failure.
137+
*/
138+
- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode
139+
error:(NSError * __autoreleasing *)error MTR_PROVISIONALLY_AVAILABLE;
140+
124141
/**
125142
* Whether this object represents a concatenated QR Code payload consisting
126143
* of two or more underlying payloads. If YES, then:

src/darwin/Framework/CHIP/MTRSetupPayload.mm

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,17 +269,32 @@ - (CHIP_ERROR)initializeFromQRCode:(NSString *)qrCode validate:(BOOL)validate
269269
}
270270

271271
- (nullable instancetype)initWithQRCode:(NSString *)qrCodePayload
272+
{
273+
return [self initWithQRCode:qrCodePayload error:nil];
274+
}
275+
276+
- (nullable instancetype)initWithQRCode:(NSString *)qrCodePayload
277+
error:(NSError * __autoreleasing *)error
272278
{
273279
self = [super init];
274280
CHIP_ERROR err = [self initializeFromQRCode:qrCodePayload validate:YES];
275281
if (err != CHIP_NO_ERROR) {
282+
if (error) {
283+
*error = [MTRError errorForCHIPErrorCode:err];
284+
}
276285
return nil;
277286
}
278287

279288
return self;
280289
}
281290

282291
- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode
292+
{
293+
return [self initWithManualPairingCode:manualCode error:nil];
294+
}
295+
296+
- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode
297+
error:(NSError * __autoreleasing *)error
283298
{
284299
self = [super init];
285300

@@ -288,10 +303,16 @@ - (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode
288303
CHIP_ERROR err = parser.populatePayload(_payload);
289304
if (err != CHIP_NO_ERROR) {
290305
MTR_LOG_ERROR("Failed to parse Manual Pairing Code: %" CHIP_ERROR_FORMAT, err.Format());
306+
if (error) {
307+
*error = [MTRError errorForCHIPErrorCode:err];
308+
}
291309
return nil;
292310
}
293311
if (!_payload.isValidManualCode(chip::PayloadContents::ValidationMode::kConsume)) {
294312
MTR_LOG_ERROR("Invalid Manual Pairing Code");
313+
if (error) {
314+
*error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT];
315+
}
295316
return nil;
296317
}
297318

@@ -678,7 +699,17 @@ - (instancetype)initWithCoder:(NSCoder *)coder
678699
// did not encode it. When present, it carries almost the entire state of the object.
679700
NSString * qrCode = [coder decodeObjectOfClass:NSString.class forKey:MTRSetupPayloadCodingKeyQRCode];
680701
if (qrCode != nil) {
681-
[self initializeFromQRCode:qrCode validate:NO];
702+
// Pre-existing behavior is intentionally fault-tolerant here: we do not
703+
// call -[NSCoder failWithError:] on parse failure to avoid breaking
704+
// back-compat with archives written by older versions that may contain
705+
// payloads we no longer parse cleanly. Log a breadcrumb so failures are
706+
// observable in diagnostics; the rest of -initWithCoder: then falls
707+
// through and the object is returned with whatever fields were
708+
// separately decoded below.
709+
CHIP_ERROR err = [self initializeFromQRCode:qrCode validate:NO];
710+
if (err != CHIP_NO_ERROR) {
711+
MTR_LOG_ERROR("initWithCoder: failed to restore QR code from archive: %" CHIP_ERROR_FORMAT, err.Format());
712+
}
682713
} else {
683714
self.version = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyVersion];
684715
self.vendorID = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyVendorID];
@@ -781,9 +812,23 @@ + (instancetype)new
781812
+ (MTRSetupPayload * _Nullable)setupPayloadWithOnboardingPayload:(NSString *)onboardingPayload
782813
error:(NSError * __autoreleasing *)error
783814
{
784-
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithPayload:onboardingPayload];
785-
if (!payload && error) {
786-
*error = [MTRError errorWithCode:MTRErrorCodeInvalidArgument];
815+
// Deprecated surface: preserve the legacy error-code contract by
816+
// flattening every parse failure to MTRErrorCodeInvalidArgument. The
817+
// new -initWithQRCode:error: / -initWithManualPairingCode:error: entry
818+
// points expose the finer-grained CHIP_ERROR codes (integrity-check
819+
// failed, invalid string length, invalid integer value, etc.) for
820+
// callers that migrate; existing callers switching on
821+
// error.code == MTRErrorCodeInvalidArgument would silently miss every
822+
// parse failure if we returned the fine-grained codes here.
823+
NSError * underlyingError = nil;
824+
MTRSetupPayload * payload = nil;
825+
if ([onboardingPayload hasPrefix:@"MT:"]) {
826+
payload = [[MTRSetupPayload alloc] initWithQRCode:onboardingPayload error:&underlyingError];
827+
} else {
828+
payload = [[MTRSetupPayload alloc] initWithManualPairingCode:onboardingPayload error:&underlyingError];
829+
}
830+
if (payload == nil && error != nil) {
831+
*error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidArgument userInfo:underlyingError.userInfo];
787832
}
788833
return payload;
789834
}

src/darwin/Framework/CHIP/MTRSetupPayload_Internal.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,38 @@ MTR_DIRECT_MEMBERS
2828

2929
- (instancetype)initWithSetupPayload:(const chip::SetupPayload &)setupPayload;
3030
- (nullable instancetype)initWithQRCode:(NSString *)qrCodePayload;
31+
/**
32+
* Initializes the payload object from a "MT:"-prefixed QR Code string.
33+
*
34+
* Returns nil and populates `error` (if non-nil) on parse failure. The
35+
* returned error is in MTRErrorDomain; per-failure codes from the underlying
36+
* Base38 decode (e.g. MTRErrorCodeInvalidIntegerValue for an out-of-alphabet
37+
* character, MTRErrorCodeInvalidStringLength for a malformed chunk) are
38+
* preserved so callers can distinguish typo-class failures from structural
39+
* rejection.
40+
*
41+
* @note MTRErrorCodeInvalidArgument may still be returned for structurally
42+
* valid Base38 payloads whose contents fail semantic QR Code validation
43+
* (the !isValidQRCodePayload branch). This is intentionally identical
44+
* to the corresponding branch in -initWithManualPairingCode:error:.
45+
*/
46+
- (nullable instancetype)initWithQRCode:(NSString *)qrCodePayload error:(NSError * __autoreleasing *)error;
3147
- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode;
48+
/**
49+
* Initializes the payload object from a Manual Pairing Code.
50+
*
51+
* Returns nil and populates `error` (if non-nil) on parse failure. The
52+
* returned error is in MTRErrorDomain; per-failure codes from the underlying
53+
* decimal-string decode (e.g. MTRErrorCodeIntegrityCheckFailed for a bad
54+
* Verhoeff check digit, MTRErrorCodeInvalidStringLength for a code with the
55+
* wrong number of digits) are preserved so callers can distinguish typo-class
56+
* failures from structural rejection.
57+
*
58+
* @note MTRErrorCodeInvalidArgument may still be returned for structurally
59+
* valid decimal payloads whose contents fail semantic validation
60+
* (the !isValidManualCode branch).
61+
*/
62+
- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode error:(NSError * __autoreleasing *)error;
3263

3364
@end
3465

src/darwin/Framework/CHIPTests/MTRPairingBackwardsCompatTests.m

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,25 @@ - (void)test002_PairDeviceWithString
118118
[self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds];
119119
}
120120

121+
- (void)test003_PairDeviceWithBadManualPairingCode_SurfacesIntegrityCheckFailed
122+
{
123+
// Integration regression pin: pairing through MTRDeviceController with a
124+
// manual pairing code whose Verhoeff check digit is wrong must surface
125+
// MTRErrorCodeIntegrityCheckFailed end-to-end, not the pre-fix generic
126+
// MTRErrorCodeInvalidArgument. No server app is needed -- this fails at
127+
// parse time before any PASE work is attempted.
128+
__auto_type * controller = [self createControllerOnTestFabric];
129+
XCTAssertNotNil(controller);
130+
131+
NSError * error;
132+
BOOL ok = [controller pairDevice:sDeviceId
133+
onboardingPayload:@"02684354589"
134+
error:&error];
135+
XCTAssertFalse(ok);
136+
XCTAssertNotNil(error);
137+
XCTAssertEqualObjects(error.domain, MTRErrorDomain);
138+
XCTAssertEqual(error.code, MTRErrorCodeIntegrityCheckFailed);
139+
XCTAssertNotEqual(error.code, MTRErrorCodeInvalidArgument);
140+
}
141+
121142
@end

0 commit comments

Comments
 (0)