taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 397226517727e583bbd1ddd39845c37e65f1a99f
parent c4c352c2133350d86f3ebe32be8e97da5cff5480
Author: Marc Stibane <marc@taler.net>
Date:   Fri, 14 Aug 2026 07:30:43 +0200

AI: Amount multiplication overflow

Diffstat:
Mtaler-swift/Sources/taler-swift/Amount.swift | 13+++++++++++--
Mtaler-swift/Tests/taler-swiftTests/AmountTests.swift | 15+++++++++++++++
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"))