@@ -310,15 +310,18 @@ impl<'data> super::Extractor for Extractor<'data> {
310310 if instr. is_mov_imm_to_stack ( ) ? {
311311 count += instr. get_printable_len ( ) ?;
312312 }
313- if count > 8 {
314- //MIN_STACKSTRING_LEN
315- res. push ( (
316- crate :: rules:: features:: Feature :: Characteristic (
317- crate :: rules:: features:: CharacteristicFeature :: new ( "stack string" , "" ) ?,
318- ) ,
319- * bb. 0 ,
320- ) ) ;
321- }
313+ }
314+ if count > 8 {
315+ //MIN_STACKSTRING_LEN
316+ // Emitted once per basic block — previously the push sat
317+ // inside the loop with no `break`, so every instruction
318+ // past the threshold added a duplicate (#24).
319+ res. push ( (
320+ crate :: rules:: features:: Feature :: Characteristic (
321+ crate :: rules:: features:: CharacteristicFeature :: new ( "stack string" , "" ) ?,
322+ ) ,
323+ * bb. 0 ,
324+ ) ) ;
322325 }
323326 Ok ( res)
324327 }
@@ -1263,7 +1266,16 @@ impl<'data> Extractor<'data> {
12631266
12641267 // case 2: if operand is like 1234h
12651268 if let Some ( stripped_operand) = operand. strip_suffix ( 'h' ) {
1266- return i128:: from_str_radix ( stripped_operand, 16 ) . ok ( ) ;
1269+ // Intel convention: an h-suffixed hex literal must start
1270+ // with a digit (0ABh). Without this check the register
1271+ // names ah/bh/ch/dh parsed as 0xA/0xB/0xC/0xD (#24).
1272+ if stripped_operand
1273+ . chars ( )
1274+ . next ( )
1275+ . is_some_and ( |c| c. is_ascii_digit ( ) )
1276+ {
1277+ return i128:: from_str_radix ( stripped_operand, 16 ) . ok ( ) ;
1278+ }
12671279 }
12681280
12691281 // case 3: if operand is like +0x1234
@@ -1288,8 +1300,13 @@ impl<'data> Extractor<'data> {
12881300 return Some ( val) ;
12891301 }
12901302
1291- // case 5: if operand is like 0x1234
1292- i128:: from_str_radix ( operand, 16 ) . ok ( )
1303+ // case 5: bare hex without 0x/h, e.g. 0dead. Must start with a
1304+ // digit — otherwise hex-looking labels (beef, face, add) parse
1305+ // as numbers (#24).
1306+ if operand. chars ( ) . next ( ) . is_some_and ( |c| c. is_ascii_digit ( ) ) {
1307+ return i128:: from_str_radix ( operand, 16 ) . ok ( ) ;
1308+ }
1309+ None
12931310 }
12941311
12951312 pub fn extract_insn_number_features (
@@ -1317,7 +1334,12 @@ impl<'data> Extractor<'data> {
13171334 insn. offset ,
13181335 ) ) ;
13191336 } else {
1320- let masked_value = ( s as u32 ) as i128 ; // Convierte a u32 y de vuelta a i128
1337+ // Negative immediates are emitted as their
1338+ // unsigned interpretation at the function's
1339+ // bitness — masking to u32 truncated x64 values
1340+ // (mov rax, -1 → 0xFFFFFFFF instead of
1341+ // 0xFFFFFFFFFFFFFFFF) (#24).
1342+ let masked_value = mask_to_bitness ( s, f. bitness ) ;
13211343 res. push ( (
13221344 crate :: rules:: features:: Feature :: Number (
13231345 crate :: rules:: features:: NumberFeature :: new (
@@ -1570,6 +1592,16 @@ pub fn generate_symbols(dll: &Option<String>, symbol: &Option<String>) -> Result
15701592 Ok ( res)
15711593}
15721594
1595+ /// Unsigned interpretation of a negative immediate at the given bitness
1596+ /// (#24): `-1` is `0xFFFFFFFF` on 32-bit and `0xFFFFFFFFFFFFFFFF` on
1597+ /// 64-bit. Previously the mask was always u32, truncating x64 values.
1598+ fn mask_to_bitness ( value : i128 , bitness : u32 ) -> i128 {
1599+ match bitness {
1600+ 64 => ( value as u64 ) as i128 ,
1601+ _ => ( value as u32 ) as i128 ,
1602+ }
1603+ }
1604+
15731605pub fn derefs ( report : & DisassemblyReport < ' _ > , p : & u64 ) -> Result < Vec < u64 > > {
15741606 let mut res = vec ! [ ] ;
15751607 let mut depth = 0 ;
@@ -1773,7 +1805,6 @@ pub fn extract_unicode_strings(data: &[u8], min_length: usize) -> Result<Vec<(St
17731805 // regex pattern for UTF-16LE and UTF-16BE
17741806 let re_le = regex:: bytes:: Regex :: new ( & format ! ( r"((?:[\x20-\x7E]\x00){{{},}})" , min_length) ) ?;
17751807 let re_be = regex:: bytes:: Regex :: new ( & format ! ( r"((?:\x00[\x20-\x7E]){{{},}})" , min_length) ) ?;
1776- let re_utf8 = regex:: bytes:: Regex :: new ( & format ! ( r"((?:[\x20-\x7E]){{{},}})" , min_length) ) ?;
17771808
17781809 // UTF-16LE
17791810 for mat in re_le. find_iter ( data) {
@@ -1799,12 +1830,11 @@ pub fn extract_unicode_strings(data: &[u8], min_length: usize) -> Result<Vec<(St
17991830 }
18001831 }
18011832
1802- // UTF-8
1803- for mat in re_utf8. find_iter ( data) {
1804- let matched_bytes = mat. as_bytes ( ) ;
1805- let decoded_string = String :: from_utf8_lossy ( matched_bytes) . to_string ( ) ;
1806- results. push ( ( decoded_string, mat. start ( ) as u64 ) ) ;
1807- }
1833+ // NOTE (#24): there used to be a third, plain-ASCII pass here
1834+ // (`[\x20-\x7E]{4,}`) — it duplicated `extract_ascii_strings`,
1835+ // which `extract_file_strings` runs alongside this function, so
1836+ // every ASCII string was emitted twice. ASCII stays with
1837+ // `extract_ascii_strings`; this function is UTF-16 only.
18081838
18091839 let cleaned_results = results
18101840 . into_iter ( )
@@ -2045,4 +2075,54 @@ mod tests {
20452075 ) ;
20462076 eprintln ! ( "upstream parity #2997: verified {sites_checked} self-XOR site(s) in {path}" ) ;
20472077 }
2078+
2079+ /// #24: register names (ah/bh/ch/dh) and hex-looking labels must not
2080+ /// parse as numbers; h-suffixed and bare hex literals starting with
2081+ /// a digit still do.
2082+ #[ test]
2083+ fn registers_and_labels_are_not_numbers ( ) {
2084+ let data = vec ! [ 0u8 ; 0x10 ] ;
2085+ let extractor =
2086+ Extractor :: from_buffer ( & data, 0x1000 , 64 , false , false ) . expect ( "parse buffer" ) ;
2087+ // Pre-#24 these parsed as 0xA / 0xB / 0xC / 0xD via the 'h' strip.
2088+ for reg in [ "ah" , "bh" , "ch" , "dh" ] {
2089+ assert_eq ! ( extractor. parse_operand_to_number( reg) , None , "{reg}" ) ;
2090+ }
2091+ // Hex-looking labels parsed via the bare-hex fallback.
2092+ for label in [ "beef" , "face" , "add" ] {
2093+ assert_eq ! ( extractor. parse_operand_to_number( label) , None , "{label}" ) ;
2094+ }
2095+ // Legitimate literals keep working.
2096+ assert_eq ! ( extractor. parse_operand_to_number( "0ABh" ) , Some ( 0xAB ) ) ;
2097+ assert_eq ! ( extractor. parse_operand_to_number( "1234h" ) , Some ( 0x1234 ) ) ;
2098+ assert_eq ! ( extractor. parse_operand_to_number( "0dead" ) , Some ( 0xdead ) ) ;
2099+ assert_eq ! ( extractor. parse_operand_to_number( "0x1234" ) , Some ( 0x1234 ) ) ;
2100+ assert_eq ! ( extractor. parse_operand_to_number( "1234" ) , Some ( 1234 ) ) ;
2101+ }
2102+
2103+ /// #24: negative immediates are emitted as their unsigned
2104+ /// interpretation at the function's bitness (was always u32).
2105+ #[ test]
2106+ fn negative_immediates_mask_at_bitness ( ) {
2107+ assert_eq ! ( mask_to_bitness( -1 , 32 ) , 0xFFFF_FFFF ) ;
2108+ assert_eq ! ( mask_to_bitness( -1 , 64 ) , 0xFFFF_FFFF_FFFF_FFFF ) ;
2109+ }
2110+
2111+ /// #24: the UTF-16 extractor must not also emit plain ASCII strings —
2112+ /// `extract_file_strings` runs `extract_ascii_strings` alongside it,
2113+ /// so every ASCII string used to appear twice.
2114+ #[ test]
2115+ fn unicode_extractor_does_not_duplicate_ascii ( ) {
2116+ let data = b"HELLO WORLD" ;
2117+ assert ! (
2118+ extract_unicode_strings( data, 4 )
2119+ . expect( "unicode" )
2120+ . is_empty( ) ,
2121+ "ASCII string leaked into the UTF-16 extractor"
2122+ ) ;
2123+ assert_eq ! (
2124+ extract_ascii_strings( data, 4 ) . expect( "ascii" ) [ 0 ] . 0 ,
2125+ "HELLO WORLD"
2126+ ) ;
2127+ }
20482128}
0 commit comments