@cryptotaxi247 / netdata-1 / commits / e482ca8f6

Add object size and bounds validation when reading journal files. (#21697)

1. Validate object size is at least sizeof(ObjectHeader) 2. Check that objects are not located in the file header area 3. Verify object offset + size doesn't overflow 4. Ensure objects don't exceed the journal's arena bounds On the writer side we update `arena_size` immediately after writing each object (rather than at entry completion, similar to systemd). This ensures subsequent reads within the same write operation can find newly written objects.

vkalintiris committed Feb 3, 2026 at 00:52 UTC e482ca8f6c1dc6d877dc1d429826f31d5cacc31e
4 files changed +79 -15
src/crates/journal-core/src/error.rs
+6
@@ -12,6 +12,12 @@ pub enum JournalError {
12 #[error("invalid object type")]
13 InvalidObjectType,
14
15 + #[error("invalid object size: {0}")]
16 + InvalidObjectSize(u64),
17 +
18 + #[error("object exceeds file bounds")]
19 + ObjectExceedsFileBounds,
20 +
21 #[error("invalid object location")]
22 InvalidObjectLocation,
23
src/crates/journal-core/src/file/file.rs
+42 -9
@@ -420,9 +420,15 @@ impl<M: MemoryMap> JournalFile<M> {
420 {
421 validate_offset_alignment(offset)?;
422
423 - let is_compact = self
424 - .journal_header_ref()
425 - .has_incompatible_flag(HeaderIncompatibleFlags::Compact);
423 + let journal_header = self.journal_header_ref();
424 + let is_compact = journal_header.has_incompatible_flag(HeaderIncompatibleFlags::Compact);
425 + let header_size = journal_header.header_size;
426 + let arena_end = header_size + journal_header.arena_size;
427 +
428 + // Objects cannot be located in the file header
429 + if offset.get() < header_size {
430 + return Err(JournalError::ObjectExceedsFileBounds);
431 + }
432
433 self.window_manager.with_guarded(offset, |wm| {
434 // Get the object header to determine size
@@ -431,9 +437,18 @@ impl<M: MemoryMap> JournalFile<M> {
437 wm.get_slice(offset.get(), std::mem::size_of::<ObjectHeader>() as u64)?;
438 let header = ObjectHeader::ref_from_bytes(header_slice)
439 .map_err(|_| JournalError::ZerocopyFailure)?;
434 - header.size
440 + header.validated_size()?
441 };
442
443 + // Validate that the object doesn't exceed the journal's arena bounds
444 + let end_offset = offset
445 + .get()
446 + .checked_add(size_needed)
447 + .ok_or(JournalError::ObjectExceedsFileBounds)?;
448 + if end_offset > arena_end {
449 + return Err(JournalError::ObjectExceedsFileBounds);
450 + }
451 +
452 // Get the full object data
453 let data = wm.get_slice(offset.get(), size_needed)?;
454
@@ -869,15 +884,22 @@ impl<M: MemoryMapMut> JournalFile<M> {
884 {
885 validate_offset_alignment(offset)?;
886
872 - let is_compact = self
873 - .journal_header_ref()
874 - .has_incompatible_flag(HeaderIncompatibleFlags::Compact);
887 + let journal_header = self.journal_header_ref();
888 + let is_compact = journal_header.has_incompatible_flag(HeaderIncompatibleFlags::Compact);
889 + let header_size = journal_header.header_size;
890 + let arena_end = header_size + journal_header.arena_size;
891 +
892 + // Objects cannot be located in the file header
893 + if offset.get() < header_size {
894 + return Err(JournalError::ObjectExceedsFileBounds);
895 + }
896
897 self.window_manager.with_guarded(offset, |wm| {
898 // Get or set the size
899 let size_needed = match size {
900 Some(size) => {
880 - // Setting object header for a new object
901 + // Setting object header for a new object (no bounds check needed,
902 + // the file will be extended as necessary)
903 let header_slice =
904 wm.get_slice_mut(offset.get(), std::mem::size_of::<ObjectHeader>() as u64)?;
905 let header = ObjectHeader::mut_from_bytes(header_slice)
@@ -895,7 +917,18 @@ impl<M: MemoryMapMut> JournalFile<M> {
917 if header.type_ != type_ as u8 {
918 return Err(JournalError::InvalidObjectType);
919 }
898 - header.size
920 + let size = header.validated_size()?;
921 +
922 + // Validate that the object doesn't exceed the journal's arena bounds
923 + let end_offset = offset
924 + .get()
925 + .checked_add(size)
926 + .ok_or(JournalError::ObjectExceedsFileBounds)?;
927 + if end_offset > arena_end {
928 + return Err(JournalError::ObjectExceedsFileBounds);
929 + }
930 +
931 + size
932 }
933 };
934
src/crates/journal-core/src/file/object.rs
+15
@@ -419,6 +419,21 @@ impl ObjectHeader {
419 pub fn aligned_size(&self) -> u64 {
420 (self.size + 7) & !7
421 }
422 +
423 + /// Validates that the object size is sane.
424 + ///
425 + /// Returns the size if valid, or an error if the size is invalid.
426 + /// This should be called when reading an ObjectHeader from a journal file
427 + /// to protect against corrupted data.
428 + pub fn validated_size(&self) -> crate::error::Result<u64> {
429 + let min_size = std::mem::size_of::<ObjectHeader>() as u64;
430 +
431 + if self.size < min_size {
432 + return Err(crate::error::JournalError::InvalidObjectSize(self.size));
433 + }
434 +
435 + Ok(self.size)
436 + }
437 }
438
439 #[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
src/crates/journal-core/src/file/writer.rs
+16 -6
@@ -146,7 +146,7 @@ impl JournalWriter {
146
147 entry_guard.header.object_header.aligned_size()
148 };
149 - self.object_added(entry_offset, entry_size);
149 + self.object_added(journal_file, entry_offset, entry_size);
150
151 self.append_to_entry_array(journal_file, entry_offset)?;
152 for entry_item_index in 0..self.entry_items.len() {
@@ -158,10 +158,20 @@ impl JournalWriter {
158 Ok(())
159 }
160
161 - fn object_added(&mut self, object_offset: NonZeroU64, object_size: u64) {
161 + fn object_added(
162 + &mut self,
163 + journal_file: &mut JournalFile<MmapMut>,
164 + object_offset: NonZeroU64,
165 + object_size: u64,
166 + ) {
167 self.tail_object_offset = object_offset;
168 self.append_offset = object_offset.saturating_add(object_size);
169 self.num_written_objects += 1;
170 +
171 + // Update arena_size immediately after writing, so subsequent reads
172 + // within the same write operation can find the newly written object.
173 + let header = journal_file.journal_header_mut();
174 + header.arena_size = self.append_offset.get() - header.header_size;
175 }
176
177 fn entry_added(&mut self, header: &mut JournalHeader, realtime: u64, monotonic: u64) {
@@ -211,7 +221,7 @@ impl JournalWriter {
221 data_guard.header.object_header.aligned_size()
222 };
223
214 - self.object_added(data_offset, data_size);
224 + self.object_added(journal_file, data_offset, data_size);
225
226 // Update hash table
227 journal_file.data_hash_table_set_tail_offset(hash, data_offset)?;
@@ -264,7 +274,7 @@ impl JournalWriter {
274 field_guard.set_payload(payload);
275 field_guard.header.object_header.aligned_size()
276 };
267 - self.object_added(field_offset, field_size);
277 + self.object_added(journal_file, field_offset, field_size);
278
279 // Update hash table
280 journal_file.field_hash_table_set_tail_offset(hash, field_offset)?;
@@ -277,7 +287,7 @@ impl JournalWriter {
287
288 fn allocate_new_array(
289 &mut self,
280 - journal_file: &JournalFile<MmapMut>,
290 + journal_file: &mut JournalFile<MmapMut>,
291 capacity: NonZeroU64,
292 ) -> Result<NonZeroU64> {
293 // let new_capacity = previous_capacity.saturating_mul(NonZeroU64::new(2).unwrap());
@@ -288,7 +298,7 @@ impl JournalWriter {
298
299 array_guard.header.object_header.aligned_size()
300 };
291 - self.object_added(array_offset, array_size);
301 + self.object_added(journal_file, array_offset, array_size);
302
303 Ok(array_offset)
304 }