Skip to main content

ntfs_core/
data.rs

1//! Reconstructing an attribute's bytes — resident inline, or non-resident by
2//! following its runlist across the volume.
3//!
4//! Sparse runs yield zeroes without touching the disk; real runs are read at
5//! `lcn × cluster_size`. The output is bounded by the attribute's real size and
6//! by the bytes its runs actually allocate, and every size is checked — a
7//! crafted runlist cannot trigger an unbounded allocation or an out-of-range
8//! seek.
9
10use std::io::{Read, Seek, SeekFrom};
11
12use crate::attribute::{Attribute, AttributeBody};
13use crate::error::{NtfsError, Result};
14use crate::runlist::{self, Run};
15
16/// Hard ceiling on a single reconstructed value (1 TiB) — far above any real
17/// artifact, but stops an allocation bomb from a crafted size.
18const MAX_VALUE_BYTES: u64 = 1 << 40;
19
20/// Read `real_size` bytes of a file described by `runs`, from `reader`.
21///
22/// The result is `min(real_size, bytes the runs allocate)` bytes long; sparse
23/// runs contribute zeroes.
24///
25/// # Errors
26///
27/// [`NtfsError::BadRunlist`] on arithmetic overflow, [`NtfsError::TooLarge`]
28/// when the requested size is implausible, or [`NtfsError::Io`] on read failure.
29pub fn read_runs<R: Read + Seek>(
30    reader: &mut R,
31    runs: &[Run],
32    cluster_size: u64,
33    real_size: u64,
34) -> Result<Vec<u8>> {
35    // Bytes the runs allocate (checked); the value can't exceed this.
36    let mut allocated = 0u64;
37    for r in runs {
38        let run_bytes = r
39            .length
40            .checked_mul(cluster_size)
41            .ok_or(NtfsError::BadRunlist("run byte length overflow"))?;
42        allocated = allocated
43            .checked_add(run_bytes)
44            .ok_or(NtfsError::BadRunlist("allocation overflow"))?;
45    }
46
47    let want = real_size.min(allocated);
48    if want > MAX_VALUE_BYTES {
49        return Err(NtfsError::TooLarge { bytes: want });
50    }
51    let want_usize = usize::try_from(want).map_err(|_| NtfsError::TooLarge { bytes: want })?;
52
53    let mut out: Vec<u8> = Vec::new();
54    out.try_reserve_exact(want_usize)
55        .map_err(|_| NtfsError::TooLarge { bytes: want })?;
56
57    let mut remaining = want;
58    for r in runs {
59        if remaining == 0 {
60            break;
61        }
62        let run_bytes = r.length * cluster_size; // already checked above
63        let take = run_bytes.min(remaining);
64        let take_usize = take as usize; // ≤ want ≤ MAX_VALUE_BYTES, fits usize
65
66        match r.lcn {
67            None => out.resize(out.len() + take_usize, 0), // sparse hole → zeroes
68            Some(lcn) => {
69                let byte_off = lcn
70                    .checked_mul(cluster_size)
71                    .ok_or(NtfsError::BadRunlist("LCN byte offset overflow"))?;
72                reader.seek(SeekFrom::Start(byte_off))?;
73                let start = out.len();
74                out.resize(start + take_usize, 0);
75                reader.read_exact(&mut out[start..])?;
76            }
77        }
78        remaining -= take;
79    }
80
81    Ok(out)
82}
83
84/// Read an attribute's value, dispatching on resident vs non-resident.
85///
86/// `record` is the (fixed-up) MFT record the attribute lives in; `reader` is the
87/// volume; `cluster_size` is from the boot sector.
88///
89/// # Errors
90///
91/// As [`read_runs`], plus [`NtfsError::BadAttribute`] when a resident value or
92/// the runlist slice is out of bounds.
93pub fn read_attribute_value<R: Read + Seek>(
94    reader: &mut R,
95    record: &[u8],
96    attribute: &Attribute,
97    cluster_size: u64,
98) -> Result<Vec<u8>> {
99    match attribute.body {
100        AttributeBody::Resident { .. } => attribute
101            .resident_content(record)
102            .map(<[u8]>::to_vec)
103            .ok_or(NtfsError::BadAttribute {
104                offset: attribute.offset,
105                detail: "resident content out of bounds",
106            }),
107        AttributeBody::NonResident { real_size, .. } => {
108            let runs = attribute_runlist(record, attribute)?;
109            read_runs(reader, &runs, cluster_size, real_size)
110        }
111    }
112}
113
114/// Decode the data-run list of a non-resident attribute from its (fixed-up)
115/// record bytes.
116///
117/// Reused to assemble a split `$DATA` whose runlist spans several `$DATA`
118/// attributes in different MFT records (via `$ATTRIBUTE_LIST`).
119///
120/// # Errors
121///
122/// [`NtfsError::BadAttribute`] for a resident attribute or an out-of-bounds
123/// runlist; [`NtfsError::BadRunlist`] for a malformed runlist.
124pub fn attribute_runlist(record: &[u8], attribute: &Attribute) -> Result<Vec<Run>> {
125    let AttributeBody::NonResident { runs_offset, .. } = attribute.body else {
126        return Err(NtfsError::BadAttribute {
127            offset: attribute.offset,
128            detail: "attribute is resident (no runlist)",
129        });
130    };
131    let attr_end = attribute
132        .offset
133        .checked_add(attribute.length as usize)
134        .ok_or(NtfsError::BadAttribute {
135            offset: attribute.offset,
136            detail: "attribute length overflow",
137        })?;
138    let runs_start =
139        attribute
140            .offset
141            .checked_add(runs_offset as usize)
142            .ok_or(NtfsError::BadAttribute {
143                offset: attribute.offset,
144                detail: "runs offset overflow",
145            })?;
146    let runs_bytes = record
147        .get(runs_start..attr_end)
148        .ok_or(NtfsError::BadAttribute {
149            offset: attribute.offset,
150            detail: "runlist out of bounds",
151        })?;
152    runlist::decode(runs_bytes)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use std::io::Cursor;
159
160    /// A volume where cluster `c` is filled with byte value `c as u8`.
161    fn volume(clusters: usize, cluster_size: usize) -> Cursor<Vec<u8>> {
162        let mut v = vec![0u8; clusters * cluster_size];
163        for c in 0..clusters {
164            let b = c as u8;
165            for x in &mut v[c * cluster_size..(c + 1) * cluster_size] {
166                *x = b;
167            }
168        }
169        Cursor::new(v)
170    }
171
172    #[test]
173    fn reads_single_run() {
174        let mut vol = volume(4, 512);
175        // One run: 2 clusters starting at LCN 1.
176        let runs = [Run {
177            length: 2,
178            lcn: Some(1),
179        }];
180        let out = read_runs(&mut vol, &runs, 512, 1024).unwrap();
181        assert_eq!(out.len(), 1024);
182        assert!(out[..512].iter().all(|&b| b == 1));
183        assert!(out[512..].iter().all(|&b| b == 2));
184    }
185
186    #[test]
187    fn sparse_run_yields_zeroes_without_reading() {
188        let mut vol = volume(1, 512); // too small to read 2 clusters — proves no read
189        let runs = [Run {
190            length: 2,
191            lcn: None,
192        }];
193        let out = read_runs(&mut vol, &runs, 512, 1024).unwrap();
194        assert_eq!(out.len(), 1024);
195        assert!(out.iter().all(|&b| b == 0));
196    }
197
198    #[test]
199    fn truncates_to_real_size() {
200        let mut vol = volume(4, 512);
201        let runs = [Run {
202            length: 2,
203            lcn: Some(0),
204        }]; // 1024 allocated
205        let out = read_runs(&mut vol, &runs, 512, 600).unwrap();
206        assert_eq!(out.len(), 600);
207    }
208
209    #[test]
210    fn mixed_data_and_sparse() {
211        let mut vol = volume(4, 512);
212        let runs = [
213            Run {
214                length: 1,
215                lcn: Some(3),
216            }, // cluster 3 → all 3s
217            Run {
218                length: 1,
219                lcn: None,
220            }, // sparse → zeros
221        ];
222        let out = read_runs(&mut vol, &runs, 512, 1024).unwrap();
223        assert!(out[..512].iter().all(|&b| b == 3));
224        assert!(out[512..].iter().all(|&b| b == 0));
225    }
226
227    #[test]
228    fn refuses_implausible_size() {
229        // A crafted runlist that *allocates* far more than the ceiling — a
230        // single sparse run of 2^40 clusters. (A huge real_size alone is
231        // harmless: it is clamped to what the runs actually allocate.)
232        let mut vol = volume(1, 512);
233        let runs = [Run {
234            length: 1 << 40,
235            lcn: None,
236        }];
237        assert!(matches!(
238            read_runs(&mut vol, &runs, 512, u64::MAX),
239            Err(NtfsError::TooLarge { .. })
240        ));
241    }
242
243    #[test]
244    fn rejects_cluster_size_overflow() {
245        let mut vol = volume(1, 512);
246        let runs = [Run {
247            length: u64::MAX,
248            lcn: Some(0),
249        }];
250        assert!(matches!(
251            read_runs(&mut vol, &runs, 512, 1024),
252            Err(NtfsError::BadRunlist(_))
253        ));
254    }
255
256    // ── read_attribute_value dispatch ─────────────────────────────────────────
257
258    #[test]
259    fn reads_resident_value() {
260        use forensicnomicon::ntfs::attr_types;
261        // Build a one-attribute record with resident $DATA content "hello".
262        let content = b"hello";
263        // Minimal resident attribute laid out by hand at record offset 0x10.
264        let attr_off = 0x10usize;
265        let mut record = vec![0u8; attr_off];
266        // header: type, length, resident, name_len 0, name_off, flags, id
267        let name_offset = 0x18u16;
268        let content_offset = 0x18u16;
269        let length = (content_offset as usize + content.len() + 7) & !7;
270        let mut a = vec![0u8; length];
271        a[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
272        a[0x04..0x08].copy_from_slice(&(length as u32).to_le_bytes());
273        a[0x0A..0x0C].copy_from_slice(&name_offset.to_le_bytes());
274        a[0x10..0x14].copy_from_slice(&(content.len() as u32).to_le_bytes());
275        a[0x14..0x16].copy_from_slice(&content_offset.to_le_bytes());
276        a[content_offset as usize..content_offset as usize + content.len()]
277            .copy_from_slice(content);
278        record.extend_from_slice(&a);
279        record.extend_from_slice(&attr_types::END.to_le_bytes());
280
281        let attrs = crate::attribute::parse_attributes(&record, attr_off).unwrap();
282        let mut vol = volume(1, 512);
283        let out = read_attribute_value(&mut vol, &record, &attrs[0], 512).unwrap();
284        assert_eq!(out, b"hello");
285    }
286
287    #[test]
288    fn reads_nonresident_value_via_runlist() {
289        use forensicnomicon::ntfs::attr_types;
290        // Non-resident $DATA: runlist of 1 cluster @ LCN 2, real size 512.
291        let runs_bytes = [0x11u8, 0x01, 0x02, 0x00]; // len 1, lcn delta +2
292        let attr_off = 0x10usize;
293        let mut record = vec![0u8; attr_off];
294        let runs_offset = 0x40u16;
295        let length = ((runs_offset as usize + runs_bytes.len()) + 7) & !7;
296        let mut a = vec![0u8; length];
297        a[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
298        a[0x04..0x08].copy_from_slice(&(length as u32).to_le_bytes());
299        a[0x08] = 1; // non-resident
300        a[0x0A..0x0C].copy_from_slice(&runs_offset.to_le_bytes()); // name offset (no name)
301        a[0x20..0x22].copy_from_slice(&runs_offset.to_le_bytes()); // runs offset
302        a[0x28..0x30].copy_from_slice(&512u64.to_le_bytes()); // allocated
303        a[0x30..0x38].copy_from_slice(&512u64.to_le_bytes()); // real size
304        a[runs_offset as usize..runs_offset as usize + runs_bytes.len()]
305            .copy_from_slice(&runs_bytes);
306        record.extend_from_slice(&a);
307        record.extend_from_slice(&attr_types::END.to_le_bytes());
308
309        let attrs = crate::attribute::parse_attributes(&record, attr_off).unwrap();
310        let mut vol = volume(4, 512); // cluster 2 → all 2s
311        let out = read_attribute_value(&mut vol, &record, &attrs[0], 512).unwrap();
312        assert_eq!(out.len(), 512);
313        assert!(out.iter().all(|&b| b == 2));
314    }
315
316    #[test]
317    fn stops_reading_once_real_size_is_met() {
318        // real_size covers only the first run; the second run must not be read.
319        let mut vol = volume(4, 512);
320        let runs = [
321            Run {
322                length: 1,
323                lcn: Some(0),
324            },
325            Run {
326                length: 1,
327                lcn: Some(1),
328            },
329        ];
330        let out = read_runs(&mut vol, &runs, 512, 512).unwrap();
331        assert_eq!(out.len(), 512); // only the first run's worth
332    }
333
334    #[test]
335    fn rejects_runlist_region_out_of_bounds() {
336        use crate::attribute::{Attribute, AttributeBody};
337        // runs_offset points past the attribute, so the runlist slice is invalid.
338        let attr = Attribute {
339            type_code: forensicnomicon::ntfs::attr_types::DATA,
340            length: 0x48,
341            non_resident: true,
342            name: None,
343            flags: 0,
344            attribute_id: 0,
345            offset: 0,
346            body: AttributeBody::NonResident {
347                start_vcn: 0,
348                last_vcn: 0,
349                runs_offset: 0xFFFF,
350                compression_unit: 0,
351                allocated_size: 512,
352                real_size: 512,
353                initialized_size: 512,
354            },
355        };
356        let record = vec![0u8; 0x48];
357        let mut vol = volume(1, 512);
358        assert!(matches!(
359            read_attribute_value(&mut vol, &record, &attr, 512),
360            Err(NtfsError::BadAttribute { detail, .. }) if detail == "runlist out of bounds"
361        ));
362    }
363
364    #[test]
365    fn rejects_runs_offset_overflow() {
366        use crate::attribute::{Attribute, AttributeBody};
367        // offset + length stays in range, but offset + runs_offset overflows.
368        let attr = Attribute {
369            type_code: forensicnomicon::ntfs::attr_types::DATA,
370            length: 0x48,
371            non_resident: true,
372            name: None,
373            flags: 0,
374            attribute_id: 0,
375            offset: usize::MAX - 0x48,
376            body: AttributeBody::NonResident {
377                start_vcn: 0,
378                last_vcn: 0,
379                runs_offset: 0x49,
380                compression_unit: 0,
381                allocated_size: 512,
382                real_size: 512,
383                initialized_size: 512,
384            },
385        };
386        let record = vec![0u8; 1];
387        let mut vol = volume(1, 512);
388        assert!(matches!(
389            read_attribute_value(&mut vol, &record, &attr, 512),
390            Err(NtfsError::BadAttribute { detail, .. }) if detail == "runs offset overflow"
391        ));
392    }
393
394    #[test]
395    fn attribute_runlist_rejects_resident_attribute() {
396        let attr = Attribute {
397            type_code: forensicnomicon::ntfs::attr_types::DATA,
398            length: 0x20,
399            non_resident: false,
400            name: None,
401            flags: 0,
402            attribute_id: 0,
403            offset: 0,
404            body: AttributeBody::Resident {
405                content_offset: 0x18,
406                content_length: 4,
407            },
408        };
409        assert!(matches!(
410            attribute_runlist(&[0u8; 0x20], &attr),
411            Err(NtfsError::BadAttribute { detail, .. }) if detail.contains("resident")
412        ));
413    }
414}