66import logging
77import datetime
88from dataclasses import dataclass , astuple
9- from typing import List , Set , Type , Iterable
9+ from typing import List , Set , Type , Iterable , Tuple
1010
11- from volatility3 .framework import renderers , interfaces
11+ from volatility3 .framework import renderers , interfaces , exceptions
1212from volatility3 .framework .renderers import format_hints
1313from volatility3 .framework .interfaces import plugins
1414from volatility3 .framework .configuration import requirements
@@ -104,7 +104,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
104104
105105 _required_framework_version = (2 , 0 , 0 )
106106
107- _version = (1 , 0 , 1 )
107+ _version = (1 , 0 , 2 )
108108
109109 @classmethod
110110 def get_requirements (cls ) -> List [interfaces .configuration .RequirementInterface ]:
@@ -147,7 +147,13 @@ def _follow_symlink(
147147 Otherwise, it returns the same symlink_path
148148 """
149149 # i_link (fast symlinks) were introduced in 4.2
150- if inode and inode .is_link and inode .has_member ("i_link" ) and inode .i_link :
150+ if (
151+ inode
152+ and inode .is_link
153+ and inode .has_member ("i_link" )
154+ and inode .i_link
155+ and inode .i_link .is_readable ()
156+ ):
151157 i_link_str = inode .i_link .dereference ().cast (
152158 "string" , max_length = 255 , encoding = "utf-8" , errors = "replace"
153159 )
@@ -253,6 +259,10 @@ def get_inodes(
253259 if not root_inode .is_valid ():
254260 continue
255261
262+ if not (root_inode .i_mapping and root_inode .i_mapping .is_readable ()):
263+ # Retrieving data from the page cache requires a valid address space
264+ continue
265+
256266 # Inode already processed?
257267 if root_inode_ptr in seen_inodes :
258268 continue
@@ -284,6 +294,10 @@ def get_inodes(
284294 if not file_inode .is_valid ():
285295 continue
286296
297+ if not (file_inode .i_mapping and file_inode .i_mapping .is_readable ()):
298+ # Retrieving data from the page cache requires a valid address space
299+ continue
300+
287301 # Inode already processed?
288302 if file_inode_ptr in seen_inodes :
289303 continue
@@ -316,10 +330,12 @@ def _generator(self):
316330 if self .config ["find" ]:
317331 if inode_in .path == self .config ["find" ]:
318332 inode_out = inode_in .to_user (vmlinux_layer )
333+
319334 yield (0 , astuple (inode_out ))
320335 break # Only the first match
321336 else :
322337 inode_out = inode_in .to_user (vmlinux_layer )
338+
323339 yield (0 , astuple (inode_out ))
324340
325341 def generate_timeline (self ):
@@ -389,7 +405,7 @@ class InodePages(plugins.PluginInterface):
389405
390406 _required_framework_version = (2 , 0 , 0 )
391407
392- _version = (2 , 0 , 0 )
408+ _version = (2 , 0 , 1 )
393409
394410 @classmethod
395411 def get_requirements (cls ) -> List [interfaces .configuration .RequirementInterface ]:
@@ -443,28 +459,80 @@ def write_inode_content_to_file(
443459 # created, saving both disk space and I/O time.
444460 # Additionally, using the page index will guarantee that each page is written at the
445461 # appropriate file position.
462+ inode_size = inode .i_size
446463 try :
447- with open_method (filename ) as f :
448- inode_size = inode .i_size
449- f .truncate (inode_size )
450-
464+ file_initialized = False
465+ with open_method (filename ) as file_obj :
451466 for page_idx , page_content in inode .get_contents ():
452467 current_fp = page_idx * vmlinux_layer .page_size
453468 max_length = inode_size - current_fp
454- page_bytes = page_content [:max_length ]
455- if current_fp + len (page_bytes ) > inode_size :
469+ page_bytes_len = min (max_length , len (page_content ))
470+ if (
471+ current_fp >= inode_size
472+ or current_fp + page_bytes_len > inode_size
473+ ):
456474 vollog .error (
457475 "Page out of file bounds: inode 0x%x, inode size %d, page index %d" ,
458476 inode .vol .offset ,
459477 inode_size ,
460478 page_idx ,
461479 )
462- f .seek (current_fp )
463- f .write (page_bytes )
464-
480+ continue
481+ page_bytes = page_content [:page_bytes_len ]
482+
483+ if not file_initialized :
484+ # Lazy initialization to avoid truncating the file until we are
485+ # certain there is something to write
486+ file_obj .truncate (inode_size )
487+ file_initialized = True
488+
489+ file_obj .seek (current_fp )
490+ file_obj .write (page_bytes )
491+ except exceptions .LinuxPageCacheException :
492+ vollog .error (
493+ f"Error dumping cached pages for inode at { inode .vol .offset :#x} "
494+ )
465495 except OSError as e :
466496 vollog .error ("Unable to write to file (%s): %s" , filename , e )
467497
498+ def _generate_inode_fields (
499+ self ,
500+ inode : interfaces .objects .ObjectInterface ,
501+ vmlinux_layer : interfaces .layers .TranslationLayerInterface ,
502+ ) -> Iterable [Tuple [int , int , int , int , bool , str ]]:
503+ inode_size = inode .i_size
504+ try :
505+ for page_obj in inode .get_pages ():
506+ if page_obj .mapping != inode .i_mapping :
507+ vollog .warning (
508+ f"Cached page at { page_obj .vol .offset :#x} has a mismatched address space with the inode. Skipping page"
509+ )
510+ continue
511+ page_vaddr = page_obj .vol .offset
512+ page_paddr = page_obj .to_paddr ()
513+ page_mapping_addr = page_obj .mapping
514+ page_index = page_obj .index
515+ page_file_offset = page_index * vmlinux_layer .page_size
516+ dump_safe = (
517+ page_file_offset < inode_size
518+ and page_mapping_addr
519+ and page_mapping_addr .is_readable ()
520+ )
521+ page_flags_list = page_obj .get_flags_list ()
522+ page_flags = "," .join ([x .replace ("PG_" , "" ) for x in page_flags_list ])
523+ fields = (
524+ page_vaddr ,
525+ page_paddr ,
526+ page_mapping_addr ,
527+ page_index ,
528+ dump_safe ,
529+ page_flags ,
530+ )
531+
532+ yield 0 , fields
533+ except exceptions .LinuxPageCacheException :
534+ vollog .warning (f"Page cache for inode at { inode .vol .offset :#x} is corrupt" )
535+
468536 def _generator (self ):
469537 vmlinux_module_name = self .config ["kernel" ]
470538 vmlinux = self .context .modules [vmlinux_module_name ]
@@ -486,7 +554,6 @@ def _generator(self):
486554 else :
487555 vollog .error ("Unable to find inode with path %s" , self .config ["find" ])
488556 return None
489-
490557 elif self .config ["inode" ]:
491558 inode = vmlinux .object ("inode" , self .config ["inode" ], absolute = True )
492559 else :
@@ -501,27 +568,6 @@ def _generator(self):
501568 vollog .error ("The inode is not a regular file" )
502569 return None
503570
504- inode_size = inode .i_size
505- for page_obj in inode .get_pages ():
506- page_vaddr = page_obj .vol .offset
507- page_paddr = page_obj .to_paddr ()
508- page_mapping_addr = page_obj .mapping
509- page_index = int (page_obj .index )
510- page_file_offset = page_index * vmlinux_layer .page_size
511- dump_safe = page_file_offset < inode_size
512- page_flags_list = page_obj .get_flags_list ()
513- page_flags = "," .join ([x .replace ("PG_" , "" ) for x in page_flags_list ])
514- fields = (
515- page_vaddr ,
516- page_paddr ,
517- page_mapping_addr ,
518- page_index ,
519- dump_safe ,
520- page_flags ,
521- )
522-
523- yield 0 , fields
524-
525571 if self .config ["dump" ]:
526572 open_method = self .open
527573 inode_address = inode .vol .offset
@@ -530,6 +576,8 @@ def _generator(self):
530576 self .write_inode_content_to_file (
531577 inode , filename , open_method , vmlinux_layer
532578 )
579+ else :
580+ yield from self ._generate_inode_fields (inode , vmlinux_layer )
533581
534582 def run (self ):
535583 headers = [
0 commit comments