Skip to content

Commit 31fd522

Browse files
dfedpblazej
andauthored
Do not leak Sendable extensions to consumers (#848)
LiveKit currently marks multiple types that are not thread-safe as being `Sendable`. These extensions mean that consumer's applications also treat these types as `Sendable`, which can lead to thread-safety bugs. This PR removes the Sendable extensions without adding new warnings. This is accomplished by utilizing internal wrapper types that are marked as `@unchecked Sendable`. --------- Co-authored-by: Błażej Pankowski <[email protected]>
1 parent d94c575 commit 31fd522

File tree

6 files changed

+62
-6
lines changed

6 files changed

+62
-6
lines changed

.changes/sendable-extension

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="changed" "Removed '@unchecked Sendable' extensions on common types"

Sources/LiveKit/Extensions/Sendable.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ extension LKRTCCallbackLogger: @unchecked Swift.Sendable {}
4848

4949
// MARK: Collections
5050

51-
extension NSHashTable: @unchecked Swift.Sendable {} // cannot specify Obj-C generics
52-
extension NSMapTable: @unchecked Swift.Sendable {} // cannot specify Obj-C generics
5351
#if swift(<6.2)
5452
extension Dictionary: Swift.Sendable where Key: Sendable, Value: Sendable {}
5553
#endif
@@ -58,4 +56,3 @@ extension Dictionary: Swift.Sendable where Key: Sendable, Value: Sendable {}
5856

5957
extension AVCaptureDevice: @unchecked Swift.Sendable {}
6058
extension AVCaptureDevice.Format: @unchecked Swift.Sendable {}
61-
extension CVPixelBuffer: @unchecked Swift.Sendable {}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2025 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
19+
/// A thin unchecked sendable wrapper around NSMapTable.
20+
final class MapTable<KeyType, ObjectType>: @unchecked Sendable where KeyType: AnyObject, ObjectType: AnyObject {
21+
init(_ mapTable: NSMapTable<KeyType, ObjectType>) {
22+
self.mapTable = mapTable
23+
}
24+
25+
class func weakToStrongObjects() -> MapTable<KeyType, ObjectType> {
26+
.init(.weakToStrongObjects())
27+
}
28+
29+
func object(forKey aKey: KeyType?) -> ObjectType? {
30+
mapTable.object(forKey: aKey)
31+
}
32+
33+
func removeObject(forKey aKey: KeyType?) {
34+
mapTable.removeObject(forKey: aKey)
35+
}
36+
37+
func setObject(_ anObject: ObjectType?, forKey aKey: KeyType?) {
38+
mapTable.setObject(anObject, forKey: aKey)
39+
}
40+
41+
var count: Int {
42+
mapTable.count
43+
}
44+
45+
func objectEnumerator() -> NSEnumerator? {
46+
mapTable.objectEnumerator()
47+
}
48+
49+
func removeAllObjects() {
50+
mapTable.removeAllObjects()
51+
}
52+
53+
private let mapTable: NSMapTable<KeyType, ObjectType>
54+
}

Sources/LiveKit/Track/Track.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public class Track: NSObject, @unchecked Sendable, Loggable {
128128
var rtpReceiver: LKRTCRtpReceiver?
129129

130130
// All VideoRendererAdapters attached to this track, key/value for direct removal.
131-
var videoRendererAdapters = NSMapTable<VideoRenderer, VideoRendererAdapter>.weakToStrongObjects()
131+
var videoRendererAdapters = MapTable<VideoRenderer, VideoRendererAdapter>.weakToStrongObjects()
132132
}
133133

134134
let _state: StateSync<State>

Sources/LiveKit/Types/Options/CameraCaptureOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public final class CameraCaptureOptions: NSObject, VideoCaptureOptions, Sendable
2525
public let deviceType: AVCaptureDevice.DeviceType?
2626
#endif
2727

28-
/// Exact devce to use.
28+
/// Exact device to use.
2929
@objc
3030
public let device: AVCaptureDevice?
3131

Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,18 @@ public final class BackgroundBlurVideoProcessor: NSObject, @unchecked Sendable,
133133
private func cacheMask(inputBuffer: CVPixelBuffer, inputDimensions: CGSize) {
134134
guard frameCount % segmentationFrameInterval == 0 else { return }
135135

136+
struct PixelBufferHolder: @unchecked Sendable {
137+
let buffer: CVPixelBuffer
138+
}
139+
let inputBuffer = PixelBufferHolder(buffer: inputBuffer)
136140
segmentationQueue.async {
137141
#if LK_SIGNPOSTS
138142
os_signpost(.begin, log: self.signpostLog, name: #function)
139143
defer {
140144
os_signpost(.end, log: self.signpostLog, name: #function)
141145
}
142146
#endif
143-
try? self.segmentationRequestHandler.perform([self.segmentationRequest], on: inputBuffer)
147+
try? self.segmentationRequestHandler.perform([self.segmentationRequest], on: inputBuffer.buffer)
144148

145149
guard let maskPixelBuffer = self.segmentationRequest.results?.first?.pixelBuffer else { return }
146150
let maskImage = CIImage(cvPixelBuffer: maskPixelBuffer)

0 commit comments

Comments
 (0)