What changed, and why it matters
This commit fixes how Keystone 3 hardware wallets parse Avalanche (AVAX) transactions. Previously, the code only handled a single derivation path and a single sender address. The update supports multiple input addresses/paths, correctly matches displayed 'from' addresses to their derivation paths, and fixes a UI layout and memory cleanup issue. The main security risk is that a malformed or multi-input AVAX transaction could have been displayed misleadingly, potentially tricking a user into approving a transaction they did not intend to sign.
Treat this as a likely security-relevant bug fix. Review whether the previous single-address parsing could have caused misleading transaction confirmation for multi-input AVAX transactions. Verify the new multi-path derivation and address matching behave correctly across X-Chain, P-Chain, and C-Chain import/export transactions. Consider whether a security advisory or CVE is warranted if user funds could have been at risk.
Security signals we found
UI display of transaction senders changed from single address to multiple addresses
Derivation path matching logic changed from single-path to multi-path iteration
Memory deallocation for 'from' vector re-enabled after struct type change
UI layout constant changed from 60 to 65 pixels to accommodate multiple entries
Evidence from the diff
The patch refactors AVAX transaction parsing in rust/rust_c/src/avalanche/mod.rs and structs.rs. It changes the parser from using one derivation path and one derived address to collecting all derivation paths, deriving an address for each, and passing a Vec<(path, address)> into the display structures. DisplayTxAvaxData.from is changed from a single DisplayAvaxFromToInfo to a VecFFI
Changed components
rust/rust_c/src/avalanche/mod.rsrust/rust_c/src/avalanche/structs.rssrc/ui/gui_chain/multi/web3/gui_avax.cInspect captured patch +101 / −69
diff --git a/rust/rust_c/src/avalanche/mod.rs b/rust/rust_c/src/avalanche/mod.rs
index c933b05..29b330b 100644
--- a/rust/rust_c/src/avalanche/mod.rs
+++ b/rust/rust_c/src/avalanche/mod.rs
@@ -69,47 +69,70 @@ unsafe fn parse_transaction_by_type(
}
};
- // Get derivation path from sign_request (avoid borrowing from a temporary)
- let derivation_paths_vec = sign_request.get_derivation_path();
- if derivation_paths_vec.is_empty() {
+ // Build full derivation paths from sign_request.
+ let derivation_keypaths = sign_request.get_derivation_path();
+ if derivation_keypaths.is_empty() {
return TransactionParseResult::from(RustCError::InvalidData(
"invalid derivation path".to_string(),
))
.c_ptr();
}
- let derivation_path = &derivation_paths_vec[0];
- let full_path = match derivation_path.get_path() {
- Some(p) => format!("m/{}", p),
- None => {
- return TransactionParseResult::from(RustCError::InvalidData(
- "invalid derivation path".to_string(),
- ))
- .c_ptr()
+
+ let mut paths: Vec<String> = Vec::new();
+ for kp in derivation_keypaths.iter() {
+ match kp.get_path() {
+ Some(p) => paths.push(format!("m/{}", p)),
+ None => {
+ return TransactionParseResult::from(RustCError::InvalidData(
+ "invalid derivation path".to_string(),
+ ))
+ .c_ptr()
+ }
}
- };
+ }
- // Derive address by matching full_path with available keys
+ // Derive addresses by matching every full path with available keys.
+ let mut from_infos: Vec<(String, String)> = Vec::new();
let mut address = String::new();
- for key in recover_c_array(public_keys).iter() {
- let key_path = recover_c_char(key.path).to_lowercase();
- if full_path.starts_with(&key_path) {
- address = match key_path.as_str() {
- "m/44'/60'/0'" => app_ethereum::address::derive_address(
- full_path.as_str(),
- &recover_c_char(key.xpub),
- &key_path,
- )
- .unwrap_or("no address".to_string()),
- _ => app_avalanche::get_address(
- app_avalanche::network::Network::AvaxMainNet,
- full_path.as_str(),
- &recover_c_char(key.xpub),
- &key_path,
- )
- .unwrap_or("no address".to_string()),
- };
- break;
+ for full_path in paths.iter() {
+ let mut derived_address = "no address".to_string();
+ for key in recover_c_array(public_keys).iter() {
+ let key_path = recover_c_char(key.path).to_lowercase();
+ if full_path.starts_with(&key_path) {
+ derived_address = match key_path.as_str() {
+ "m/44'/60'/0'" => app_ethereum::address::derive_address(
+ full_path.as_str(),
+ &recover_c_char(key.xpub),
+ &key_path,
+ )
+ .unwrap_or("no address".to_string()),
+ _ => app_avalanche::get_address(
+ app_avalanche::network::Network::AvaxMainNet,
+ full_path.as_str(),
+ &recover_c_char(key.xpub),
+ &key_path,
+ )
+ .unwrap_or("no address".to_string()),
+ };
+
+ if derived_address != "no address" && address.is_empty() {
+ address = derived_address.clone();
+ }
+ break;
+ }
}
+ from_infos.push((full_path.clone(), derived_address));
+ }
+
+ if address.is_empty() {
+ address = "no address".to_string();
+ }
+
+ if from_infos.is_empty() {
+ return TransactionParseResult::from(RustCError::InvalidData(
+ "invalid derivation path".to_string(),
+ ))
+ .c_ptr();
}
// Helper macro: given a concrete tx type `$tx_type`, parse raw tx bytes (`tx_data`)
@@ -123,7 +146,7 @@ unsafe fn parse_transaction_by_type(
TransactionParseResult::success(
DisplayAvaxTx::from_tx_info(
parse_data,
- full_path.clone(),
+ from_infos.clone(),
address.clone(),
type_id,
)
@@ -139,7 +162,6 @@ unsafe fn parse_transaction_by_type(
})
};
}
-
match type_id {
TypeId::BaseTx => {
let header = get_avax_tx_header(tx_data.clone()).unwrap();
@@ -152,7 +174,7 @@ unsafe fn parse_transaction_by_type(
TransactionParseResult::success(
DisplayAvaxTx::from_tx_info(
parse_data,
- "".to_string(),
+ from_infos.clone(),
address.clone(),
type_id,
)
diff --git a/rust/rust_c/src/avalanche/structs.rs b/rust/rust_c/src/avalanche/structs.rs
index 1ef6361..39c2622 100644
--- a/rust/rust_c/src/avalanche/structs.rs
+++ b/rust/rust_c/src/avalanche/structs.rs
@@ -46,7 +46,7 @@ pub struct DisplayTxAvaxData {
amount: PtrString,
method: PtrT<DisplayAvaxMethodInfo>,
to: PtrT<VecFFI<DisplayAvaxFromToInfo>>,
- from: PtrT<DisplayAvaxFromToInfo>,
+ from: PtrT<VecFFI<DisplayAvaxFromToInfo>>,
}
impl_c_ptr!(DisplayTxAvaxData);
@@ -72,22 +72,25 @@ impl_c_ptr!(DisplayAvaxFromToInfo);
impl DisplayAvaxFromToInfo {
fn from_index(
value: &AvaxFromToInfo,
- from_path: &str,
- from_address: String,
+ from_infos: &[(String, String)],
type_id: TypeId,
) -> Self {
let address = value.address.first().unwrap().clone();
+ let matched_path = from_infos
+ .iter()
+ .find(|(_, from_address)| *from_address == address)
+ .map(|(from_path, _)| from_path.clone());
let is_change = match type_id {
TypeId::XchainImportTx
| TypeId::PchainImportTx
| TypeId::XchainExportTx
| TypeId::PchainExportTx => false,
- _ => address == from_address,
+ _ => matched_path.is_some(),
};
let path = if !is_change {
null_mut()
} else {
- convert_c_char(from_path.to_string())
+ matched_path.map_or(null_mut(), convert_c_char)
};
DisplayAvaxFromToInfo {
address: convert_c_char(address.clone()),
@@ -132,12 +135,12 @@ impl From<AvaxMethodInfo> for DisplayAvaxMethodInfo {
impl DisplayAvaxTx {
pub fn from_tx_info<T: AvaxTxInfo>(
value: T,
- from_path: String,
+ from_infos: Vec<(String, String)>,
from_address: String,
type_id: TypeId,
) -> Self {
DisplayAvaxTx {
- data: DisplayTxAvaxData::from_tx_info(value, from_path, from_address, type_id).c_ptr(),
+ data: DisplayTxAvaxData::from_tx_info(value, from_infos, from_address, type_id).c_ptr(),
}
}
}
@@ -145,21 +148,34 @@ impl DisplayAvaxTx {
impl DisplayTxAvaxData {
fn from_tx_info<T: AvaxTxInfo>(
value: T,
- from_path: String,
+ from_infos: Vec<(String, String)>,
from_address: String,
type_id: TypeId,
) -> Self {
+ let total_input_amount = format!(
+ "{} AVAX",
+ value.get_total_input_amount() as f64 / NAVAX_TO_AVAX_RATIO
+ );
+
+ let from = VecFFI::from(
+ from_infos
+ .iter()
+ .map(|(from_path, from_addr)| DisplayAvaxFromToInfo {
+ address: convert_c_char(from_addr.clone()),
+ amount: convert_c_char(total_input_amount.clone()),
+ path: if from_path.is_empty() {
+ null_mut()
+ } else {
+ convert_c_char(from_path.clone())
+ },
+ is_change: false,
+ })
+ .collect::<Vec<DisplayAvaxFromToInfo>>(),
+ )
+ .c_ptr();
+
DisplayTxAvaxData {
- from: DisplayAvaxFromToInfo {
- address: convert_c_char(from_address.clone()),
- amount: convert_c_char(format!(
- "{} AVAX",
- value.get_total_input_amount() as f64 / NAVAX_TO_AVAX_RATIO
- )),
- path: convert_c_char(from_path.clone()),
- is_change: false,
- }
- .c_ptr(),
+ from,
amount: convert_c_char(format!(
"{} AVAX",
value.get_output_amount(from_address.clone(), type_id) as f64 / NAVAX_TO_AVAX_RATIO
@@ -181,14 +197,7 @@ impl DisplayTxAvaxData {
value
.get_outputs_addresses()
.iter()
- .map(|v| {
- DisplayAvaxFromToInfo::from_index(
- v,
- &from_path,
- from_address.clone(),
- type_id,
- )
- })
+ .map(|v| DisplayAvaxFromToInfo::from_index(v, &from_infos, type_id))
.collect::<Vec<DisplayAvaxFromToInfo>>(),
)
.c_ptr(),
@@ -211,11 +220,12 @@ impl DisplayTxAvaxData {
impl Free for DisplayTxAvaxData {
unsafe fn free(&self) {
- // let x = Box::from_raw(self.from);
- // let ve = Vec::from_raw_parts(x.data, x.size, x.cap);
- // ve.iter().for_each(|v| {
- // v.free();
- // });
+ let x = Box::from_raw(self.from);
+ let ve = Vec::from_raw_parts(x.data, x.size, x.cap);
+ ve.iter().for_each(|v| {
+ v.free();
+ });
+
let x = Box::from_raw(self.to);
let ve = Vec::from_raw_parts(x.data, x.size, x.cap);
ve.iter().for_each(|v| {
diff --git a/src/ui/gui_chain/multi/web3/gui_avax.c b/src/ui/gui_chain/multi/web3/gui_avax.c
index 3101f4a..cc8d8d7 100644
--- a/src/ui/gui_chain/multi/web3/gui_avax.c
+++ b/src/ui/gui_chain/multi/web3/gui_avax.c
@@ -92,7 +92,7 @@ lv_obj_t *CreateTxOverviewFromTo(lv_obj_t *parent, void *from, int fromLen, void
for (int i = 0; i < fromLen; i++) {
lv_obj_t *label = GuiCreateIllustrateLabel(container, ptr[i].address);
lv_obj_set_width(label, 360);
- lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 54 + 60 * i);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 54 + 65 * i);
}
ptr = (DisplayUtxoFromTo *)to;
@@ -174,7 +174,7 @@ void GuiAvaxTxOverview(lv_obj_t *parent, void *totalData)
GuiAlignToPrevObj(container, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
}
- container = CreateTxOverviewFromTo(parent, txData->data->from, 1, txData->data->to->data, txData->data->to->size);
+ container = CreateTxOverviewFromTo(parent, txData->data->from->data, txData->data->to->size, txData->data->to->data, txData->data->to->size);
GuiAlignToPrevObj(container, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
lv_obj_update_layout(parent);
}
@@ -216,7 +216,7 @@ void GuiAvaxTxRawData(lv_obj_t *parent, void *totalData)
GuiAlignToPrevObj(container, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
}
- container = CreateTxDetailsFromTo(parent, "From", txData->data->from, 1);
+ container = CreateTxDetailsFromTo(parent, "From", txData->data->from->data, txData->data->from->size);
GuiAlignToPrevObj(container, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
container = CreateTxDetailsFromTo(parent, "To", txData->data->to->data, txData->data->to->size);
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.