From 32bb24ad4142c125981cfa87285a7431232b88f4 Mon Sep 17 00:00:00 2001 From: ETolboom Date: Tue, 29 Sep 2026 12:47:25 +0200 Subject: [PATCH] fix(logging): stop os_log crashing on more than five arguments OSLog.log handed the [CVarArg] array to os_log for 6+ arguments, which reads garbage pointers for each %@ and segfaults in _os_log_fmt_flatten_object_impl. The seven-argument "Backfill acknowledged" line hit this on every backfill, crash-looping the app. The fallback now asserts in debug and logs the pre-rendered line as a single argument in release. The backfill ack line is collapsed to one argument. --- .../Session/TransmitterSession.swift | 8 ++++---- Sources/G6SensorCore/Transport/OSLog.swift | 20 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/G6SensorCore/Session/TransmitterSession.swift b/Sources/G6SensorCore/Session/TransmitterSession.swift index 3176be4..17d77a9 100644 --- a/Sources/G6SensorCore/Session/TransmitterSession.swift +++ b/Sources/G6SensorCore/Session/TransmitterSession.swift @@ -466,10 +466,10 @@ public final class TransmitterSession: TransmitterConnectionDelegate { let ack = try peripheral.requestBackfill(startTime: startTime, endTime: endTime) - log.default("Backfill acknowledged: status %{public}@, backfillStatus %{public}@, id %{public}@, range %{public}@-%{public}@, length %{public}@, crc %{public}@", - String(ack.status), String(ack.backfillStatus), String(ack.identifier), - String(ack.startTime), String(ack.endTime), - String(ack.bufferLength), String(format: "%04x", ack.bufferCRC)) + log.default("Backfill acknowledged: %{public}@", + "status \(ack.status), backfillStatus \(ack.backfillStatus), id \(ack.identifier), " + + "range \(ack.startTime)-\(ack.endTime), length \(ack.bufferLength), " + + "crc \(String(format: "%04x", ack.bufferCRC))") // Nothing stored for the window: an empty result, not a failure. guard ack.bufferLength > 0 else { diff --git a/Sources/G6SensorCore/Transport/OSLog.swift b/Sources/G6SensorCore/Transport/OSLog.swift index 847d5be..999052d 100644 --- a/Sources/G6SensorCore/Transport/OSLog.swift +++ b/Sources/G6SensorCore/Transport/OSLog.swift @@ -34,7 +34,8 @@ extension OSLog { } private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { - mirrorToFile(message, type: type, args) + let rendered = render(message, args) + mirrorToFile(rendered, type: type) switch args.count { case 0: @@ -50,7 +51,10 @@ extension OSLog { case 5: os_log(message, log: self, type: type, args[0], args[1], args[2], args[3], args[4]) default: - os_log(message, log: self, type: type, args) + // os_log can't take the array as varargs: handing it over reads + // garbage pointers for each %@ and crashes. Log the rendered line. + assertionFailure("OSLog supports at most 5 arguments, got \(args.count): \(message)") + os_log("%{public}@", log: self, type: type, rendered) } } @@ -65,15 +69,17 @@ extension OSLog { return Self.categories[ObjectIdentifier(self)] ?? "G6SensorKit" } - /// Mirrors every OSLog line into the exportable file log. Format - /// specifiers are OSLog-flavoured (%{public}@), so the privacy qualifier - /// is stripped before String(format:) sees them. - private func mirrorToFile(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { + /// Format specifiers are OSLog-flavoured (%{public}@), so the privacy + /// qualifier is stripped before String(format:) sees them. + private func render(_ message: StaticString, _ args: [CVarArg]) -> String { let template = "\(message)" .replacingOccurrences(of: "%{public}", with: "%") .replacingOccurrences(of: "%{private}", with: "%") - let rendered = args.isEmpty ? template : String(format: template, arguments: args) + return args.isEmpty ? template : String(format: template, arguments: args) + } + /// Mirrors every OSLog line into the exportable file log. + private func mirrorToFile(_ rendered: String, type: OSLogType) { let level: String switch type { case .debug: level = "DEBUG"