Skip to content

Commit aaeb5f4

Browse files
authored
[FirebaseCoreInternal] Guard against invalid indexing into RingBuffer type (#10032)
* [FirebaseCoreInternal] Refactor RingBuffer indexing code * Add unit tests for pop API * Some more refactoring * Add bandaid for improper bounds * Review * Review 2 * Prevent duplicate dailies * CHANGELOG * Use guard statement instead * Collect more info
1 parent 7e3548b commit aaeb5f4

File tree

4 files changed

+118
-35
lines changed

4 files changed

+118
-35
lines changed

FirebaseCore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Firebase 9.4.0
2+
- [fixed] Fixed rare crash on launch due to out-of-bounds exception in FirebaseCore. (#10025)
3+
14
# Firebase 9.3.0
25
- [fixed] Remove GoogleSignInSwiftSupport from Zip and Carthage distributions due to
36
infeasibility. The GoogleSignIn distribution continues. (#9937)

FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatsBundle.swift

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,49 @@ struct HeartbeatsBundle: Codable, HeartbeatsPayloadConvertible {
5757
return // Do not append if capacity is non-positive.
5858
}
5959

60-
// 1. Push the heartbeat to the back of the buffer.
61-
if let overwrittenHeartbeat = buffer.push(heartbeat) {
62-
// If a heartbeat was overwritten, update the cache to ensure it's date
63-
// is removed (if it was stored).
64-
lastAddedHeartbeatDates = lastAddedHeartbeatDates.mapValues { date in
65-
overwrittenHeartbeat.date == date ? .distantPast : date
60+
do {
61+
// Push the heartbeat to the back of the buffer.
62+
if let overwrittenHeartbeat = try buffer.push(heartbeat) {
63+
// If a heartbeat was overwritten, update the cache to ensure it's date
64+
// is removed.
65+
lastAddedHeartbeatDates = lastAddedHeartbeatDates.mapValues { date in
66+
overwrittenHeartbeat.date == date ? .distantPast : date
67+
}
68+
}
69+
70+
// Update cache with the new heartbeat's date.
71+
heartbeat.timePeriods.forEach {
72+
lastAddedHeartbeatDates[$0] = heartbeat.date
6673
}
67-
}
6874

69-
// 2. Update cache with the new heartbeat's date.
70-
heartbeat.timePeriods.forEach {
71-
lastAddedHeartbeatDates[$0] = heartbeat.date
75+
} catch let error as RingBufferError {
76+
// A ring buffer error occurred while pushing to the buffer so the bundle
77+
// is reset.
78+
self = HeartbeatsBundle(capacity: capacity)
79+
80+
// Create a diagnostic heartbeat to capture the failure and add it to the
81+
// buffer. The failure is added as a key/value pair to the agent string.
82+
// Given that the ring buffer has been reset, it is not expected for the
83+
// second push attempt to fail.
84+
let errorDescription = error.errorDescription.replacingOccurrences(of: " ", with: "-")
85+
let diagnosticHeartbeat = Heartbeat(
86+
agent: "\(heartbeat.agent) error/\(errorDescription)",
87+
date: heartbeat.date,
88+
timePeriods: heartbeat.timePeriods
89+
)
90+
91+
let secondPushAttempt = Result {
92+
try buffer.push(diagnosticHeartbeat)
93+
}
94+
95+
if case .success = secondPushAttempt {
96+
// Update cache with the new heartbeat's date.
97+
diagnosticHeartbeat.timePeriods.forEach {
98+
lastAddedHeartbeatDates[$0] = diagnosticHeartbeat.date
99+
}
100+
}
101+
} catch {
102+
// Ignore other error.
72103
}
73104
}
74105

@@ -90,7 +121,11 @@ struct HeartbeatsBundle: Codable, HeartbeatsPayloadConvertible {
90121
}
91122

92123
poppedHeartbeats.reversed().forEach {
93-
buffer.push($0)
124+
do {
125+
try buffer.push($0)
126+
} catch {
127+
// Ignore error.
128+
}
94129
}
95130

96131
return removedHeartbeat

FirebaseCore/Internal/Sources/HeartbeatLogging/RingBuffer.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,61 @@
1414

1515
import Foundation
1616

17+
/// Error types for `RingBuffer` operations.
18+
enum RingBufferError: LocalizedError {
19+
case outOfBoundsPush(pushIndex: Array.Index, endIndex: Array.Index)
20+
21+
var errorDescription: String {
22+
switch self {
23+
case let .outOfBoundsPush(pushIndex, endIndex):
24+
return "Out-of-bounds push at index \(pushIndex) to ring buffer with" +
25+
"end index of \(endIndex)."
26+
}
27+
}
28+
}
29+
1730
/// A generic circular queue structure.
1831
struct RingBuffer<Element>: Sequence {
1932
/// An array of heartbeats treated as a circular queue and intialized with a fixed capacity.
2033
private var circularQueue: [Element?]
2134
/// The current "tail" and insert point for the `circularQueue`.
22-
private var tailIndex: Int = 0
35+
private var tailIndex: Array.Index
2336

2437
/// Designated initializer.
2538
/// - Parameter capacity: An `Int` representing the capacity.
2639
init(capacity: Int) {
2740
circularQueue = Array(repeating: nil, count: capacity)
41+
tailIndex = circularQueue.startIndex
2842
}
2943

3044
/// Pushes an element to the back of the buffer, returning the element (`Element?`) that was overwritten.
3145
/// - Parameter element: The element to push to the back of the buffer.
3246
/// - Returns: The element that was overwritten or `nil` if nothing was overwritten.
3347
/// - Complexity: O(1)
3448
@discardableResult
35-
mutating func push(_ element: Element) -> Element? {
49+
mutating func push(_ element: Element) throws -> Element? {
3650
guard circularQueue.count > 0 else {
3751
// Do not push if `circularQueue` is a fixed empty array.
3852
return nil
3953
}
4054

41-
defer {
42-
// Increment index, wrapping around to the start if needed.
43-
tailIndex += 1
44-
tailIndex %= circularQueue.count
55+
guard circularQueue.indices.contains(tailIndex) else {
56+
// We have somehow entered an invalid state (#10025).
57+
throw RingBufferError.outOfBoundsPush(
58+
pushIndex: tailIndex,
59+
endIndex: circularQueue.endIndex
60+
)
4561
}
4662

4763
let replaced = circularQueue[tailIndex]
4864
circularQueue[tailIndex] = element
65+
66+
// Increment index, wrapping around to the start if needed.
67+
tailIndex += 1
68+
if tailIndex >= circularQueue.endIndex {
69+
tailIndex = circularQueue.startIndex
70+
}
71+
4972
return replaced
5073
}
5174

@@ -61,8 +84,8 @@ struct RingBuffer<Element>: Sequence {
6184

6285
// Decrement index, wrapping around to the back if needed.
6386
tailIndex -= 1
64-
if tailIndex < 0 {
65-
tailIndex = circularQueue.count - 1
87+
if tailIndex < circularQueue.startIndex {
88+
tailIndex = circularQueue.endIndex - 1
6689
}
6790

6891
guard let popped = circularQueue[tailIndex] else {

FirebaseCore/Internal/Tests/Unit/RingBufferTests.swift

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class RingBufferTests: XCTestCase {
2323
// Given
2424
var ringBuffer = RingBuffer<Element>(capacity: 0)
2525
// When
26-
ringBuffer.push("ezra")
26+
try ringBuffer.push("ezra")
2727
// Then
2828
XCTAssertEqual(Array(ringBuffer), [])
2929
}
@@ -32,17 +32,17 @@ class RingBufferTests: XCTestCase {
3232
// Given
3333
var ringBuffer = RingBuffer<Element>(capacity: 3) // [nil, nil, nil]
3434
// When
35-
let overwrittenElement = ringBuffer.push("vader") // ["vader", nil, nil]
35+
let overwrittenElement = try ringBuffer.push("vader") // ["vader", nil, nil]
3636
// Then
3737
XCTAssertNil(overwrittenElement)
3838
}
3939

4040
func testPush_WhenAtFullCapacity_OverwritesAndReturnsTailElement() throws {
4141
// Given
4242
var ringBuffer = RingBuffer<Element>(capacity: 1) // [nil]
43-
ringBuffer.push("luke") // ["luke"] where "luke" is the tail element.
43+
try ringBuffer.push("luke") // ["luke"] where "luke" is the tail element.
4444
// When
45-
let overwrittenElement = ringBuffer.push("vader")
45+
let overwrittenElement = try ringBuffer.push("vader")
4646
// Then
4747
XCTAssertEqual(overwrittenElement, "luke")
4848
XCTAssertEqual(Array(ringBuffer), ["vader"])
@@ -52,10 +52,10 @@ class RingBufferTests: XCTestCase {
5252
// Given
5353
var ringBuffer = RingBuffer<Element>(capacity: 3) // [nil, nil, nil]
5454
// When
55-
ringBuffer.push("chewy") // ["chewy", nil, nil]
56-
ringBuffer.push("vader") // ["chewy", "vader", nil]
57-
ringBuffer.push("jabba") // ["chewy", "vader", "jabba"]
58-
ringBuffer.push("lando") // ["lando", "vader", "jabba"]
55+
try ringBuffer.push("chewy") // ["chewy", nil, nil]
56+
try ringBuffer.push("vader") // ["chewy", "vader", nil]
57+
try ringBuffer.push("jabba") // ["chewy", "vader", "jabba"]
58+
try ringBuffer.push("lando") // ["lando", "vader", "jabba"]
5959
// Then
6060
XCTAssertEqual(Array(ringBuffer), ["lando", "vader", "jabba"])
6161
}
@@ -64,9 +64,9 @@ class RingBufferTests: XCTestCase {
6464
// Given
6565
var ringBuffer = RingBuffer<Element>(capacity: 10)
6666
// When
67-
ringBuffer.push("han solo")
68-
ringBuffer.push("boba")
69-
ringBuffer.push("jabba")
67+
try ringBuffer.push("han solo")
68+
try ringBuffer.push("boba")
69+
try ringBuffer.push("jabba")
7070
// Then
7171
XCTAssertEqual(Array(ringBuffer), ["han solo", "boba", "jabba"])
7272
}
@@ -76,12 +76,34 @@ class RingBufferTests: XCTestCase {
7676
var ringBuffer = RingBuffer<Int>(capacity: 10)
7777
// When
7878
for index in 1 ... 1000 {
79-
ringBuffer.push(index)
79+
try ringBuffer.push(index)
8080
}
8181
// Then
8282
XCTAssertEqual(Array(ringBuffer), Array(991 ... 1000))
8383
}
8484

85+
func testPopBeforePushing() throws {
86+
// Given
87+
var ringBuffer = RingBuffer<Element>(capacity: 2) // [`tailIndex` -> nil, nil]
88+
// When
89+
ringBuffer.pop() // [nil, `tailIndex` -> nil]
90+
try ringBuffer.push("yoda") // [nil, "yoda"]
91+
try ringBuffer.push("mando") // ["mando", "yoda"]
92+
// Then
93+
XCTAssertEqual(Array(ringBuffer), ["mando", "yoda"])
94+
}
95+
96+
func testPopStressTest() throws {
97+
// Given
98+
var ringBuffer = RingBuffer<Int>(capacity: 10)
99+
// When
100+
for _ in 1 ... 1000 {
101+
XCTAssertNil(ringBuffer.pop())
102+
}
103+
// Then
104+
XCTAssertEqual(Array(ringBuffer), [])
105+
}
106+
85107
func testPop_WhenCapacityIsZero_DoesNothingAndReturnsNil() throws {
86108
// Given
87109
var ringBuffer = RingBuffer<String>(capacity: 0)
@@ -95,9 +117,9 @@ class RingBufferTests: XCTestCase {
95117
func testPopRemovesAndReturnsLastElement() throws {
96118
// Given
97119
var ringBuffer = RingBuffer<Element>(capacity: 3)
98-
ringBuffer.push("one")
99-
ringBuffer.push("two")
100-
ringBuffer.push("three")
120+
try ringBuffer.push("one")
121+
try ringBuffer.push("two")
122+
try ringBuffer.push("three")
101123
// When
102124
XCTAssertEqual(ringBuffer.pop(), "three")
103125
XCTAssertEqual(ringBuffer.pop(), "two")
@@ -110,7 +132,7 @@ class RingBufferTests: XCTestCase {
110132
// Given
111133
var ringBuffer = RingBuffer<Int>(capacity: 10)
112134
for number in Array(1 ... 15) {
113-
ringBuffer.push(number)
135+
try ringBuffer.push(number)
114136
}
115137

116138
// ringBuffer: [11, 12, 13, 14, 15, 6, 7, 8, 9, 10]

0 commit comments

Comments
 (0)