Skip to content

Commit 0299c00

Browse files
authored
Merge pull request #175 from github/aneubeck-triage-rust-gems-security-alerts
Fix high-severity code scanning issues in geo_filters, hash-sorted-map, and bpe
2 parents 31331e8 + 1b5785c commit 0299c00

10 files changed

Lines changed: 581 additions & 95 deletions

File tree

‎crates/bpe/src/appendable_encoder.rs‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,10 @@ impl<'a> AppendableEncoder<'a> {
5656
});
5757
break;
5858
} else {
59-
let prev_token =
60-
unsafe { self.states.get_unchecked(new_range.start - 1).last_token };
59+
let prev = &self.states[new_range.start - 1];
60+
let prev_token = prev.last_token;
6161
if self.bpe.is_valid_token_pair(prev_token, new_token) {
62-
let prev_count =
63-
unsafe { self.states.get_unchecked(new_range.start - 1).count };
62+
let prev_count = prev.count;
6463
self.states.push(State {
6564
state,
6665
last_token: new_token,

‎crates/bpe/src/byte_pair_encoding.rs‎

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,101 @@ fn serialize_daac<S: Serializer>(
6363
s.serialize_bytes(&daac.serialize())
6464
}
6565

66+
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
67+
u32::from_le_bytes(
68+
bytes[offset..offset + 4]
69+
.try_into()
70+
.expect("slice of length 4"),
71+
)
72+
}
73+
74+
fn validate_daac_bytes(v: &[u8]) -> Result<(), &'static str> {
75+
const STATE_SIZE: usize = 12;
76+
const OUTPUT_SIZE: usize = 12;
77+
const MAX_OUTPUTS: u32 = 0x00ff_ffff;
78+
79+
if v.len() < 4 {
80+
return Err("truncated DoubleArrayAhoCorasick states length");
81+
}
82+
let states_len = read_u32_le(v, 0);
83+
if states_len < 256 || !states_len.is_multiple_of(256) {
84+
return Err("invalid DoubleArrayAhoCorasick states length");
85+
}
86+
let states_bytes_len = (states_len as usize)
87+
.checked_mul(STATE_SIZE)
88+
.ok_or("DoubleArrayAhoCorasick states byte length overflow")?;
89+
let outputs_offset = 4usize
90+
.checked_add(states_bytes_len)
91+
.ok_or("DoubleArrayAhoCorasick states offset overflow")?;
92+
let outputs_header_end = outputs_offset
93+
.checked_add(4)
94+
.ok_or("DoubleArrayAhoCorasick outputs header overflow")?;
95+
if v.len() < outputs_header_end {
96+
return Err("truncated DoubleArrayAhoCorasick states data");
97+
}
98+
99+
let outputs_len = read_u32_le(v, outputs_offset);
100+
if outputs_len > MAX_OUTPUTS {
101+
return Err("invalid DoubleArrayAhoCorasick outputs length");
102+
}
103+
let outputs_bytes_len = (outputs_len as usize)
104+
.checked_mul(OUTPUT_SIZE)
105+
.ok_or("DoubleArrayAhoCorasick outputs byte length overflow")?;
106+
let outputs_end = outputs_header_end
107+
.checked_add(outputs_bytes_len)
108+
.ok_or("DoubleArrayAhoCorasick outputs end overflow")?;
109+
let total_len = outputs_end
110+
.checked_add(5)
111+
.ok_or("DoubleArrayAhoCorasick total length overflow")?;
112+
if v.len() != total_len {
113+
return Err("unexpected DoubleArrayAhoCorasick byte length");
114+
}
115+
116+
let match_kind = v[outputs_end];
117+
if match_kind > 2 {
118+
return Err("invalid DoubleArrayAhoCorasick match kind");
119+
}
120+
let num_states = read_u32_le(v, outputs_end + 1);
121+
if num_states > states_len {
122+
return Err("invalid DoubleArrayAhoCorasick num_states");
123+
}
124+
125+
for chunk in v[4..outputs_offset].as_chunks::<STATE_SIZE>().0 {
126+
let base = read_u32_le(chunk, 0);
127+
let fail = read_u32_le(chunk, 4);
128+
let opos_ch = read_u32_le(chunk, 8);
129+
let output_pos = opos_ch >> 8;
130+
if base >= states_len || fail >= states_len || output_pos > outputs_len {
131+
return Err("DoubleArrayAhoCorasick state out of bounds");
132+
}
133+
}
134+
135+
for (i, chunk) in v[outputs_header_end..outputs_end]
136+
.as_chunks::<OUTPUT_SIZE>()
137+
.0
138+
.iter()
139+
.enumerate()
140+
{
141+
let length = read_u32_le(chunk, 4);
142+
let parent = read_u32_le(chunk, 8);
143+
if length == 0 || (parent as usize) > i {
144+
return Err("DoubleArrayAhoCorasick output out of bounds");
145+
}
146+
}
147+
148+
Ok(())
149+
}
150+
66151
struct DaacVisitor;
67152
impl Visitor<'_> for DaacVisitor {
68153
type Value = DoubleArrayAhoCorasick<u32>;
69154

70-
fn expecting(&self, _formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
71-
Err(std::fmt::Error)
155+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
156+
formatter.write_str("a serialized DoubleArrayAhoCorasick automaton")
72157
}
73158

74159
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
160+
validate_daac_bytes(v).map_err(E::custom)?;
75161
Ok(unsafe { DoubleArrayAhoCorasick::deserialize_unchecked(v).0 })
76162
}
77163
}
@@ -114,20 +200,20 @@ fn is_valid_token_pair(
114200
// Reverse the merge operation from BPE.
115201
if token1 > token2 {
116202
limit = token1;
117-
token1 = unsafe { split_table.get_unchecked(token1 as usize).1 };
203+
token1 = split_table[token1 as usize].1;
118204
if token1 == limit {
119205
limit = token2 + 1;
120-
token2 = unsafe { split_table.get_unchecked(token2 as usize).0 };
206+
token2 = split_table[token2 as usize].0;
121207
if token2 + 1 == limit {
122208
return true;
123209
}
124210
}
125211
} else {
126212
limit = token2 + 1;
127-
token2 = unsafe { split_table.get_unchecked(token2 as usize).0 };
213+
token2 = split_table[token2 as usize].0;
128214
if token2 + 1 == limit {
129215
limit = token1;
130-
token1 = unsafe { split_table.get_unchecked(token1 as usize).1 };
216+
token1 = split_table[token1 as usize].1;
131217
if token1 == limit {
132218
return true;
133219
}
@@ -137,10 +223,8 @@ fn is_valid_token_pair(
137223
}
138224

139225
fn token_range(token_starts: &[u32], token_id: u32) -> Range<usize> {
140-
unsafe {
141-
*token_starts.get_unchecked(token_id as usize) as usize
142-
..*token_starts.get_unchecked(token_id as usize + 1) as usize
143-
}
226+
let idx = token_id as usize;
227+
token_starts[idx] as usize..token_starts[idx + 1] as usize
144228
}
145229

146230
fn token_bytes<'a>(all_tokens: &'a [u8], token_starts: &[u32], token_id: u32) -> &'a [u8] {
@@ -404,7 +488,7 @@ impl BytePairEncoding {
404488
last_token.push(new_token);
405489
break;
406490
} else {
407-
let prev_token = unsafe { *last_token.get_unchecked(new_range.start - 1) };
491+
let prev_token = last_token[new_range.start - 1];
408492
if self.is_valid_token_pair(prev_token, new_token) {
409493
last_token.push(new_token);
410494
break;
@@ -694,7 +778,9 @@ pub fn create_test_bytes(bpe: &BytePairEncoding, min_bytes: usize) -> Vec<u8> {
694778

695779
#[cfg(test)]
696780
mod tests {
697-
use super::hash_bytes;
781+
use aneubeck_daachorse::DoubleArrayAhoCorasick;
782+
783+
use super::{hash_bytes, read_u32_le, validate_daac_bytes, BytePairEncoding};
698784

699785
/// The hash must not depend on the target. It keys the serialized `bytes_hash_to_token`,
700786
/// and a hardcoded hash factor is only collision-free for one particular mapping.
@@ -712,4 +798,46 @@ mod tests {
712798
// Tiktoken vocabularies contain arbitrary bytes, not just UTF-8.
713799
assert_eq!(hash_bytes(&[0, 0xff, 0x80], FACTOR), 3276605559);
714800
}
801+
802+
#[test]
803+
#[should_panic]
804+
fn token_bytes_out_of_bounds_panics() {
805+
let tokens: Vec<Vec<u8>> = (0..=255u8).map(|b| vec![b]).collect();
806+
let bpe = BytePairEncoding::from_dictionary(tokens, None);
807+
let _ = bpe.token_bytes(999);
808+
}
809+
810+
#[test]
811+
fn validate_daac_bytes_checks_invariants() {
812+
let pma =
813+
DoubleArrayAhoCorasick::<u32>::new(vec!["a", "ab", "bab"]).expect("valid patterns");
814+
let valid = pma.serialize();
815+
assert!(validate_daac_bytes(&valid).is_ok());
816+
817+
// Truncated input
818+
assert!(validate_daac_bytes(&valid[..valid.len() - 1]).is_err());
819+
820+
// Non-multiple-of-256 states_len
821+
let mut bad_states_len = valid.clone();
822+
bad_states_len[0..4].copy_from_slice(&255u32.to_le_bytes());
823+
assert!(validate_daac_bytes(&bad_states_len).is_err());
824+
825+
// Out-of-bounds state `base`
826+
let mut bad_base = valid.clone();
827+
bad_base[4..8].copy_from_slice(&9999u32.to_le_bytes());
828+
assert!(validate_daac_bytes(&bad_base).is_err());
829+
830+
// Out-of-bounds state `fail`
831+
let mut bad_fail = valid.clone();
832+
bad_fail[8..12].copy_from_slice(&9999u32.to_le_bytes());
833+
assert!(validate_daac_bytes(&bad_fail).is_err());
834+
835+
// Out-of-bounds output `parent` (self-referential 1-based parent == 1 at index 0)
836+
let states_len = read_u32_le(&valid, 0) as usize;
837+
let first_output_parent_offset = 4 + states_len * 12 + 4 + 8;
838+
let mut bad_parent = valid.clone();
839+
bad_parent[first_output_parent_offset..first_output_parent_offset + 4]
840+
.copy_from_slice(&1u32.to_le_bytes());
841+
assert!(validate_daac_bytes(&bad_parent).is_err());
842+
}
715843
}

‎crates/bpe/src/prependable_encoder.rs‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,10 @@ impl<'a> PrependableEncoder<'a> {
5656
});
5757
break;
5858
} else {
59-
let next_token =
60-
unsafe { self.states.get_unchecked(new_range.start - 1).prev_token };
59+
let prev = &self.states[new_range.start - 1];
60+
let next_token = prev.prev_token;
6161
if self.bpe.is_valid_token_pair(new_token, next_token) {
62-
let prev_count =
63-
unsafe { self.states.get_unchecked(new_range.start - 1).count };
62+
let prev_count = prev.count;
6463
self.states.push(State {
6564
state,
6665
prev_token: new_token,

‎crates/geo_filters/src/config.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ mod bitchunks;
88
mod buckets;
99
mod estimation;
1010
mod lookup;
11+
mod unaligned;
1112

1213
pub(crate) use bitchunks::*;
1314
pub(crate) use buckets::*;
1415
pub(crate) use estimation::*;
1516
pub(crate) use lookup::*;
1617
use once_cell::sync::Lazy;
18+
pub(crate) use unaligned::*;
1719

1820
/// Trait that configures geo filters and determines counting precision.
1921
///

0 commit comments

Comments
 (0)