From 8d118f417c43d51c983f41f1f1bf725a94104d51 Mon Sep 17 00:00:00 2001 From: Kouame Behouba Manasse Date: Fri, 2 Jun 2023 17:31:56 +0300 Subject: [PATCH 1/2] crit: add memory page content retrieval features This commit adds the MemoryReader struct with methods to retrieve the memory pages associated with a process. This feature enhances the functionality of crit as a package by providing access processes memory pages content. Signed-off-by: Kouame Behouba Manasse --- crit/mempages.go | 171 +++++++++++++++++++++++++++ crit/mempages_test.go | 260 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 crit/mempages.go create mode 100644 crit/mempages_test.go diff --git a/crit/mempages.go b/crit/mempages.go new file mode 100644 index 000000000..e6be7ad51 --- /dev/null +++ b/crit/mempages.go @@ -0,0 +1,171 @@ +package crit + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/checkpoint-restore/go-criu/v6/crit/images/mm" + "github.com/checkpoint-restore/go-criu/v6/crit/images/pagemap" +) + +var sysPageSize = os.Getpagesize() + +// MemoryReader is a struct used to retrieve +// the content of memory associated with a specific process ID (pid). +// New instances should be created with NewMemoryReader() +type MemoryReader struct { + checkpointDir string + pid uint32 + pagesID uint32 + pageSize int + pagemapEntries []*pagemap.PagemapEntry +} + +// NewMemoryReader creates a new instance of MemoryReader with all the fields populated +func NewMemoryReader(checkpointDir string, pid uint32, pageSize int) (*MemoryReader, error) { + if pageSize == 0 { + pageSize = sysPageSize + } + + // Check if the given page size is a positive power of 2, otherwise return an error + if (pageSize & (pageSize - 1)) != 0 { + return nil, errors.New("page size should be a positive power of 2") + } + + pagemapImg, err := getImg(filepath.Join(checkpointDir, fmt.Sprintf("pagemap-%d.img", pid)), &pagemap.PagemapHead{}) + if err != nil { + return nil, err + } + + pagesID := pagemapImg.Entries[0].Message.(*pagemap.PagemapHead).GetPagesId() + + pagemapEntries := make([]*pagemap.PagemapEntry, 0) + + for _, entry := range pagemapImg.Entries[1:] { + pagemapEntries = append(pagemapEntries, entry.Message.(*pagemap.PagemapEntry)) + } + + return &MemoryReader{ + checkpointDir: checkpointDir, + pid: pid, + pageSize: pageSize, + pagesID: pagesID, + pagemapEntries: pagemapEntries, + }, nil +} + +// GetMemPages retrieves the content of memory pages +// associated with a given process ID (pid). +// It retrieves the memory content within the +// specified range defined by the start and end addresses. +func (mr *MemoryReader) GetMemPages(start, end uint64) (*bytes.Buffer, error) { + size := end - start + + startPage := start / uint64(mr.pageSize) + endPage := end / uint64(mr.pageSize) + + var buffer bytes.Buffer + + for pageNumber := startPage; pageNumber <= endPage; pageNumber++ { + var page []byte = nil + + pageMem, err := mr.getPage(pageNumber) + if err != nil { + return nil, err + } + + if pageMem != nil { + page = pageMem + } else { + page = bytes.Repeat([]byte("\x00"), mr.pageSize) + } + + var nSkip, nRead uint64 + + if pageNumber == startPage { + nSkip = start - pageNumber*uint64(mr.pageSize) + if startPage == endPage { + nRead = size + } else { + nRead = uint64(mr.pageSize) - nSkip + } + } else if pageNumber == endPage { + nSkip = 0 + nRead = end - pageNumber*uint64(mr.pageSize) + } else { + nSkip = 0 + nRead = uint64(mr.pageSize) + } + + if _, err := buffer.Write(page[nSkip : nSkip+nRead]); err != nil { + return nil, err + } + } + + return &buffer, nil +} + +// getPage retrieves a memory page from the pages.img file. +func (mr *MemoryReader) getPage(pageNo uint64) ([]byte, error) { + var offset uint64 = 0 + + // Iterate over pagemap entries to find the corresponding page + for _, m := range mr.pagemapEntries { + found := false + for i := 0; i < int(*m.NrPages); i++ { + if m.GetVaddr()+uint64(i)*uint64(mr.pageSize) == pageNo*uint64(mr.pageSize) { + found = true + break + } + offset += uint64(mr.pageSize) + } + + if !found { + continue + } + f, err := os.Open(filepath.Join(mr.checkpointDir, fmt.Sprintf("pages-%d.img", mr.pagesID))) + if err != nil { + return nil, err + } + + defer f.Close() + + buff := make([]byte, mr.pageSize) + + if _, err := f.ReadAt(buff, int64(offset)); err != nil { + return nil, err + } + + return buff, nil + } + return nil, nil +} + +// GetPsArgs retrieves process arguments from memory pages +func (mr *MemoryReader) GetPsArgs() (*bytes.Buffer, error) { + mmImg, err := getImg(filepath.Join(mr.checkpointDir, fmt.Sprintf("mm-%d.img", mr.pid)), &mm.MmEntry{}) + if err != nil { + return nil, err + } + mm := mmImg.Entries[0].Message.(*mm.MmEntry) + + return mr.GetMemPages(*mm.MmArgStart, *mm.MmArgEnd) +} + +// GetPsArgs retrieves process environment variables from memory pages. +func (mr *MemoryReader) GetPsEnvVars() (*bytes.Buffer, error) { + mmImg, err := getImg(filepath.Join(mr.checkpointDir, fmt.Sprintf("mm-%d.img", mr.pid)), &mm.MmEntry{}) + if err != nil { + return nil, err + } + mm := mmImg.Entries[0].Message.(*mm.MmEntry) + + return mr.GetMemPages(*mm.MmEnvStart, *mm.MmEnvEnd) +} + +func (mr *MemoryReader) GetPagemapEntries() []*pagemap.PagemapEntry { + return mr.pagemapEntries +} diff --git a/crit/mempages_test.go b/crit/mempages_test.go new file mode 100644 index 000000000..1da7e9b9f --- /dev/null +++ b/crit/mempages_test.go @@ -0,0 +1,260 @@ +package crit + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/checkpoint-restore/go-criu/v6/crit/images/pstree" +) + +const ( + testImgsDir = "test-imgs" +) + +func TestNewMemoryReader(t *testing.T) { + pid, err := getTestImgPID() + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + name string + dir string + pid uint32 + pageSize int + expectedError error + }{ + { + name: "Page size is 0", + dir: testImgsDir, + pid: pid, + pageSize: 0, + expectedError: nil, + }, + { + name: "Invalid page size", + dir: testImgsDir, + pid: pid, + pageSize: 4000, + expectedError: errors.New("page size should be a positive power of 2"), + }, + { + name: "Invalid test-imgs directory", + dir: "no test directory", + pid: pid, + expectedError: errors.New("no such file or directory"), + }, + { + name: "Valid test-imgs directory, pid and page size", + dir: testImgsDir, + pid: pid, + pageSize: 4096, + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mr, err := NewMemoryReader(tc.dir, tc.pid, tc.pageSize) + if err != nil && !strings.Contains(err.Error(), tc.expectedError.Error()) { + t.Errorf("Expected error: %v, got error: %v", tc.expectedError, err) + } + + if mr == nil && tc.expectedError == nil { + t.Errorf("MemoryReader creation failed for checkpoint directory: %s and pid: %d", tc.dir, tc.pid) + } + }) + } +} + +// TestNewMemoryReader test GetMempages method of MemoryReader. +func TestGetMemPages(t *testing.T) { + type testcase struct { + name string + mr *MemoryReader + start uint64 + end uint64 + expectedError error + } + + pid, err := getTestImgPID() + if err != nil { + t.Fatal(err) + } + + // Create a temporary empty memory pages file for testing + tmpFilePath := filepath.Join(os.TempDir(), "pages-0.img") + tmpFile, err := os.Create(tmpFilePath) + if err != nil { + t.Fatal(err) + } + + defer func() { + tmpFile.Close() + if err := os.Remove(tmpFile.Name()); err != nil { + t.Fatal(err) + } + }() + + mr, err := NewMemoryReader(testImgsDir, pid, sysPageSize) + if err != nil { + t.Fatal(err) + } + + testCases := []testcase{ + { + name: "Zero memory area size", + mr: &MemoryReader{ + checkpointDir: testImgsDir, + pid: pid, + pageSize: sysPageSize, + pagesID: mr.pagesID, + pagemapEntries: mr.GetPagemapEntries(), + }, + start: 0, + end: 0, + expectedError: nil, + }, + { + name: "Valid pagemap entry 1", + mr: &MemoryReader{ + checkpointDir: testImgsDir, + pid: pid, + pageSize: sysPageSize, + pagesID: mr.pagesID, + pagemapEntries: mr.GetPagemapEntries(), + }, + start: mr.pagemapEntries[0].GetVaddr(), + end: mr.pagemapEntries[0].GetVaddr() + uint64(uint32(sysPageSize)*mr.pagemapEntries[0].GetNrPages()), + expectedError: nil, + }, + { + name: "Valid pagemap entry 2", + mr: &MemoryReader{ + checkpointDir: testImgsDir, + pid: pid, + pageSize: sysPageSize, + pagesID: mr.pagesID, + pagemapEntries: mr.GetPagemapEntries(), + }, + start: mr.pagemapEntries[1].GetVaddr(), + end: mr.pagemapEntries[1].GetVaddr() + uint64(uint32(sysPageSize)*mr.pagemapEntries[1].GetNrPages()), + expectedError: nil, + }, + { + name: "Invalid pages file", + mr: &MemoryReader{ + checkpointDir: testImgsDir, + pid: pid, + pageSize: sysPageSize, + pagesID: mr.pagesID + 1, + pagemapEntries: mr.GetPagemapEntries(), + }, + start: mr.pagemapEntries[0].GetVaddr(), + end: mr.pagemapEntries[0].GetVaddr() + uint64(uint32(sysPageSize)*mr.pagemapEntries[0].GetNrPages()), + expectedError: errors.New("no such file or directory"), + }, + { + name: "Empty pages file", + mr: &MemoryReader{ + checkpointDir: os.TempDir(), + pid: pid, + pageSize: sysPageSize, + pagesID: 0, + pagemapEntries: mr.GetPagemapEntries(), + }, + start: mr.pagemapEntries[1].GetVaddr(), + end: mr.pagemapEntries[1].GetVaddr() + uint64(uint32(sysPageSize)*mr.pagemapEntries[1].GetNrPages()), + expectedError: errors.New("EOF"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buff, err := tc.mr.GetMemPages(tc.start, tc.end) + if err != nil && tc.expectedError != nil { + if !strings.Contains(err.Error(), tc.expectedError.Error()) { + t.Errorf("Expected error: %v, got error: %v", tc.expectedError, err) + } + } + + if tc.expectedError == nil && buff == nil { + t.Errorf("Returned memory chunk is expected to be non-empty") + } + }) + } +} + +func TestGetPsArgsAndEnvVars(t *testing.T) { + pid, err := getTestImgPID() + if err != nil { + t.Fatal(err) + } + + mr, err := NewMemoryReader(testImgsDir, pid, sysPageSize) + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + name string + expectedError error + mr *MemoryReader + }{ + { + name: "wrong PID", + expectedError: errors.New("no such file or directory"), + mr: &MemoryReader{ + checkpointDir: testImgsDir, + pid: 0, + }, + }, + { + name: "valid arguments and environment variables", + expectedError: nil, + mr: mr, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args, err := tc.mr.GetPsArgs() + if err != nil && tc.expectedError != nil { + if !strings.Contains(err.Error(), tc.expectedError.Error()) { + t.Errorf("Expected error: %v, got error: %v", tc.expectedError, err) + } + } + + if tc.expectedError == nil && args == nil { + t.Errorf("Expected non-nil arguments, got nil") + } + }) + + t.Run(tc.name, func(t *testing.T) { + envVars, err := tc.mr.GetPsEnvVars() + if err != nil && tc.expectedError != nil { + if !strings.Contains(err.Error(), tc.expectedError.Error()) { + t.Errorf("Expected error: %v, got error: %v", tc.expectedError, err) + } + } + + if tc.expectedError == nil && envVars == nil { + t.Errorf("Expected non-nil environment variables, got nil") + } + }) + } +} + +// helper function to get the PID from the test-imgs directory +func getTestImgPID() (uint32, error) { + psTreeImg, err := getImg(filepath.Join(testImgsDir, "pstree.img"), &pstree.PstreeEntry{}) + if err != nil { + return 0, err + } + process := psTreeImg.Entries[0].Message.(*pstree.PstreeEntry) + + return process.GetPgid(), nil +} From 4c337d01365d78cbee5aaa38daaabb9fae52a9a8 Mon Sep 17 00:00:00 2001 From: Kouame Behouba Manasse Date: Thu, 15 Jun 2023 23:09:09 +0300 Subject: [PATCH 2/2] test: add function to test processes memory pages reading features This commit introduces a new function `readMemoryPages` that reads the process arguments and environment variables from memory pages and compares them with the `environ` and `cmdline` files of the process. Signed-off-by: Kouame Behouba Manasse --- test/crit/Makefile | 2 ++ test/crit/main.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/test/crit/Makefile b/test/crit/Makefile index 9733ce402..b218ea6a7 100644 --- a/test/crit/Makefile +++ b/test/crit/Makefile @@ -16,6 +16,8 @@ test-imgs: ../loop/loop mkdir -p $@ $(CRIU) dump -v4 -o dump.log -D $@ -t $(PID) $(CRIU) restore -v4 -o restore.log -D $@ -d + cp /proc/${PID}/environ $@ + cp /proc/${PID}/cmdline $@ pkill -9 loop ../../crit/bin/crit: diff --git a/test/crit/main.go b/test/crit/main.go index 0cfbab6bb..7ebd97e49 100644 --- a/test/crit/main.go +++ b/test/crit/main.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io/ioutil" "log" "os" "path/filepath" @@ -12,6 +13,7 @@ import ( "github.com/checkpoint-restore/go-criu/v6/crit" "github.com/checkpoint-restore/go-criu/v6/crit/cli" + "github.com/checkpoint-restore/go-criu/v6/crit/images/pstree" ) const testImgDir = "test-imgs" @@ -26,6 +28,11 @@ func main() { if err = recodeImgs(imgs); err != nil { log.Fatal(err) } + + // Run test for memory pages reading features + if err = readMemoryPages(); err != nil { + log.Fatal(err) + } log.Println("=== PASS") } @@ -124,3 +131,61 @@ nextFile: return imgs, nil } + +// readMemoryPages reads and compares process arguments +// and environment variables from memory pages and corresponding test files. +func readMemoryPages() error { + psTreeFile, err := os.Open(filepath.Join(testImgDir, "pstree.img")) + if err != nil { + return err + } + defer psTreeFile.Close() + + c := crit.New(psTreeFile, nil, testImgDir, false, true) + + psTreeImg, err := c.Decode(&pstree.PstreeEntry{}) + if err != nil { + return err + } + + pid := psTreeImg.Entries[0].Message.(*pstree.PstreeEntry).GetPid() + + mr, err := crit.NewMemoryReader(testImgDir, pid, os.Getpagesize()) + if err != nil { + return err + } + + // Retrieve process arguments from memory pages + argsBuff, err := mr.GetPsArgs() + if err != nil { + return err + } + + // Read process environment variables from the environ test file + testFileArgs, err := ioutil.ReadFile(filepath.Join(testImgDir, "cmdline")) + if err != nil { + return err + } + + if !bytes.Equal(testFileArgs, argsBuff.Bytes()) { + return errors.New("process arguments do not match") + } + + // Retrieve process environment variables from memory pages + envVarsBuffer, err := mr.GetPsEnvVars() + if err != nil { + return err + } + + // Read process environment variables from the environ test file + envVarsTestFile, err := ioutil.ReadFile(filepath.Join(testImgDir, "environ")) + if err != nil { + return err + } + + if !bytes.Equal(envVarsTestFile, envVarsBuffer.Bytes()) { + return errors.New("process environment variables do not match") + } + + return nil +}