Skip to content

Commit da79bc1

Browse files
authored
App Improvement (#61)
* fix: prevent concurrent APDU processing during UI review Introduce a `review_pending` flag to block new commands while the device is waiting for user approval. This prevents the host from mutating state or buffers during an asynchronous review window, addressing potential race conditions on transports like BLE. * improvement: update zemu tests configuration and dependencies Update Jest configuration to handle ESM-only dependencies like `get-port` and bump multiple devDependencies (including Zemu, Ledgerhq, and ESLint plugins) to their latest versions. * test: update zemu snapshots Update the main menu UI snapshots for various device models (Nano S, Nano S+, Nano X, Stax, Flex) to reflect recent changes. * refactor: centralize review lock management in main dispatcher Move the `review_pending` flag logic from individual command handlers into the central APDU dispatcher. This ensures the lock is consistently acquired at the start of processing and automatically released upon any command completion (success or error) or transport reset, reducing the risk of the dispatcher becoming permanently locked.
1 parent 6a5e172 commit da79bc1

57 files changed

Lines changed: 803 additions & 519 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ GIT_DESCRIBE=$(shell git describe --tags --abbrev=8 --always --long --dirty 2>/d
3737
VERSION_TAG=$(shell echo $(GIT_DESCRIBE) | sed 's/^v//g')
3838
APPVERSION_M=1
3939
APPVERSION_N=6
40-
APPVERSION_P=3
40+
APPVERSION_P=4
4141
APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
4242
APPNAME = "Mina"
4343

src/globals.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ unsigned int ux_step;
66
unsigned int ux_step_count;
77
const internalStorage_t N_storage_real;
88

9+
bool review_pending = false;
10+
911
void sendResponse(uint8_t tx, bool approve) {
12+
review_pending = false;
1013
G_io_apdu_buffer[tx++] = approve? 0x90 : 0x69;
1114
G_io_apdu_buffer[tx++] = approve? 0x00 : 0x85;
1215
// Send back the response, do not restart the event loop

src/globals.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <os.h>
44
#include <ux.h>
55
#include <os_io_seproxyhal.h>
6+
#include <stdbool.h>
67

78
#define P1_FIRST 0x00
89
#define P1_MORE 0x80
@@ -21,6 +22,12 @@ typedef struct internalStorage_t {
2122
extern const internalStorage_t N_storage_real;
2223
#define N_storage (*(volatile internalStorage_t*) PIC(&N_storage_real))
2324

25+
// True while a user-confirmation UI flow is open and the device has
26+
// deferred its APDU reply with IO_ASYNCH_REPLY. Any new APDU arriving
27+
// in this state must be rejected so the host cannot mutate the bytes
28+
// being reviewed.
29+
extern bool review_pending;
30+
2431
void sendResponse(uint8_t tx, bool approve);
2532

2633
// type userid x y w h str rad fill fg bg fid iid txt touchparams... ]

src/main.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ void handleApdu(volatile unsigned int *flags, volatile unsigned int *tx,
4848
THROW(0x6E00);
4949
}
5050

51+
// Refuse any new command while a user-confirmation screen is
52+
// still waiting for approval. Without this, a second APDU can
53+
// race into handler state (globals, review buffers) during the
54+
// asynchronous review window — exploitable on transports that
55+
// do not gate new APDUs at the SDK layer (BLE).
56+
if (review_pending) {
57+
THROW(0x6985);
58+
}
59+
// An APDU is now in flight. Non-review commands release this
60+
// lock when their response is emitted (sendResponse or the
61+
// THROW handled in app_main's CATCH_OTHER). Review-based
62+
// commands keep it until the user approves/rejects.
63+
review_pending = true;
64+
5165
// Check user supplied command data length against the actual
5266
// length to make sure it's not a lie!
5367
uint8_t dataLength = G_io_apdu_buffer[OFFSET_LC];
@@ -172,6 +186,11 @@ void app_main(void) {
172186
handleApdu(&flags, &tx, rx);
173187
}
174188
CATCH(EXCEPTION_IO_RESET) {
189+
// Transport went away mid-review (BLE disconnect, USB
190+
// replug). The deferred reply will never arrive, so drop
191+
// the lock before we unwind — otherwise the dispatcher
192+
// stays frozen after re-init.
193+
review_pending = false;
175194
THROW(EXCEPTION_IO_RESET);
176195
}
177196
CATCH_OTHER(e) {
@@ -191,6 +210,11 @@ void app_main(void) {
191210
if (e != APDU_CODE_OK) {
192211
flags &= ~IO_ASYNCH_REPLY;
193212
}
213+
// Any THROW exits the APDU — whether a success code
214+
// (e.g. GET_CONF) or an error. Handlers that genuinely
215+
// own a review return normally and keep the lock; we
216+
// only reach here when the APDU is done.
217+
review_pending = false;
194218
// Unexpected exception => report
195219
G_io_apdu_buffer[tx] = sw >> 8;
196220
G_io_apdu_buffer[tx + 1] = sw;

tests_zemu/jest.config.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1+
// ESM-only dependencies that must be transpiled to CommonJS before Jest
2+
// can require() them. `get-port` went ESM-only in v6; @zondax/zemu still
3+
// require()s it. Add more names here if another transitive dep ships ESM.
4+
const esmDeps = ['get-port']
5+
16
module.exports = {
27
preset: 'ts-jest',
38
testEnvironment: 'node',
4-
transformIgnorePatterns: ['^.+\\.js$'],
9+
transform: {
10+
'^.+\\.tsx?$': 'ts-jest',
11+
'^.+\\.m?js$': [
12+
'ts-jest',
13+
{
14+
useESM: false,
15+
isolatedModules: true,
16+
tsconfig: {
17+
allowJs: true,
18+
esModuleInterop: true,
19+
module: 'CommonJS',
20+
target: 'ES2020',
21+
},
22+
},
23+
],
24+
},
25+
transformIgnorePatterns: [`/node_modules/(?!(${esmDeps.join('|')})/)`],
526
reporters: ['default', ['summary', { summaryThreshold: 1 }]],
627
globalSetup: './globalsetup.js',
728
testPathIgnorePatterns: ['/node_modules/', '/dist/'],

tests_zemu/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,34 +27,34 @@
2727
"dependencies": {
2828
"@mina-wallet-adapter/mina-ledger-js": "^1.0.9",
2929
"@zondax/ledger-mina-js": "^0.2.0",
30-
"@zondax/zemu": "^0.65.0",
30+
"@zondax/zemu": "^0.66.2",
3131
"bs58check": "^4.0.0",
3232
"mina-signer": "^3.1.0",
3333
"o1js": "^2.2.0"
3434
},
3535
"devDependencies": {
36-
"@ledgerhq/hw-transport-node-hid": "^6.29.13",
37-
"@ledgerhq/logs": "^6.13.0",
38-
"@noble/curves": "^2.0.1",
36+
"@ledgerhq/hw-transport-node-hid": "^6.33.0",
37+
"@ledgerhq/logs": "^6.17.0",
38+
"@noble/curves": "^2.2.0",
3939
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
4040
"@types/jest": "^30.0.0",
4141
"@types/ledgerhq__hw-transport": "^6.0.0",
42-
"@typescript-eslint/eslint-plugin": "^8.46.1",
43-
"@typescript-eslint/parser": "^8.46.1",
42+
"@typescript-eslint/eslint-plugin": "^8.58.2",
43+
"@typescript-eslint/parser": "^8.58.2",
4444
"blakejs": "^1.2.1",
4545
"crypto-js": "4.2.0",
4646
"eslint": "^9.37.0",
4747
"eslint-config-prettier": "^10.1.8",
4848
"eslint-plugin-import": "^2.32.0",
49-
"eslint-plugin-jest": "^29.0.1",
50-
"eslint-plugin-prettier": "^5.5.4",
49+
"eslint-plugin-jest": "^29.15.2",
50+
"eslint-plugin-prettier": "^5.5.5",
5151
"eslint-plugin-promise": "^7.2.1",
5252
"eslint-plugin-tsdoc": "^0.4.0",
53-
"eslint-plugin-unused-imports": "^4.2.0",
54-
"jest": "^30.2.0",
53+
"eslint-plugin-unused-imports": "^4.4.1",
54+
"jest": "^30.3.0",
5555
"jssha": "^3.3.1",
56-
"prettier": "^3.6.2",
57-
"ts-jest": "^29.4.5",
56+
"prettier": "^3.8.3",
57+
"ts-jest": "^29.4.9",
5858
"ts-node": "^10.9.2",
5959
"typescript": "^5.9.3"
6060
}
-3.06 KB
Binary file not shown.
-3.06 KB
Binary file not shown.
-11 Bytes
-5.26 KB
Binary file not shown.

0 commit comments

Comments
 (0)