fs_ext4/hash.rs
1//! Directory-index (htree) name hashes.
2//!
3//! An indexed ext4 directory orders its entries by a 32-bit hash of each
4//! name (kernel.org ext4 documentation, "Hash Tree Directories"). The
5//! `dx_root` block records which of three algorithms the directory uses:
6//!
7//! * **legacy** — a small multiplicative/xor recurrence over the name's
8//! bytes, producing only a major hash;
9//! * **half MD4** — the three rounds of MD4 (RFC 1320) applied to 32-byte
10//! blocks of the name, run as a chained compression function whose
11//! chaining value starts at the filesystem's hash seed;
12//! * **TEA** — the Tiny Encryption Algorithm (Wheeler & Needham, FSE 1994)
13//! used the same way over 16-byte blocks, with the name block as the
14//! cipher key and the chaining value as the plaintext.
15//!
16//! Each algorithm comes in a "signed" and an "unsigned" flavour, which
17//! differ only in whether a name byte >= 0x80 is widened to 32 bits with or
18//! without sign extension. For pure-ASCII names the two agree.
19//!
20//! The byte-level format facts below (padding word, packing order, block
21//! sizes, which chaining words become major/minor, the legacy constants)
22//! were taken from the BSD-licensed FreeBSD and lwext4 descriptions of this
23//! format and confirmed against `debugfs dx_hash` from e2fsprogs; see
24//! `tests/htree_hash_vectors.rs` and `tests/htree_hash_differential.rs`.
25
26/// Hash versions as recorded in `dx_root_info.hash_version` (kernel.org ext4
27/// documentation, "Hash Tree Directories").
28#[repr(u8)]
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum HashVersion {
31 Legacy = 0,
32 HalfMd4 = 1,
33 Tea = 2,
34 LegacyUnsigned = 3,
35 HalfMd4Unsigned = 4,
36 TeaUnsigned = 5,
37}
38
39impl HashVersion {
40 /// Map an on-disk hash version byte to a supported version.
41 pub fn from_u8(v: u8) -> Option<Self> {
42 Some(match v {
43 0 => Self::Legacy,
44 1 => Self::HalfMd4,
45 2 => Self::Tea,
46 3 => Self::LegacyUnsigned,
47 4 => Self::HalfMd4Unsigned,
48 5 => Self::TeaUnsigned,
49 _ => return None,
50 })
51 }
52
53 /// True for the three "unsigned" variants (codes 3, 4, 5).
54 pub fn is_unsigned(self) -> bool {
55 matches!(
56 self,
57 Self::LegacyUnsigned | Self::HalfMd4Unsigned | Self::TeaUnsigned
58 )
59 }
60
61 /// How this version widens a name byte into a 32-bit quantity.
62 fn byte_widening(self) -> ByteWidening {
63 if self.is_unsigned() {
64 ByteWidening::ZeroExtend
65 } else {
66 ByteWidening::SignExtend
67 }
68 }
69}
70
71/// Result of [`name_hash`]: `major` orders entries in the index, `minor`
72/// breaks ties between names whose major hashes collide.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct NameHash {
75 pub major: u32,
76 pub minor: u32,
77}
78
79/// Hash `name` for a directory index using `version` and the superblock's
80/// `s_hash_seed` (as four little-endian 32-bit words).
81pub fn name_hash(name: &[u8], version: HashVersion, seed: &[u32; 4]) -> NameHash {
82 let widening = version.byte_widening();
83 let (major, minor) = match version {
84 HashVersion::Legacy | HashVersion::LegacyUnsigned => {
85 (LegacyRecurrence::digest(name, widening), 0)
86 }
87 HashVersion::HalfMd4 | HashVersion::HalfMd4Unsigned => {
88 chain_blocks::<8, Md4HalfRounds>(name, widening, seed)
89 }
90 HashVersion::Tea | HashVersion::TeaUnsigned => {
91 chain_blocks::<4, TeaCipher>(name, widening, seed)
92 }
93 };
94 // The lowest bit of a major hash is never part of the value (every
95 // `debugfs dx_hash` major is even, for every version): index entries
96 // keep that bit free for their own bookkeeping.
97 let mut major = major & !1;
98 // 0xFFFFFFFE is reserved: the directory index uses it as its
99 // end-of-directory marker, so no name may hash to it. A name that
100 // would is given the next even value down, 0xFFFFFFFC, as both BSD
101 // references do; that is what directories written by other ext4
102 // implementations contain. (`debugfs dx_hash` prints the value before
103 // this remap, which `tests/htree_hash_differential.rs` accounts for.)
104 if major == END_OF_DIRECTORY_MAJOR {
105 major = END_OF_DIRECTORY_MAJOR - 2;
106 }
107 NameHash { major, minor }
108}
109
110/// The major hash value the directory index reserves to mean "end of
111/// directory". No name is ever given it (see [`name_hash`]).
112const END_OF_DIRECTORY_MAJOR: u32 = 0xFFFF_FFFE;
113
114/// The version to use for a directory whose `dx_root` records
115/// `root_version`, given the superblock's `EXT2_FLAGS_UNSIGNED_HASH` flag.
116/// `dx_root` only ever records the signed codes (0, 1, 2); when the flag is
117/// set, the matching unsigned variant (3, 4, 5) applies instead.
118pub fn effective_version(root_version: HashVersion, unsigned_hash: bool) -> HashVersion {
119 if !unsigned_hash {
120 return root_version;
121 }
122 match root_version {
123 HashVersion::Legacy => HashVersion::LegacyUnsigned,
124 HashVersion::HalfMd4 => HashVersion::HalfMd4Unsigned,
125 HashVersion::Tea => HashVersion::TeaUnsigned,
126 already_unsigned => already_unsigned,
127 }
128}
129
130// ---------------------------------------------------------------------------
131// Turning name bytes into 32-bit words
132// ---------------------------------------------------------------------------
133
134/// Whether a name byte is treated as a signed or an unsigned `char` when it
135/// is widened to 32 bits. This is the only difference between the signed
136/// and unsigned hash versions.
137#[derive(Debug, Clone, Copy)]
138enum ByteWidening {
139 SignExtend,
140 ZeroExtend,
141}
142
143impl ByteWidening {
144 fn widen(self, byte: u8) -> u32 {
145 match self {
146 // 0x80..=0xFF become 0xFFFFFF80..=0xFFFFFFFF.
147 ByteWidening::SignExtend => byte as i8 as i32 as u32,
148 ByteWidening::ZeroExtend => u32::from(byte),
149 }
150 }
151}
152
153/// Splits a name into fixed-size blocks of `N` words for the block-based
154/// algorithms (half MD4: 8 words, TEA: 4 words).
155///
156/// Each block is built from the bytes still unconsumed at that point:
157///
158/// * A "fill word" repeats the low byte of the number of bytes still
159/// unconsumed (the whole remaining tail, not just this block's share)
160/// in all four byte lanes.
161/// * Each word takes up to four consecutive name bytes. It starts as the
162/// fill word, and each byte in turn is appended at the bottom: the word
163/// shifts left by 8 and the widened byte is *added* (with wrapping). With
164/// four bytes the fill word is shifted out completely and the first byte
165/// ends up most significant; with fewer, the top lanes keep fill bytes.
166/// Under sign extension, adding a byte >= 0x80 borrows from the lanes
167/// above it, which is what makes the signed variants differ.
168/// * Words past the end of the name are the plain fill word.
169///
170/// A block consumes up to `4 * N` bytes. An empty name yields no blocks
171/// at all, so the hash is the seed state untouched (confirmed with
172/// `debugfs dx_hash` on the empty name).
173struct WordFeeder<'a, const N: usize> {
174 rest: &'a [u8],
175 widening: ByteWidening,
176}
177
178impl<'a, const N: usize> WordFeeder<'a, N> {
179 const BLOCK_BYTES: usize = 4 * N;
180
181 fn new(name: &'a [u8], widening: ByteWidening) -> Self {
182 Self {
183 rest: name,
184 widening,
185 }
186 }
187
188 fn fill_word(remaining: usize) -> u32 {
189 let r = remaining as u32;
190 r | (r << 8) | (r << 16) | (r << 24)
191 }
192
193 fn pack(&self, bytes: &[u8], fill: u32) -> u32 {
194 bytes.iter().fold(fill, |word, &b| {
195 (word << 8).wrapping_add(self.widening.widen(b))
196 })
197 }
198}
199
200impl<const N: usize> Iterator for WordFeeder<'_, N> {
201 type Item = [u32; N];
202
203 fn next(&mut self) -> Option<[u32; N]> {
204 if self.rest.is_empty() {
205 return None;
206 }
207 let fill = Self::fill_word(self.rest.len());
208 let take = self.rest.len().min(Self::BLOCK_BYTES);
209 let (this_block, later) = self.rest.split_at(take);
210 let mut block = [fill; N];
211 for (word, bytes) in block.iter_mut().zip(this_block.chunks(4)) {
212 *word = self.pack(bytes, fill);
213 }
214 self.rest = later;
215 Some(block)
216 }
217}
218
219// ---------------------------------------------------------------------------
220// Block mixers and the loop that drives them
221// ---------------------------------------------------------------------------
222
223/// A compression step that folds one block of name words into a 128-bit
224/// chaining state.
225trait BlockMixer<const N: usize> {
226 /// Fold `block` into `state`.
227 fn absorb(state: &mut [u32; 4], block: &[u32; N]);
228 /// Which chaining words are reported as (major, minor).
229 fn report(state: &[u32; 4]) -> (u32, u32);
230}
231
232/// The initial chaining values from RFC 1320 section 3.3 (words A, B, C, D).
233/// They are used whenever the superblock's hash seed is entirely zero —
234/// an unset seed — which `debugfs dx_hash -s 00000000-...` confirms.
235/// A seed with any non-zero word is used exactly as given.
236const RFC1320_INITIAL_STATE: [u32; 4] = [0x6745_2301, 0xEFCD_AB89, 0x98BA_DCFE, 0x1032_5476];
237
238fn starting_state(seed: &[u32; 4]) -> [u32; 4] {
239 if seed.iter().all(|&w| w == 0) {
240 RFC1320_INITIAL_STATE
241 } else {
242 *seed
243 }
244}
245
246/// Feed every `N`-word block of `name` through mixer `M`, starting from
247/// the seed, and report the mixer's (major, minor) words.
248fn chain_blocks<const N: usize, M: BlockMixer<N>>(
249 name: &[u8],
250 widening: ByteWidening,
251 seed: &[u32; 4],
252) -> (u32, u32) {
253 let mut state = starting_state(seed);
254 for block in WordFeeder::<N>::new(name, widening) {
255 M::absorb(&mut state, &block);
256 }
257 M::report(&state)
258}
259
260// --- Half MD4 --------------------------------------------------------------
261
262/// MD4's three rounds (RFC 1320 section 3.4) cut down to an 8-word block:
263/// each round makes two passes of four steps instead of four passes, and
264/// the result is added back into the chaining state (feed-forward), as in
265/// MD4. Major is chaining word B, minor is word C.
266struct Md4HalfRounds;
267
268/// RFC 1320 round 1 auxiliary function: bitwise "if X then Y else Z".
269fn rfc1320_f(x: u32, y: u32, z: u32) -> u32 {
270 (x & y) | (!x & z)
271}
272
273/// RFC 1320 round 2 auxiliary function: bitwise majority of X, Y, Z.
274fn rfc1320_g(x: u32, y: u32, z: u32) -> u32 {
275 (x & y) | (x & z) | (y & z)
276}
277
278/// RFC 1320 round 3 auxiliary function: bitwise parity.
279fn rfc1320_h(x: u32, y: u32, z: u32) -> u32 {
280 x ^ y ^ z
281}
282
283/// RFC 1320 additive constants for rounds 2 and 3 (round 1 adds none).
284const RFC1320_ROUND2_CONSTANT: u32 = 0x5A82_7999;
285const RFC1320_ROUND3_CONSTANT: u32 = 0x6ED9_EBA1;
286
287/// One MD4 round over an 8-word block: which word each of the 8 steps
288/// reads and how far it rotates.
289struct Md4Round {
290 mix: fn(u32, u32, u32) -> u32,
291 constant: u32,
292 words: [usize; 8],
293 rotations: [u32; 4],
294}
295
296const HALF_MD4_ROUNDS: [Md4Round; 3] = [
297 Md4Round {
298 mix: rfc1320_f,
299 constant: 0,
300 words: [0, 1, 2, 3, 4, 5, 6, 7],
301 rotations: [3, 7, 11, 19],
302 },
303 Md4Round {
304 mix: rfc1320_g,
305 constant: RFC1320_ROUND2_CONSTANT,
306 words: [1, 3, 5, 7, 0, 2, 4, 6],
307 rotations: [3, 5, 9, 13],
308 },
309 Md4Round {
310 mix: rfc1320_h,
311 constant: RFC1320_ROUND3_CONSTANT,
312 words: [3, 7, 2, 6, 1, 5, 0, 4],
313 rotations: [3, 9, 11, 15],
314 },
315];
316
317impl BlockMixer<8> for Md4HalfRounds {
318 fn absorb(state: &mut [u32; 4], block: &[u32; 8]) {
319 // Registers A, B, C, D live at indices 0..4. As in RFC 1320, the
320 // step targets cycle A, D, C, B, and each step's three inputs are
321 // the registers that follow its target in the order A B C D.
322 let mut reg = *state;
323 for round in &HALF_MD4_ROUNDS {
324 for (step, &word) in round.words.iter().enumerate() {
325 let target = (4 - step % 4) % 4;
326 let x = reg[(target + 1) % 4];
327 let y = reg[(target + 2) % 4];
328 let z = reg[(target + 3) % 4];
329 reg[target] = reg[target]
330 .wrapping_add((round.mix)(x, y, z))
331 .wrapping_add(block[word])
332 .wrapping_add(round.constant)
333 .rotate_left(round.rotations[step % 4]);
334 }
335 }
336 for (s, r) in state.iter_mut().zip(reg) {
337 *s = s.wrapping_add(r);
338 }
339 }
340
341 fn report(state: &[u32; 4]) -> (u32, u32) {
342 (state[1], state[2])
343 }
344}
345
346// --- TEA -------------------------------------------------------------------
347
348/// TEA (Wheeler & Needham) with the 4-word name block as the 128-bit key
349/// and the first two chaining words as the 64-bit plaintext. Only 16
350/// cycles are run rather than the paper's recommended 32. The ciphertext
351/// is added back into those two chaining words; the other two are never
352/// touched. Major is chaining word 0, minor is word 1.
353struct TeaCipher;
354
355/// The TEA key schedule constant, 2^32 / golden ratio (TEA paper).
356const TEA_DELTA: u32 = 0x9E37_79B9;
357const TEA_CYCLES: u32 = 16;
358
359/// The TEA encipher routine from the paper, for `cycles` cycles.
360fn tea_encipher(plain: [u32; 2], key: &[u32; 4], cycles: u32) -> [u32; 2] {
361 let [mut y, mut z] = plain;
362 let mut sum: u32 = 0;
363 for _ in 0..cycles {
364 sum = sum.wrapping_add(TEA_DELTA);
365 y = y.wrapping_add(
366 (z << 4).wrapping_add(key[0]) ^ z.wrapping_add(sum) ^ (z >> 5).wrapping_add(key[1]),
367 );
368 z = z.wrapping_add(
369 (y << 4).wrapping_add(key[2]) ^ y.wrapping_add(sum) ^ (y >> 5).wrapping_add(key[3]),
370 );
371 }
372 [y, z]
373}
374
375impl BlockMixer<4> for TeaCipher {
376 fn absorb(state: &mut [u32; 4], block: &[u32; 4]) {
377 let [y, z] = tea_encipher([state[0], state[1]], block, TEA_CYCLES);
378 state[0] = state[0].wrapping_add(y);
379 state[1] = state[1].wrapping_add(z);
380 }
381
382 fn report(state: &[u32; 4]) -> (u32, u32) {
383 (state[0], state[1])
384 }
385}
386
387// --- Legacy ----------------------------------------------------------------
388
389/// The original ext3 directory hash: a three-term recurrence. Each byte,
390/// widened and multiplied by a fixed odd constant, is xored into the most
391/// recent term and added to the term before it; a sum that lands in the
392/// upper half of the 32-bit range is pulled back down by 2^31 - 1. The
393/// result is the last term doubled. It ignores the hash seed and has no
394/// minor hash.
395struct LegacyRecurrence;
396
397impl LegacyRecurrence {
398 const MULTIPLIER: u32 = 0x006D_22F5;
399 const FIRST_TERMS: (u32, u32) = (0x37AB_E8F9, 0x12A3_FE2D);
400 const REDUCTION: u32 = 0x7FFF_FFFF;
401
402 fn digest(name: &[u8], widening: ByteWidening) -> u32 {
403 let (_older, newest) = name
404 .iter()
405 .fold(Self::FIRST_TERMS, |(older, newer), &byte| {
406 let scaled = widening.widen(byte).wrapping_mul(Self::MULTIPLIER);
407 let mut next = older.wrapping_add(newer ^ scaled);
408 if next & 0x8000_0000 != 0 {
409 next = next.wrapping_sub(Self::REDUCTION);
410 }
411 (newer, next)
412 });
413 newest << 1
414 }
415}