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 } diff --git a/ptracer/context_helper_linux_test.go b/ptracer/context_helper_linux_test.go new file mode 100644 index 0000000..b1e7dc7 --- /dev/null +++ b/ptracer/context_helper_linux_test.go @@ -0,0 +1,250 @@ +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 +} + +// 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) + } + + for _, line := range bytes.Split(maps, []byte{'\n'}) { + if len(line) == 0 { + continue + } + 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) + } + } + } + + t.Fatalf("Failed to find readable memory region with minimum size %d", minSize) + return 0 +} + +// 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.Errorf("vmRead() error = %v", err) + } + if n == 0 { + t.Error("vmRead returned 0 bytes") + } +} + +// 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 +} + +// getVmReadStrTestCases returns test cases for vmReadStr testing +func getVmReadStrTestCases() []vmReadStrTestCase { + return []vmReadStrTestCase{ + { + 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, + }, + } +} + +// 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) + + // Ensure the found memory region is large enough + baseAddr := findReadableMemoryRegion(t, pid, tc.buffSize) + + // 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 reading results + if !bytes.Equal(buff, originalBuff) { + // 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 + 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)) + } + }) + } +}