diff --git a/cmd/memparse.go b/cmd/memparse.go index c4c82c54..84aac8c2 100644 --- a/cmd/memparse.go +++ b/cmd/memparse.go @@ -10,6 +10,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/checkpoint-restore/checkpointctl/internal" metadata "github.com/checkpoint-restore/checkpointctl/lib" @@ -49,6 +50,22 @@ func MemParse() *cobra.Command { "Specify the output file to be written to", ) + flags.StringVarP( + searchPattern, + "search", + "s", + "", + "Search for a pattern in memory pages (accepts plain string or regex)", + ) + + flags.IntVarP( + searchContext, + "context", + "c", + 0, + "Specify the number of bytes surrounding each match", + ) + return cmd } @@ -79,6 +96,10 @@ func memparse(cmd *cobra.Command, args []string) error { } defer internal.CleanupTasks(tasks) + if *searchPattern != "" { + return printMemorySearchResult(tasks[0]) + } + if *pID != 0 { return printProcessMemoryPages(tasks[0]) } @@ -273,3 +294,40 @@ func generateHexAndAscii(data []byte) (string, string) { return hex, ascii } + +func printMemorySearchResult(task internal.Task) error { + memReader, err := crit.NewMemoryReader( + filepath.Join(task.OutputDir, metadata.CheckpointDirectory), + *pID, pageSize, + ) + if err != nil { + return err + } + + if err := internal.UntarFiles( + task.CheckpointFilePath, task.OutputDir, + []string{filepath.Join(metadata.CheckpointDirectory, fmt.Sprintf("pages-%d.img", memReader.GetPagesID()))}, + ); err != nil { + return err + } + + matches, err := memReader.SearchPattern(*searchPattern, *searchContext) + if err != nil { + return err + } + + if len(matches) == 0 { + fmt.Printf("\nNothing found for pattern %s in memory pages of process %d", *searchPattern, *pID) + return nil + } + + patternLength := len(*searchPattern) + (*searchContext * 2) + + fmt.Printf("\n%-16s %-*s %-5s\n", "Address", patternLength, "Pattern", "Instance") + fmt.Println(strings.Repeat("-", 19+patternLength+9)) + + for i, match := range matches { + fmt.Printf("%016x %-*s [%d]\n", match.Vaddr, patternLength, match.Pattern, i+1) + } + return nil +} diff --git a/cmd/options.go b/cmd/options.go index 7991f4c0..4a759730 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -14,4 +14,6 @@ var ( files *bool = &internal.Files sockets *bool = &internal.Sockets showAll *bool = &internal.ShowAll + searchPattern *string = &internal.SearchPattern + searchContext *int = &internal.SearchContext ) diff --git a/internal/options.go b/internal/options.go index 4a09d6ed..3e0f4eff 100644 --- a/internal/options.go +++ b/internal/options.go @@ -12,4 +12,6 @@ var ( Files bool Sockets bool ShowAll bool + SearchPattern string + SearchContext int ) diff --git a/vendor/github.com/checkpoint-restore/go-criu/v7/crit/mempages.go b/vendor/github.com/checkpoint-restore/go-criu/v7/crit/mempages.go index c823edba..c2c2a3dc 100644 --- a/vendor/github.com/checkpoint-restore/go-criu/v7/crit/mempages.go +++ b/vendor/github.com/checkpoint-restore/go-criu/v7/crit/mempages.go @@ -4,8 +4,10 @@ import ( "bytes" "errors" "fmt" + "io" "os" "path/filepath" + "regexp" "github.com/checkpoint-restore/go-criu/v7/crit/images/mm" "github.com/checkpoint-restore/go-criu/v7/crit/images/pagemap" @@ -193,3 +195,92 @@ func (mr *MemoryReader) GetShmemSize() (int64, error) { return size, nil } + +// PatternMatch represents a match when searching for a pattern in memory. +type PatternMatch struct { + Vaddr uint64 + Length int + Context int + Pattern string +} + +// SearchPattern searches for a pattern in the process memory pages. +func (mr *MemoryReader) SearchPattern(pattern string, context int) ([]PatternMatch, error) { + if context < 0 { + return nil, errors.New("context size cannot be negative") + } + + regexPattern, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + + var results []PatternMatch + chunkSize := 10 * 1024 * 1024 // Set chunk size of 10MB to be read at a time + + f, err := os.Open(filepath.Join(mr.checkpointDir, fmt.Sprintf("pages-%d.img", mr.pagesID))) + if err != nil { + return nil, err + } + defer f.Close() + + for _, entry := range mr.pagemapEntries { + startAddr := entry.GetVaddr() + endAddr := startAddr + uint64(entry.GetNrPages())*uint64(mr.pageSize) + + initialOffset := uint64(0) + for _, e := range mr.pagemapEntries { + if e == entry { + break + } + initialOffset += uint64(e.GetNrPages()) * uint64(mr.pageSize) + } + + for offset := uint64(0); offset < endAddr-startAddr; offset += uint64(chunkSize) { + readSize := chunkSize + if endAddr-startAddr-offset < uint64(chunkSize) { + readSize = int(endAddr - startAddr - offset) + } + + buff := make([]byte, readSize) + if _, err := f.ReadAt(buff, int64(initialOffset+offset)); err != nil { + if err == io.EOF { + break + } + return nil, err + } + + // Replace non-printable ASCII characters in the buffer with spaces (0x20) to prevent unexpected behavior + // during regex matching. non-printable characters might cause incorrect interpretation or premature + // termination of strings, leading to inaccuracies in pattern matching. + for i := range buff { + if buff[i] < 32 || buff[i] >= 127 { + buff[i] = 0x20 + } + } + + indexes := regexPattern.FindAllIndex(buff, -1) + + for _, index := range indexes { + startContext := index[0] - context + if startContext < 0 { + startContext = 0 + } + + endContext := index[1] + context + if endContext > len(buff) { + endContext = len(buff) + } + + results = append(results, PatternMatch{ + Vaddr: startAddr + offset + uint64(index[0]), + Length: index[1] - index[0], + Context: context, + Pattern: string(buff[startContext:endContext]), + }) + } + } + } + + return results, nil +}