Skip to content
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

Merged
merged 25 commits into from May 11, 2021

Conversation

ncooke3
Copy link
Member

@ncooke3 ncooke3 commented May 7, 2021

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.

Key fix: Always write a mutable heartbeat dictionary to disk.

Since a mutable heartbeat dictionary is now stored, updated helper dictionary getter method to always
return mutable heartbeat dictionary.

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
NSData *data = // archive a `NSDictionary` instance

// Then, unarchive into an `NSMutableDictionary`
NSMutableDictionary *mutableDict = // unarchive `data`
// `mutableDict`'s underlying type is a `__NSSingleEntryDictionaryI` where 
// the trailing `I` means it's *immutable*
mutableDict[tag] = [NSDate date]; // 🔥 Crash because `mutableDict` is actually *immutable*

// ------------------------------------- //

NSData *data = // archive a `NSMutableDictionary` instance

// Then, unarchive into an `NSDictionary`
NSDictionary *immutableDict = // unarchive `data`
// `immutableDict`'s underlying type is a `__NSDictionaryM` where 
// the trailing `M` means it's *mutable*
immutableDict[tag] = [NSDate date]; // ✔ This is ok because `immutableDict` is actually *mutable*.

@google-oss-bot
Copy link

google-oss-bot commented May 7, 2021

Coverage Report

Affected SDKs

  • GoogleUtilities-ios-unit-GoogleUtilities.framework

    SDK overall coverage changed from 0.28% (66f5945) to 0.28% (b4733d2) by -0.00%.

    Filename Base (66f5945) Head (b4733d2) Diff

Test Logs

Copy link
Contributor

@maksymmalyhin maksymmalyhin left a 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];
Copy link
Contributor

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?

Copy link
Member

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);
}

Copy link
Contributor

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.

Copy link
Contributor

@maksymmalyhin maksymmalyhin left a 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.

Copy link
Member

@paulb777 paulb777 left a 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 {
Copy link
Member Author

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];
Copy link
Contributor

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.

Comment on lines 360 to 362
#pragma mark - GoogleUtilities Issue #36

- (void)testFix_pre7dot4_CompabitibilityIssue {
Copy link
Member Author

@ncooke3 ncooke3 May 10, 2021

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];
Copy link
Contributor

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"
Copy link
Member

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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make repo-relative

Copy link
Contributor

@maksymmalyhin maksymmalyhin left a 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

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do this!

@maksymmalyhin maksymmalyhin merged commit 5835941 into main May 11, 2021
@maksymmalyhin maksymmalyhin deleted the nc/fix-hb-bug branch May 11, 2021 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants