From 99241ac91a5a563df6bec8e7a07fe84a3bbb2357 Mon Sep 17 00:00:00 2001 From: ZQ Date: Tue, 11 Feb 2025 12:09:10 +0800 Subject: [PATCH 1/4] test(ptracer): add testfile and verify vmReadStr problem --- ptracer/context_helper_linux_test.go | 263 +++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 ptracer/context_helper_linux_test.go diff --git a/ptracer/context_helper_linux_test.go b/ptracer/context_helper_linux_test.go new file mode 100644 index 0000000..7eba76a --- /dev/null +++ b/ptracer/context_helper_linux_test.go @@ -0,0 +1,263 @@ +package ptracer + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "testing" +) + +// TestHasNull tests the hasNull function +func TestHasNull(t *testing.T) { + tests := []struct { + name string + data []byte + want bool + }{ + { + name: "empty buffer", + data: []byte{}, + want: false, + }, + { + name: "no null", + data: []byte("hello"), + want: false, + }, + { + name: "has null at start", + data: []byte{0, 1, 2, 3}, + want: true, + }, + { + name: "has null at end", + data: []byte{1, 2, 3, 0}, + want: true, + }, + { + name: "has null in middle", + data: []byte{1, 0, 3, 4}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasNull(tt.data); got != tt.want { + t.Errorf("hasNull() = %v, want %v", got, tt.want) + } + }) + } +} + +// Helper function: creates a child process and returns its PID +func createTestProcess(t *testing.T) (int, func()) { + cmd := exec.Command("sleep", "10") // use sleep command to create a running process + if err := cmd.Start(); err != nil { + t.Fatalf("Failed to start test process: %v", err) + } + + cleanup := func() { + cmd.Process.Kill() + cmd.Wait() + } + + return cmd.Process.Pid, cleanup +} + +// TestVmRead tests the vmRead function +func TestVmRead(t *testing.T) { + pid, cleanup := createTestProcess(t) + defer cleanup() + + // create test data + testData := []byte("Hello, World!") + buff := make([]byte, len(testData)) + + // get process memory maps + maps, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid)) + if err != nil { + t.Fatalf("Failed to read process maps: %v", err) + } + + // parse memory maps to find a readable address + var addr uintptr + for _, line := range bytes.Split(maps, []byte{'\n'}) { + if len(line) == 0 { + continue + } + if bytes.Contains(line, []byte("r-x")) { // find a readable segment + var start uint64 + fmt.Sscanf(string(line), "%x-", &start) + addr = uintptr(start) + break + } + } + + if addr == 0 { + t.Fatal("Failed to find readable memory region") + } + + // test reading + n, err := vmRead(pid, addr, buff) + if err != nil { + t.Fatalf("vmRead failed: %v", err) + } + if n == 0 { + t.Error("vmRead returned 0 bytes") + } +} + +// TestVmReadStr tests the vmReadStr function +func TestVmReadStr(t *testing.T) { + pid, cleanup := createTestProcess(t) + defer cleanup() + + // test cases + testCases := []struct { + name string + buffSize int + addrAlign uintptr // address alignment, used to test different alignment scenarios + wantErr bool + }{ + { + name: "small_buffer_aligned", + buffSize: 10, + addrAlign: 0, + wantErr: false, + }, + { + name: "small_buffer_unaligned", + buffSize: 10, + addrAlign: 1, + wantErr: false, + }, + { + name: "exact_page_size", + buffSize: pageSize, + addrAlign: 0, + wantErr: false, + }, + { + name: "cross_page_boundary", + buffSize: pageSize + 100, + addrAlign: uintptr(pageSize - 50), + wantErr: false, + }, + { + name: "large_buffer_unaligned", + buffSize: pageSize * 2, + addrAlign: 123, + wantErr: false, + }, + { + name: "buffer_smaller_than_to_boundary", + buffSize: 10, + addrAlign: uintptr(pageSize - 100), // distance to page boundary is 100 bytes, but buffer is only 10 bytes + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buff := make([]byte, tc.buffSize) + + // get a readable memory address + maps, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid)) + if err != nil { + t.Fatalf("Failed to read process maps: %v", err) + } + + var baseAddr uintptr + for _, line := range bytes.Split(maps, []byte{'\n'}) { + if len(line) == 0 { + continue + } + if bytes.Contains(line, []byte("r-x")) { + var start uint64 + fmt.Sscanf(string(line), "%x-", &start) + baseAddr = uintptr(start) + break + } + } + + if baseAddr == 0 { + t.Fatal("Failed to find readable memory region") + } + + // use test case specified alignment offset + testAddr := baseAddr + tc.addrAlign + + // record buffer content before reading + originalBuff := make([]byte, len(buff)) + copy(originalBuff, buff) + + err = vmReadStr(pid, testAddr, buff) + if (err != nil) != tc.wantErr { + t.Errorf("vmReadStr() error = %v, wantErr %v", err, tc.wantErr) + } + + // verify if actual reading occurred + if !bytes.Equal(buff, originalBuff) { + // at least some data was read + t.Logf("Data was read successfully for case: %s", tc.name) + } + + // special case: check buffer size smaller than distance to boundary + if tc.name == "buffer_smaller_than_to_boundary" { + distToBoundary := pageSize - int(testAddr%uintptr(pageSize)) + if distToBoundary > len(buff) { + t.Logf("Verified buffer handling when smaller than distance to boundary: dist=%d, buff=%d", + distToBoundary, len(buff)) + } + } + }) + } +} + +// TestSliceBehavior tests slice behavior +func TestSliceBehavior(t *testing.T) { + tests := []struct { + name string + buffSize int + nextRead int + expected int + }{ + { + name: "small_buffer_large_read", + buffSize: 10, + nextRead: 4096, + expected: 10, // must be limited to buffer size + }, + { + name: "large_buffer_small_read", + buffSize: 8192, + nextRead: 4096, + expected: 4096, // can use full read amount + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buff := make([]byte, tt.buffSize) + // safely calculate actual read amount + actualRead := tt.nextRead + if tt.buffSize < actualRead { + actualRead = tt.buffSize + } + slice := buff[:actualRead] + + if len(slice) != tt.expected { + t.Errorf("Expected slice len %d, got %d", tt.expected, len(slice)) + } + }) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} From 8b7494f7965818a866ff5accd751067c7f6e65b3 Mon Sep 17 00:00:00 2001 From: ZQ Date: Tue, 11 Feb 2025 12:11:28 +0800 Subject: [PATCH 2/4] fix(ptracer): fix vmReadStr logic error --- ptracer/context_helper_linux.go | 39 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/ptracer/context_helper_linux.go b/ptracer/context_helper_linux.go index 3fb8056..5a0aad7 100644 --- a/ptracer/context_helper_linux.go +++ b/ptracer/context_helper_linux.go @@ -36,30 +36,37 @@ func getIovecs(base *byte, l int) []unix.Iovec { } func vmReadStr(pid int, addr uintptr, buff []byte) error { - // Deal with unaligned addr - n := 0 - r := pageSize - int(addr%uintptr(pageSize)) - if r == 0 { - r = pageSize + // Handle unaligned address: calculate remaining bytes to page boundary + totalRead := 0 // Total bytes read so far + // Calculate distance to next page boundary, nextRead is the number of bytes to read + nextRead := pageSize - int(addr%uintptr(pageSize)) + if nextRead == 0 { + nextRead = pageSize // If exactly at page boundary, use full page size } + // Read in a loop until buffer is full or termination condition is met for len(buff) > 0 { - if l := len(buff); r < l { - r = l + // If remaining buffer is smaller than planned read size, reduce read size + if restToRead := len(buff); restToRead < nextRead { + nextRead = restToRead } - nn, err := vmRead(pid, addr+uintptr(n), buff[:r]) + // Read data from current position + curRead, err := vmRead(pid, addr+uintptr(totalRead), buff[:nextRead]) if err != nil { - return err + return err // Read error + } + if curRead == 0 { + break // No more data to read + } + if hasNull(buff[:curRead]) { + break // Found string terminator } - if hasNull(buff[:nn]) { - return nil - } - - n += nn - buff = buff[nn:] - r = pageSize + // Update counters and buffer + totalRead += curRead // Update total bytes read + buff = buff[curRead:] // Move buffer pointer + nextRead = pageSize // Reset to full page size } return nil } From 9df6a51947f48a17cf0bf865a32e04250f519d28 Mon Sep 17 00:00:00 2001 From: ZQ Date: Tue, 11 Feb 2025 13:14:49 +0800 Subject: [PATCH 3/4] fix(ptracer): delete min func --- ptracer/context_helper_linux_test.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ptracer/context_helper_linux_test.go b/ptracer/context_helper_linux_test.go index 7eba76a..633b938 100644 --- a/ptracer/context_helper_linux_test.go +++ b/ptracer/context_helper_linux_test.go @@ -254,10 +254,3 @@ func TestSliceBehavior(t *testing.T) { }) } } - -func min(a, b int) int { - if a < b { - return a - } - return b -} From 25c32240cfe18efc464fcf94896cabb2b6d4d2c6 Mon Sep 17 00:00:00 2001 From: ZQ Date: Tue, 11 Feb 2025 13:45:35 +0800 Subject: [PATCH 4/4] test(ptracer): add helper func to find readable memory --- ptracer/context_helper_linux_test.go | 118 +++++++++++++-------------- 1 file changed, 56 insertions(+), 62 deletions(-) diff --git a/ptracer/context_helper_linux_test.go b/ptracer/context_helper_linux_test.go index 633b938..b1e7dc7 100644 --- a/ptracer/context_helper_linux_test.go +++ b/ptracer/context_helper_linux_test.go @@ -66,61 +66,66 @@ func createTestProcess(t *testing.T) (int, func()) { return cmd.Process.Pid, cleanup } -// TestVmRead tests the vmRead function -func TestVmRead(t *testing.T) { - pid, cleanup := createTestProcess(t) - defer cleanup() - - // create test data - testData := []byte("Hello, World!") - buff := make([]byte, len(testData)) - - // get process memory maps +// findReadableMemoryRegion finds a readable memory region in the process's address space +func findReadableMemoryRegion(t *testing.T, pid int, minSize int) uintptr { maps, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid)) if err != nil { t.Fatalf("Failed to read process maps: %v", err) } - // parse memory maps to find a readable address - var addr uintptr for _, line := range bytes.Split(maps, []byte{'\n'}) { if len(line) == 0 { continue } - if bytes.Contains(line, []byte("r-x")) { // find a readable segment - var start uint64 - fmt.Sscanf(string(line), "%x-", &start) - addr = uintptr(start) - break + if bytes.Contains(line, []byte("r-x")) { + // Parse address range: start-end + var start, end uint64 + _, err := fmt.Sscanf(string(line), "%x-%x", &start, &end) + if err != nil { + continue + } + + // Calculate region size + size := end - start + if size >= uint64(minSize) { + return uintptr(start) + } } } - if addr == 0 { - t.Fatal("Failed to find readable memory region") - } + t.Fatalf("Failed to find readable memory region with minimum size %d", minSize) + return 0 +} - // test reading - n, err := vmRead(pid, addr, buff) +// TestVmRead tests the vmRead function +func TestVmRead(t *testing.T) { + pid, cleanup := createTestProcess(t) + defer cleanup() + + buff := make([]byte, 100) + // Ensure the found memory region is large enough + baseAddr := findReadableMemoryRegion(t, pid, len(buff)) + + n, err := vmRead(pid, baseAddr, buff) if err != nil { - t.Fatalf("vmRead failed: %v", err) + t.Errorf("vmRead() error = %v", err) } if n == 0 { t.Error("vmRead returned 0 bytes") } } -// TestVmReadStr tests the vmReadStr function -func TestVmReadStr(t *testing.T) { - pid, cleanup := createTestProcess(t) - defer cleanup() +// vmReadStrTestCase defines a test case for vmReadStr +type vmReadStrTestCase struct { + name string + buffSize int + addrAlign uintptr // address alignment, used to test different alignment scenarios + wantErr bool +} - // test cases - testCases := []struct { - name string - buffSize int - addrAlign uintptr // address alignment, used to test different alignment scenarios - wantErr bool - }{ +// getVmReadStrTestCases returns test cases for vmReadStr testing +func getVmReadStrTestCases() []vmReadStrTestCase { + return []vmReadStrTestCase{ { name: "small_buffer_aligned", buffSize: 10, @@ -158,53 +163,42 @@ func TestVmReadStr(t *testing.T) { wantErr: false, }, } +} + +// TestVmReadStr tests the vmReadStr function +func TestVmReadStr(t *testing.T) { + pid, cleanup := createTestProcess(t) + defer cleanup() + + testCases := getVmReadStrTestCases() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { buff := make([]byte, tc.buffSize) - // get a readable memory address - maps, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid)) - if err != nil { - t.Fatalf("Failed to read process maps: %v", err) - } + // Ensure the found memory region is large enough + baseAddr := findReadableMemoryRegion(t, pid, tc.buffSize) - var baseAddr uintptr - for _, line := range bytes.Split(maps, []byte{'\n'}) { - if len(line) == 0 { - continue - } - if bytes.Contains(line, []byte("r-x")) { - var start uint64 - fmt.Sscanf(string(line), "%x-", &start) - baseAddr = uintptr(start) - break - } - } - - if baseAddr == 0 { - t.Fatal("Failed to find readable memory region") - } - - // use test case specified alignment offset + // Use test case specified alignment offset testAddr := baseAddr + tc.addrAlign - // record buffer content before reading + // Record buffer content before reading originalBuff := make([]byte, len(buff)) copy(originalBuff, buff) - err = vmReadStr(pid, testAddr, buff) + err := vmReadStr(pid, testAddr, buff) if (err != nil) != tc.wantErr { t.Errorf("vmReadStr() error = %v, wantErr %v", err, tc.wantErr) } - // verify if actual reading occurred + // Verify reading results if !bytes.Equal(buff, originalBuff) { - // at least some data was read + // For vmReadStr, we only care that some data was read + // and that the function completed without error t.Logf("Data was read successfully for case: %s", tc.name) } - // special case: check buffer size smaller than distance to boundary + // Special case: check buffer size smaller than distance to boundary if tc.name == "buffer_smaller_than_to_boundary" { distToBoundary := pageSize - int(testAddr%uintptr(pageSize)) if distToBoundary > len(buff) {