New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix crashes where setHeartbeatDate:forTag:
reads immutable dictionary.
#37
Conversation
Coverage ReportAffected SDKs
Test Logs
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM in general, but I have a suggestion, let's consider it first.
} | ||
|
||
- (BOOL)writeDictionary:(NSDictionary *)dictionary | ||
- (BOOL)writeDictionary:(NSMutableDictionary *)dictionary | ||
forWritingURL:(NSURL *)writingFileURL | ||
error:(NSError **)outError { | ||
NSData *data = [GULSecureCoding archivedDataWithRootObject:dictionary error:outError]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we change only writeDictionary:...
method to archive the mutable dictionary (a comment that we do that for backward compatibility and reminder to eventually delete it should be added as well). Then the rest of the GULHeartbeatDateStorage.m
code can remain unchanged and also we will have a good migration story because the current version of the code (7.4.0) can work with both mutable and immutable version of the file.
It should be a better option unless I'm missing something. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So with your proposal, the only change to the library would be line 151 changed to:
NSData *data = [GULSecureCoding archivedDataWithRootObject:dictionary.mutableCopy error:outError];
SGTM
// This implies the storage directory & file were created, written to, and read from. | ||
XCTAssertEqualObjects(retrievedDate, date); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably add a test to check if the archive contains the mutable dictionary after the heartbeat has been updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make sure we don't end up with a similar crash in the future we should add type validations to heartbeatDictionaryWithFileURL:
and heartbeatDateForTag:
methods and make sure we don't crash on unexpected file content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change the version in the podspec to 7.4.1
@@ -169,6 +169,65 @@ - (void)testHeartbeatDateForTagWhenHeartbeatFileReturnsInvalidData { | |||
XCTAssertNil(retrievedDate); | |||
} | |||
|
|||
- (void)testHeartbeatDateForTagWhenHeartbeatFileContainsUnexpectedContent { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make sure we don't end up with a similar crash in the future we should add type validations to heartbeatDictionaryWithFileURL: and heartbeatDateForTag: methods and make sure we don't crash on unexpected file content.
This test and the following two tests show how the hb APIs recover when the file contains unexpected content or invalid data. Are these sufficient for the cases you are describing above? @maksymmalyhin
NSString *tag = @"tag"; | ||
|
||
// 4. Retrieve heartbeat info that is not stored and assert the retrieved info is nil. | ||
NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... I would expect this to crash on heartbeatDictionary[tag];. But with this particular test data, I think, it doesn't because of the secure coding validation.
Let't add another invalid data set where we have e.g. a dictionary of dictionaries with dates. Handling this case will most likely require code changes.
#pragma mark - GoogleUtilities Issue #36 | ||
|
||
- (void)testFix_pre7dot4_CompabitibilityIssue { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this test to validate current fix. This test passes. If the implementation is modified with a one line edit to write an immutable dictionary to disk (effectively, reverting this PR), then this test fails with error from #36
// Developer downgrades from 7.4.0 | ||
|
||
// Set heartbeat from pre-7.4.0 API. Note: The relevant logic from pre-7.4.0 APIs is used below. | ||
NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of having a full copies of GULHeartbeatDateStorage.[hm]
files like GULHeartbeatDateStorageV7.3.1
and GULHeartbeatDateStorageV7.4.0
, so instances of them can be created in the same test to do operations and check mutual compatibility. It seems to be the easiest and the most readable way to implement the migration tests.
@@ -18,6 +18,10 @@ | |||
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h" | |||
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h" | |||
|
|||
// Import specific version implementations for compatibility testing. | |||
#import "Sources/GULHeartbeatDateStorageV7.3.1.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imports should be repo-relative - prepend with "GoogleUtilities"
* limitations under the License. | ||
*/ | ||
|
||
#import "GULHeartbeatDateStorageV7.4.0.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make repo-relative
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM on CI green, thank you for the fix and the tests!
Optionally we may add a couple more migration test cases e.g. test compatibility for the following: 7.3.1->7.4.0->7.4.1->7.3.1.
#import <Foundation/Foundation.h> | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional: I would add a comment in this and other similar files that this is a copy of the specific file version required for compatibility tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll do this!
This reverts commit 44110e6.
Fix for planned GoogleUtils 7.4.1 patch release.
NSDictionary
-related type changes introduced in #19 caused crashes for app versions that used an older GoogleUtilities version on top of an app version that's using GoogleUtilities version 7.4.0.The crash occurred in
setHeartbeatDate:forTag:
(for apps with GULs pre-7.4) when the API internally expected a mutable dictionary but received an immutable dictionary (introduced in 7.4.0) from persistent storage.See a thorough explanation here.
Fixes #36
Fixes #8047
Some interesting things to note when it comes to archiving/unarchiving `NSDictionary`s
tldr; keep track of what type you are archiving and unarchiving because the underlying types can make things tricky