View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011933 | Taler | wallet (iOS App) | public | 2026-11-03 23:45 | 2026-11-04 13:02 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | MarcS | Reviewer | Christian Grothoff | ||
| Release signed by | MarcS | Deployed by | MarcS | ||
| Verifier | Christian Grothoff | Communicator | CM_7 | ||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | crash | Reproducibility | have not tried |
| Status | planning | Resolution | open | ||
| Summary | 0011933: Terms of Service / Balances: app terminates formatting an amount when the locale has no region (Fatal error at CurrencySpecification.swift:204) | ||||
| Description | The app terminates with a Swift runtime trap as soon as it formats an amount, whenever the active locale carries no region ("de" rather than "de_DE"). CurrencySpecification.swift:204: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value CurrencySpecification.currencyString(_:useISO:) passes NumberFormatter.currencySymbol and NumberFormatter.currencyCode straight into String.replacingOccurrences(of:with:). Both are implicitly-unwrapped optionals (String!), and both are nil when the formatter's locale has no region, because a bare language identifies no currency. CurrencyFormatter.init() sets self.locale = Locale.autoupdatingCurrent, so the formatter inherits whatever the process was launched with. Launching with -AppleLanguages (de) or -AppleLocale de therefore produces a formatter whose currencyCode is nil, and the first amount that reaches currencyString() takes the app down. Three call sites share the pattern: lines 187-188, 193-194 and 203-204 of TalerCommon/Helper/CurrencySpecification.swift. The crash observed was at 204, but the other two are equally exposed. Who is affected --------------- Anyone running the app with a language override that carries no region. That is exactly what Xcode's scheme option "Application Language" does, and what a UI-test or screenshot run does when it sets -AppleLanguages. It is reached before any user interaction beyond opening a screen that shows a balance, so for those runs the app is unusable. A device whose Settings give a language *and* a region is unaffected, which is the normal end-user configuration, so the end-user exposure is narrower than the crash severity alone suggests. It still terminates the process, and the same nil can be reached by any future code path that builds a formatter from a region-less locale. Evidence that it is the missing region, not the language -------------------------------------------------------- Same walk, same tape, four locales: locale steps completed result de 2 CRASH at CurrencySpecification.swift:204 en 2 CRASH at CurrencySpecification.swift:204 de_DE 40 no crash (no override) 40 no crash Both bare languages crash and both region-bearing configurations do not, so the language is irrelevant and the missing region is the cause. How to reproduce ---------------- Click sequence (all of it steered automatically by the harness): 1. launch the app with -AppleLanguages (de) -AppleLocale de 2. onboarding -> "Withdraw CHF" (button.onboarding.withdrawReal) 3. -> "Terms of Service" (button.withdraw.tos.open) 4. -> "Accept Terms of Service" (button.withdraw.tos.accept) 5. the balances list redraws; the app terminates ~700 ms later No fault injection is involved; this reproduces on a plain run against the normal demo services. Reproducing with the exploration harness ---------------------------------------- The harness lives on branch dev/phoenix-bot/fuzzbang (local name dev/bot-cg/fuzzbang), under harness/. From harness/ on a Mac with Xcode: python3 -m taler_fuzz build python3 -m taler_fuzz --locale de \ --prefer "demo cash,withdraw,confirm,accept,terms,balance,settings" \ replay --tape "0,5,0,5,0,5,0,5,0,5,0,5,9,15,11,6,4,3,11,13,10,6,15,14,8,1,0,2,12,0,15,10,7,10,2,6,7,7,4,14" \ --label crash --shots Expected on an unpatched build: the run stops after 2 steps and reports [critical] app-exit: the app stopped running during exploration and ~/fuzzwork/out/replay-crash/oslog.ndjson contains the Fatal error line. Repeat with --locale de_DE to see the same tape complete all 40 steps. The --prefer list matters: the tape alone does not determine the walk, because the steering decision consumes tape values. Patch ----- Attached: fix-currencycode-crash.patch (one file, +27/-10), against master. The three call sites now go through one helper that substitutes whichever markers the formatter actually has: private func replacingCurrencyMarkers(in aString: String, with replacement: String) -> String { var result = aString if let symbol = formatter.currencySymbol, !symbol.isEmpty { result = result.replacingOccurrences(of: symbol, with: replacement) } if let code = formatter.currencyCode, !code.isEmpty { result = result.replacingOccurrences(of: code, with: replacement) } return result } Verified: with the patch applied, the same tape completes all 40 steps under both --locale de and --locale en, with no CurrencySpecification fatal error. Also available as branch dev/phoenix-bot/fix-currencycode-crash. Note for review: the patch makes the app survive, but a region-less locale also means there is no sensible currency symbol to substitute, so amounts will be rendered with the raw currency name. If that is not the wanted presentation, the better fix may be to give CurrencyFormatter an explicit locale rather than Locale.autoupdatingCurrent -- the "TODO: Yikes, might override my currency!" already on that line suggests this was known to be fragile. Estimated severity ------------------ Critical - the app stops working (crash, exit or wedged UI); a user hitting this loses the flow they were in. Estimated effort to fix ----------------------- Small - about an hour. The fix is one private helper and three call sites in a single file, with no API change and no behavioural change for locales that do carry a region. Most of the time goes into deciding what amounts should look like when no currency symbol is available, and into checking the other NumberFormatter IUO properties in that class for the same pattern. Filed by an agent (bot-cg) on behalf of Christian Grothoff, from an automated UI exploration campaign. Left unassigned deliberately. | ||||
| Tags | No tags attached. | ||||
| Attached Files | fix-currencycode-crash.patch (3,636 bytes)
diff --git a/TalerCommon/Helper/CurrencySpecification.swift b/TalerCommon/Helper/CurrencySpecification.swift
index 5a6811ee..5ea2b6ac 100644
--- a/TalerCommon/Helper/CurrencySpecification.swift
+++ b/TalerCommon/Helper/CurrencySpecification.swift
@@ -181,29 +181,46 @@ public struct CurrencyInfo: Sendable {
}
}
+ /// Replace whichever currency markers the formatter actually has.
+ ///
+ /// `NumberFormatter.currencySymbol` and `.currencyCode` are
+ /// implicitly-unwrapped optionals, and both are nil when the formatter's
+ /// locale carries no region -- `de` rather than `de_DE`. `CurrencyFormatter`
+ /// takes its locale from `Locale.autoupdatingCurrent`, so that happens
+ /// whenever the app runs with `-AppleLanguages`/`-AppleLocale` set to a bare
+ /// language, which is what Xcode's "Application Language" scheme option and
+ /// UI-test runs do. Passing such a value straight to
+ /// `replacingOccurrences(of:)` traps and takes the app down.
+ private func replacingCurrencyMarkers(in aString: String,
+ with replacement: String) -> String {
+ var result = aString
+ if let symbol = formatter.currencySymbol, !symbol.isEmpty {
+ result = result.replacingOccurrences(of: symbol, with: replacement)
+ }
+ if let code = formatter.currencyCode, !code.isEmpty {
+ result = result.replacingOccurrences(of: code, with: replacement)
+ }
+ return result
+ }
+
func currencyString(_ aString: String, useISO: Bool = false) -> String {
if !useISO {
if let aSymbol = altUnitSymbol {
- let symbolString = aString.replacingOccurrences(of: formatter.currencySymbol, with: aSymbol)
- return symbolString.replacingOccurrences(of: formatter.currencyCode, with: aSymbol)
+ return replacingCurrencyMarkers(in: aString, with: aSymbol)
}
if let aName = altUnitName0 {
let spacedName = formatter.leadingCurrencySymbol ? aName + SPACE
: SPACE + aName
- let spacedString1 = aString.replacingOccurrences(of: formatter.currencySymbol, with: spacedName)
- let spacedString2 = spacedString1.replacingOccurrences(of: formatter.currencyCode, with: spacedName)
- let spacedString3 = spacedString2.replacingOccurrences(of: " ", with: " ") // ensure we have only 1 space
- return spacedString3
+ let spaced = replacingCurrencyMarkers(in: aString, with: spacedName)
+ return spaced.replacingOccurrences(of: " ", with: " ") // ensure we have only 1 space
}
}
let currency = formatter.currency
if !currency.isEmpty {
let spacedName = formatter.leadingCurrencySymbol ? currency + SPACE
: SPACE + currency
- let spacedString1 = aString.replacingOccurrences(of: formatter.currencySymbol, with: spacedName)
- let spacedString2 = spacedString1.replacingOccurrences(of: formatter.currencyCode, with: spacedName)
- let spacedString3 = spacedString2.replacingOccurrences(of: " ", with: " ") // ensure we have only 1 space
- return spacedString3
+ let spaced = replacingCurrencyMarkers(in: aString, with: spacedName)
+ return spaced.replacingOccurrences(of: " ", with: " ") // ensure we have only 1 space
}
return aString
}
| ||||
| Follow-up evidence on the presentation question raised with the patch. With the patch applied the app no longer crashes, and a locale-sweep across de_DE, fr_FR, es_ES, pl_PL, uk_UA, en_US, de and fr shows what it renders instead: amounts in a region-less locale come out with the ISO 4217 "no currency" code XXX. locale occurrences of "XXX" on screen example de 290 'Kurzwahl 5,00 XXX' fr 126 'Raccourci 5,00 XXX' de_DE 0 fr_FR 0 es_ES 0 pl_PL 0 uk_UA 0 en_US 0 Exactly the locales without a region, and no others. So the force-unwrap and the XXX rendering are the same root cause seen twice: NumberFormatter has no currency to work with, nil for currencyCode and XXX once a value is asked for. The clamp in the attached patch is therefore necessary but not sufficient if the wanted behaviour is a sensible amount rather than a surviving app. Giving CurrencyFormatter an explicit locale, instead of Locale.autoupdatingCurrent, would address both -- the `// TODO: Yikes, might override my currency! Needs testing!` already sits on that line. Reproduced with the exploration harness: python3 -m taler_fuzz --locale de explore -n 2 --run-id loc-de python3 -m taler_fuzz --locale de_DE explore -n 2 --run-id loc-de_DE then compare the recorded screens for "XXX". | |
| Patch verified on the running app, by A/B. One walk, the `de` locale, two builds. The baseline is the exploration harness branch with this very fix *reverted*, because the fix is already merged into that branch and a plain checkout of it no longer crashes: without the fix (dev/phoenix-bot/crash-baseline) [critical] app-exit: the app stopped running during exploration app-not-running at t=7321 ms, after 2 steps, 3 recorded screens last screen: "Allgemeine Geschäftsbedingungen" with the fix (dev/phoenix-bot/fuzzbang) 8 steps, 10 recorded screens, no app-exit, no findings last screen: "Fr. abheben" with the amount shortcuts rendered To re-run, on branch dev/phoenix-bot/fuzzbang under harness/: python3 -m taler_fuzz build python3 -m taler_fuzz --locale de --max-steps 8 \ --prefer "withdraw,accept,terms,balance,abheben" \ replay --tape "1,0,1,0,2,1,0,1,0" --shots --label crash-check `--locale de` is what matters: a bare language with no region, which is what Xcode's "Application Language" scheme option and UI-test runs produce. With `de_DE` the app does not crash. Something the fix revealed, filed separately -------------------------------------------- The attached screenshot after-survives-de-locale.png is the patched build, and it shows the next defect in the same area: the navigation title reads "Fr. abheben", so the app knows the currency, while every amount on the screen reads 0,00 XXX 5,00 XXX 10,00 XXX 25,00 XXX 50,00 XXX That is not caused by this patch -- it was simply unreachable before, because the app died first. Filed as its own issue rather than folded in here. Added by an agent (bot-cg) on behalf of Christian Grothoff. | |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-03 23:45 | bot-cg | New Issue | |
| 2026-11-03 23:45 | bot-cg | File Added: fix-currencycode-crash.patch | |
| 2026-11-04 02:01 | bot-cg | Note Added: 0030220 | |
| 2026-11-04 09:03 | bot-cg | File Added: after-survives-de-locale.png | |
| 2026-11-04 09:03 | bot-cg | Note Added: 0030231 | |
| 2026-11-04 13:02 | Christian Grothoff | Implementer | => MarcS |
| 2026-11-04 13:02 | Christian Grothoff | Reviewer | => Christian Grothoff |
| 2026-11-04 13:02 | Christian Grothoff | Release signed by | => MarcS |
| 2026-11-04 13:02 | Christian Grothoff | Deployed by | => MarcS |
| 2026-11-04 13:02 | Christian Grothoff | Verifier | => Christian Grothoff |
| 2026-11-04 13:02 | Christian Grothoff | Status | new => planning |
| 2026-11-04 13:02 | Christian Grothoff | Description Updated |