commit 397226517727e583bbd1ddd39845c37e65f1a99f
parent c4c352c2133350d86f3ebe32be8e97da5cff5480
Author: Marc Stibane <marc@taler.net>
Date: Fri, 14 Aug 2026 07:30:43 +0200
AI: Amount multiplication overflow
Diffstat:
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/taler-swift/Sources/taler-swift/Amount.swift b/taler-swift/Sources/taler-swift/Amount.swift
@@ -487,13 +487,22 @@ public final class Amount: Codable, Hashable, @unchecked Sendable, CustomStringC
/// - Parameters:
/// - amount: The amount to multiply.
/// - factor: The scalar multiplying `amount`.
+ /// - Throws:
+ /// - `AmountError.invalidAmount` if the product exceeds the maximum allowed value.
/// - Returns: The product of `amount` and `factor`, normalized.
public static func * (amount: Amount, factor: UInt32) throws -> Amount {
let result = try amount.normalizedCopy()
- result.integer = result.integer * UInt64(factor)
+ // report the overflow instead of trapping - normalize() below rejects any product
+ // above the maximum anyway, but only if we get that far
+ let product = result.integer.multipliedReportingOverflow(by: UInt64(factor))
+ guard !product.overflow else { throw AmountError.invalidAmount }
+ // fraction < fractionalBase() and factor <= UInt32.max, so this cannot overflow
let fraction_tmp = UInt64(result.fraction) * UInt64(factor)
- result.integer += fraction_tmp / UInt64(fractionalBase())
+ let sum = product.partialValue.addingReportingOverflow(fraction_tmp / UInt64(fractionalBase()))
+ guard !sum.overflow else { throw AmountError.invalidAmount }
+ result.integer = sum.partialValue
result.fraction = UInt32(fraction_tmp % UInt64(fractionalBase()))
+ try result.normalize()
return result
}
diff --git a/taler-swift/Tests/taler-swiftTests/AmountTests.swift b/taler-swift/Tests/taler-swiftTests/AmountTests.swift
@@ -86,6 +86,21 @@ class AmountTests: XCTestCase {
XCTAssert(try Amount(fromString: "EUR:500000000.00000001") * 3 == Amount(fromString: "EUR:1500000000.00000003"))
XCTAssertThrowsError(try Amount(fromString: "4000000000000000") * 2)
}
+
+ func testMultiplicationOverflow() {
+ // 2^52 is the largest integer part the protocol allows, so it can be parsed and
+ // then multiplied. Neither the overflow of `integer * factor` nor a product that
+ // is merely out of range may escape as a trap or as an invalid Amount.
+ let maxValue = 4503599627370496 // 2^52
+ XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue)") * 2)
+ XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue)") * 5000)
+ XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue).99999999") * UInt32.max)
+ XCTAssertThrowsError(try Amount(fromString: "EUR:2251799813685249") * 2)
+
+ // the carry from the fraction still works, and the result is normalized
+ XCTAssert(try Amount(fromString: "EUR:0.00000001") * UInt32.max == Amount(fromString: "EUR:42.94967295"))
+ XCTAssert(try Amount(fromString: "EUR:2251799813685248") * 2 == Amount(fromString: "EUR:\(maxValue)"))
+ }
func testDivision() {
XCTAssert(try Amount(fromString: "EUR:2") / 1 == Amount(fromString: "EUR:2"))