Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion docs/firebase-runtime-failure-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This backlog tracks binding drifts that are concrete candidates for the runtime-

| Case id | Target/member | Audit finding reference | Expected runtime failure mechanism | Proof status | Fix PR | Merged commit |
| --- | --- | --- | --- | --- | --- | --- |
| `abtesting-activateexperiment` | `Firebase.ABTesting.ExperimentController.ActivateExperiment` | `output/firebase-binding-audit/report.json` -> `ABTesting` -> `ActivateExperiment` (`signature-drift`) | The old binding exports `activateExperiment:forServiceOrigin:` as `NSObject` instead of `ABTExperimentPayload`, so callers can send an arbitrary object and native ABTesting immediately raises `ObjCRuntime.ObjCException` when it invokes `ABTExperimentPayload` selectors on that object. | Proved locally on the unfixed binding. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-activateexperiment-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-activateexperiment-success.json`. | - | - |
| `abtesting-validaterunningexperiments` | `Firebase.ABTesting.ExperimentController.ValidateRunningExperiments` | `output/firebase-binding-audit/report.json` -> `ABTesting` -> `ValidateRunningExperiments` (`signature-drift`) | The old binding exports `validateRunningExperimentsForServiceOrigin:runningExperimentPayloads:` as `NSObject[]` instead of `ABTExperimentPayload[]`, so callers can send arbitrary objects and native ABTesting immediately raises `ObjCRuntime.ObjCException` when it invokes `ABTExperimentPayload` selectors on those objects. | Proved locally on the unfixed binding. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-validaterunningexperiments-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-validaterunningexperiments-success.json`. | - | - |

## Investigating

Expand All @@ -16,5 +16,6 @@ No additional runtime-failure candidates are currently promoted.

| Case id | Target/member | Audit finding reference | Runtime failure mechanism | Proof status | Fix PR | Merged commit |
| --- | --- | --- | --- | --- | --- | --- |
| `abtesting-activateexperiment` | `Firebase.ABTesting.ExperimentController.ActivateExperiment` | `output/firebase-binding-audit/report.json` -> `ABTesting` -> `ActivateExperiment` (`signature-drift`) | The old binding exported `activateExperiment:forServiceOrigin:` as `NSObject` instead of `ABTExperimentPayload`, so callers could send an arbitrary object and native ABTesting immediately raised `ObjCRuntime.ObjCException` when it invoked `ABTExperimentPayload` selectors on that object. | Proved and fixed. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-activateexperiment-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-activateexperiment-success.json`. | [#114](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/114) | `f77b1c57eff15cf6c1b5ca0df47d8195b46f0f40` |
| `cloudfunctions-usefunctionsemulatororigin` | `Firebase.CloudFunctions.CloudFunctions.UseFunctionsEmulatorOrigin` | Manual runtime candidate from the current binding surface and native `FirebaseFunctions` Swift header | The binding exported `useFunctionsEmulatorOrigin:` even though the current framework only exposes `useEmulatorWithHost:port:`. Calling the stale selector raised `ObjCRuntime.ObjCException` with an unrecognized-selector native exception. | Proved and fixed. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfunctions-usefunctionsemulatororigin-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfunctions-usefunctionsemulatororigin-success.json`. | [#113](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/113) | `b6a6c82c74e0cf3645edb9dc5519d4a628f7ed36` |
| `cloudfirestore-getquerynamed` | `Firebase.CloudFirestore.Firestore.GetQueryNamed` | `output/firebase-binding-audit/report.json` -> `CloudFirestore` -> `GetQueryNamed` (`signature-drift`) | The old binding exported `getQueryNamed:completion:` as `NSInputStream` instead of `NSString`, so native Firestore received `__NSCFInputStream` and threw an Objective-C exception when it treated the argument like a string. | Proved and fixed. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-success.json`. | [#112](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/112) | `3077a538a892de0db3350f0a459bf8620729f4db` |
2 changes: 1 addition & 1 deletion source/Firebase/ABTesting/ApiDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface ExperimentController {

// -(void)validateRunningExperimentsForServiceOrigin:(NSString * _Nonnull)origin runningExperimentPayloads:(NSArray<ABTExperimentPayload *> * _Nonnull)payloads;
[Export ("validateRunningExperimentsForServiceOrigin:runningExperimentPayloads:")]
void ValidateRunningExperiments (string origin, NSObject [] payloads);
void ValidateRunningExperiments (string origin, ExperimentPayload [] payloads);

// -(void)activateExperiment:(ABTExperimentPayload * _Nonnull)experimentPayload forServiceOrigin:(NSString * _Nonnull)origin;
[Export ("activateExperiment:forServiceOrigin:")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
using ObjCRuntime;
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_VALIDATERUNNINGEXPERIMENTS
using Firebase.ABTesting;
using Foundation;
using ObjCRuntime;
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_GETQUERYNAMED
using Firebase.CloudFirestore;
using Foundation;
Expand Down Expand Up @@ -126,6 +132,66 @@ void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEven
}
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_VALIDATERUNNINGEXPERIMENTS
static Task<string> VerifyABTestingValidateRunningExperimentsAsync()
{
const string selector = "validateRunningExperimentsForServiceOrigin:runningExperimentPayloads:";

var controller = ExperimentController.SharedInstance;
if (controller is null)
{
throw new InvalidOperationException("Firebase.ABTesting.ExperimentController.SharedInstance returned null after App.Configure().");
}

var payloads = Array.Empty<ExperimentPayload>();
Comment thread
AdamEssenmacher marked this conversation as resolved.
var origin = "codex";
NSException? marshaledException = null;
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;

void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
{
marshaledException ??= args.Exception;
marshaledExceptionMode ??= args.ExceptionMode;
}

Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
try
{
try
{
controller.ValidateRunningExperiments(origin, payloads);
}
catch (ObjCException ex)
{
throw new InvalidOperationException(
$"Selector '{selector}' should not throw after the binding fix, but observed {ex.GetType().FullName}. " +
$"Managed payload array type: {payloads.GetType().FullName}. Payload count: {payloads.Length}. " +
$"Origin argument type: {origin.GetType().FullName}. " +
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
ex);
}

if (marshaledException is not null)
{
throw new InvalidOperationException(
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
}

return Task.FromResult(
$"Selector '{selector}' completed without ObjC exception after the binding fix. " +
$"Managed payload array type: {payloads.GetType().FullName}. Payload count: {payloads.Length}. " +
$"Origin argument type: {origin.GetType().FullName}.");
}
finally
{
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
}
}
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_GETQUERYNAMED
static async Task<string> VerifyCloudFirestoreGetQueryNamedAsync()
{
Expand Down
11 changes: 11 additions & 0 deletions tests/E2E/Firebase.Foundation/runtime-drift-cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
}
]
},
{
"id": "abtesting-validaterunningexperiments",
"method": "VerifyABTestingValidateRunningExperimentsAsync",
"bindingPackage": "AdamE.Firebase.iOS.ABTesting",
"packages": [
{
"id": "AdamE.Firebase.iOS.ABTesting",
"version": "12.6.0"
}
]
},
{
"id": "cloudfirestore-getquerynamed",
"method": "VerifyCloudFirestoreGetQueryNamedAsync",
Expand Down
Loading