From 0f9bc8f2c3011cd84e3b836870a4f04e1728e475 Mon Sep 17 00:00:00 2001 From: Luca Corbatto Date: Wed, 1 Jun 2022 16:11:50 +0200 Subject: [PATCH] Fixes #24 --- procio/memory_linux.go | 23 +++- procio/memory_linux_test.go | 142 +++++++++++++++++++ procio/mock_CachingProcess_test.go | 175 ++++++++++++++++++++++++ procio/mock_MemoryReaderFactory_test.go | 47 +++++++ procio/mock_MemoryReader_test.go | 80 +++++++++++ procio/mock_Process_test.go | 170 +++++++++++++++++++++++ procio/mock_memoryReaderImpl_test.go | 112 +++++++++++++++ procio/process_linux.go | 6 +- procio/procio.go | 2 + 9 files changed, 755 insertions(+), 2 deletions(-) create mode 100644 procio/mock_CachingProcess_test.go create mode 100644 procio/mock_MemoryReaderFactory_test.go create mode 100644 procio/mock_MemoryReader_test.go create mode 100644 procio/mock_Process_test.go create mode 100644 procio/mock_memoryReaderImpl_test.go create mode 100644 procio/procio.go diff --git a/procio/memory_linux.go b/procio/memory_linux.go index 6fd04db..39b317c 100644 --- a/procio/memory_linux.go +++ b/procio/memory_linux.go @@ -7,6 +7,7 @@ import ( "bufio" "fmt" "io" + "os" "regexp" "strconv" "strings" @@ -106,7 +107,7 @@ func stateSegmentHead(in *bufio.Reader, out chan<- *MemorySegmentInfo, lastSeg * } func parseSegmentHead(line string) (*MemorySegmentInfo, error) { - line = strings.TrimSpace(line) + line = strings.TrimRight(line, "\n") matches := segmentHeadRegex.FindStringSubmatch(line) if len(matches) != expectedFieldCount { return nil, fmt.Errorf("invalid format \"%s\"", line) @@ -228,6 +229,26 @@ func parseBytes(value string) (uintptr, error) { return uintptr(amountUint) * detailUnitMultiplier, nil } +func sanitizeMappedFile(proc Process, seg *MemorySegmentInfo) { + if seg.MappedFile == nil { + return + } + originalPath := seg.MappedFile.Path() + if strings.Contains(originalPath, "\\012") { + // newline characters are encoded as '\012', but we cannot distinguish between an actual + // literal '\012' or it representing the newline character => lookup the link + linkPath := fmt.Sprintf("%s/%d/map_files/%x-%x", procPath, proc.PID(), seg.BaseAddress, seg.BaseAddress+seg.Size) + actualPath, err := os.Readlink(linkPath) + if err != nil { + // Could not read the link => leave it as-is + return + } + if originalPath != actualPath { + seg.MappedFile = fileio.NewFile(actualPath) + } + } +} + // PermissionsToNative converts the given Permissions to the // native linux representation. func PermissionsToNative(perms Permissions) int { diff --git a/procio/memory_linux_test.go b/procio/memory_linux_test.go index f39f7cf..a30ddbd 100644 --- a/procio/memory_linux_test.go +++ b/procio/memory_linux_test.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "io" + "os" "testing" "github.com/fkie-cad/yapscan/fileio" @@ -160,6 +161,52 @@ func TestParseSegmentHead(t *testing.T) { }) }) + Convey("A private file-backed segment with whitespaces should work", t, func() { + info, err := parseSegmentHead("00400000-0048a000 r-xp 00000000 fd:03 960637 /bin/some path/with whitespaces.txt") + So(err, ShouldBeNil) + So(info, ShouldResemble, &MemorySegmentInfo{ + ParentBaseAddress: 0x400000, + BaseAddress: 0x400000, + AllocatedPermissions: Permissions{ + Read: true, Execute: true, + }, + CurrentPermissions: Permissions{ + Read: true, Execute: true, + }, + Size: 0x8a000, + RSS: 0, + State: StateCommit, + Type: SegmentTypePrivateMapped, + MappedFile: &fileio.OSFile{ + FilePath: "/bin/some path/with whitespaces.txt", + }, + SubSegments: []*MemorySegmentInfo{}, + }) + }) + + Convey("A private file-backed segment with trailing whitespace should work", t, func() { + info, err := parseSegmentHead("00400000-0048a000 r-xp 00000000 fd:03 960637 /bin/bash ") + So(err, ShouldBeNil) + So(info, ShouldResemble, &MemorySegmentInfo{ + ParentBaseAddress: 0x400000, + BaseAddress: 0x400000, + AllocatedPermissions: Permissions{ + Read: true, Execute: true, + }, + CurrentPermissions: Permissions{ + Read: true, Execute: true, + }, + Size: 0x8a000, + RSS: 0, + State: StateCommit, + Type: SegmentTypePrivateMapped, + MappedFile: &fileio.OSFile{ + FilePath: "/bin/bash ", + }, + SubSegments: []*MemorySegmentInfo{}, + }) + }) + Convey("A shared file-backed segment should work", t, func() { info, err := parseSegmentHead("00400000-0048a000 rwxs 00000000 fd:03 960637 /bin/bash") So(err, ShouldBeNil) @@ -351,3 +398,98 @@ func TestPermissionsToNative(t *testing.T) { }) } } + +func TestSanitizeMappedFile(t *testing.T) { + Convey("Sanitizing a segment without a mapped file should do nothing", t, func() { + proc := NewMockProcess(t) + + seg := &MemorySegmentInfo{} + sanitizeMappedFile(proc, seg) + So(seg, ShouldResemble, &MemorySegmentInfo{}) + }) + + Convey("Sanitizing a segment with a mapped file", t, func() { + Convey("without any special escapes should do nothing", func() { + proc := NewMockProcess(t) + + seg := &MemorySegmentInfo{ + MappedFile: fileio.NewFile("/some/normal/path"), + } + + sanitizeMappedFile(proc, seg) + + So(seg, ShouldResemble, &MemorySegmentInfo{ + MappedFile: fileio.NewFile("/some/normal/path"), + }) + }) + + Convey("with a newline escape sequence", func() { + pid := 42 + proc := NewMockProcess(t) + proc.On("PID").Return(pid) + + Convey("where the link is non-existent should do nothing", func() { + seg := &MemorySegmentInfo{ + MappedFile: fileio.NewFile("/path/withEscapeSequence\\012"), + } + + sanitizeMappedFile(proc, seg) + + So(seg, ShouldResemble, &MemorySegmentInfo{ + MappedFile: fileio.NewFile("/path/withEscapeSequence\\012"), + }) + }) + + Convey("but the link shows its a literal '\\012' should do nothing", func() { + origProcPath := procPath + defer func() { + procPath = origProcPath + }() + + tempdir := t.TempDir() + procPath = tempdir + + mappedName := "/path/withEscapeSequence\\012/but/notANewline" + seg := &MemorySegmentInfo{ + MappedFile: fileio.NewFile(mappedName), + } + + mapFilesPath := fmt.Sprintf("%s/%d/map_files", tempdir, pid) + os.MkdirAll(mapFilesPath, 0700) + os.Symlink(mappedName, fmt.Sprintf("%s/%x-%x", mapFilesPath, seg.BaseAddress, seg.BaseAddress+seg.Size)) + + sanitizeMappedFile(proc, seg) + + So(seg, ShouldResemble, &MemorySegmentInfo{ + MappedFile: fileio.NewFile(mappedName), + }) + }) + + Convey("and its a newline character should replace the path", func() { + origProcPath := procPath + defer func() { + procPath = origProcPath + }() + + tempdir := t.TempDir() + procPath = tempdir + + mappedName := "/path/withEscapeSequence\\012/asNewline" + readName := "/path/withEscapeSequence\n/asNewline" + seg := &MemorySegmentInfo{ + MappedFile: fileio.NewFile(mappedName), + } + + mapFilesPath := fmt.Sprintf("%s/%d/map_files", tempdir, pid) + os.MkdirAll(mapFilesPath, 0700) + os.Symlink(readName, fmt.Sprintf("%s/%x-%x", mapFilesPath, seg.BaseAddress, seg.BaseAddress+seg.Size)) + + sanitizeMappedFile(proc, seg) + + So(seg, ShouldResemble, &MemorySegmentInfo{ + MappedFile: fileio.NewFile(readName), + }) + }) + }) + }) +} diff --git a/procio/mock_CachingProcess_test.go b/procio/mock_CachingProcess_test.go new file mode 100644 index 0000000..58366a3 --- /dev/null +++ b/procio/mock_CachingProcess_test.go @@ -0,0 +1,175 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package procio + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// MockCachingProcess is an autogenerated mock type for the CachingProcess type +type MockCachingProcess struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *MockCachingProcess) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Crash provides a mock function with given fields: _a0 +func (_m *MockCachingProcess) Crash(_a0 CrashMethod) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(CrashMethod) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Handle provides a mock function with given fields: +func (_m *MockCachingProcess) Handle() interface{} { + ret := _m.Called() + + var r0 interface{} + if rf, ok := ret.Get(0).(func() interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// Info provides a mock function with given fields: +func (_m *MockCachingProcess) Info() (*ProcessInfo, error) { + ret := _m.Called() + + var r0 *ProcessInfo + if rf, ok := ret.Get(0).(func() *ProcessInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ProcessInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InvalidateCache provides a mock function with given fields: +func (_m *MockCachingProcess) InvalidateCache() { + _m.Called() +} + +// MemorySegments provides a mock function with given fields: +func (_m *MockCachingProcess) MemorySegments() ([]*MemorySegmentInfo, error) { + ret := _m.Called() + + var r0 []*MemorySegmentInfo + if rf, ok := ret.Get(0).(func() []*MemorySegmentInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*MemorySegmentInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PID provides a mock function with given fields: +func (_m *MockCachingProcess) PID() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Resume provides a mock function with given fields: +func (_m *MockCachingProcess) Resume() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// String provides a mock function with given fields: +func (_m *MockCachingProcess) String() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Suspend provides a mock function with given fields: +func (_m *MockCachingProcess) Suspend() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockCachingProcess creates a new instance of MockCachingProcess. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockCachingProcess(t testing.TB) *MockCachingProcess { + mock := &MockCachingProcess{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/procio/mock_MemoryReaderFactory_test.go b/procio/mock_MemoryReaderFactory_test.go new file mode 100644 index 0000000..67a7c2b --- /dev/null +++ b/procio/mock_MemoryReaderFactory_test.go @@ -0,0 +1,47 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package procio + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// MockMemoryReaderFactory is an autogenerated mock type for the MemoryReaderFactory type +type MockMemoryReaderFactory struct { + mock.Mock +} + +// NewMemoryReader provides a mock function with given fields: proc, seg +func (_m *MockMemoryReaderFactory) NewMemoryReader(proc Process, seg *MemorySegmentInfo) (MemoryReader, error) { + ret := _m.Called(proc, seg) + + var r0 MemoryReader + if rf, ok := ret.Get(0).(func(Process, *MemorySegmentInfo) MemoryReader); ok { + r0 = rf(proc, seg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(MemoryReader) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(Process, *MemorySegmentInfo) error); ok { + r1 = rf(proc, seg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMockMemoryReaderFactory creates a new instance of MockMemoryReaderFactory. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockMemoryReaderFactory(t testing.TB) *MockMemoryReaderFactory { + mock := &MockMemoryReaderFactory{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/procio/mock_MemoryReader_test.go b/procio/mock_MemoryReader_test.go new file mode 100644 index 0000000..1044a95 --- /dev/null +++ b/procio/mock_MemoryReader_test.go @@ -0,0 +1,80 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package procio + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// MockMemoryReader is an autogenerated mock type for the MemoryReader type +type MockMemoryReader struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *MockMemoryReader) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Read provides a mock function with given fields: p +func (_m *MockMemoryReader) Read(p []byte) (int, error) { + ret := _m.Called(p) + + var r0 int + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Seek provides a mock function with given fields: offset, whence +func (_m *MockMemoryReader) Seek(offset int64, whence int) (int64, error) { + ret := _m.Called(offset, whence) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64, int) int64); ok { + r0 = rf(offset, whence) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, int) error); ok { + r1 = rf(offset, whence) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMockMemoryReader creates a new instance of MockMemoryReader. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockMemoryReader(t testing.TB) *MockMemoryReader { + mock := &MockMemoryReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/procio/mock_Process_test.go b/procio/mock_Process_test.go new file mode 100644 index 0000000..d9e0f3b --- /dev/null +++ b/procio/mock_Process_test.go @@ -0,0 +1,170 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package procio + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// MockProcess is an autogenerated mock type for the Process type +type MockProcess struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *MockProcess) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Crash provides a mock function with given fields: _a0 +func (_m *MockProcess) Crash(_a0 CrashMethod) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(CrashMethod) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Handle provides a mock function with given fields: +func (_m *MockProcess) Handle() interface{} { + ret := _m.Called() + + var r0 interface{} + if rf, ok := ret.Get(0).(func() interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// Info provides a mock function with given fields: +func (_m *MockProcess) Info() (*ProcessInfo, error) { + ret := _m.Called() + + var r0 *ProcessInfo + if rf, ok := ret.Get(0).(func() *ProcessInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ProcessInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MemorySegments provides a mock function with given fields: +func (_m *MockProcess) MemorySegments() ([]*MemorySegmentInfo, error) { + ret := _m.Called() + + var r0 []*MemorySegmentInfo + if rf, ok := ret.Get(0).(func() []*MemorySegmentInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*MemorySegmentInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PID provides a mock function with given fields: +func (_m *MockProcess) PID() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Resume provides a mock function with given fields: +func (_m *MockProcess) Resume() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// String provides a mock function with given fields: +func (_m *MockProcess) String() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Suspend provides a mock function with given fields: +func (_m *MockProcess) Suspend() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockProcess creates a new instance of MockProcess. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockProcess(t testing.TB) *MockProcess { + mock := &MockProcess{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/procio/mock_memoryReaderImpl_test.go b/procio/mock_memoryReaderImpl_test.go new file mode 100644 index 0000000..6c34c46 --- /dev/null +++ b/procio/mock_memoryReaderImpl_test.go @@ -0,0 +1,112 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package procio + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// mockMemoryReaderImpl is an autogenerated mock type for the memoryReaderImpl type +type mockMemoryReaderImpl struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *mockMemoryReaderImpl) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Process provides a mock function with given fields: +func (_m *mockMemoryReaderImpl) Process() Process { + ret := _m.Called() + + var r0 Process + if rf, ok := ret.Get(0).(func() Process); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(Process) + } + } + + return r0 +} + +// Read provides a mock function with given fields: p +func (_m *mockMemoryReaderImpl) Read(p []byte) (int, error) { + ret := _m.Called(p) + + var r0 int + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Seek provides a mock function with given fields: offset, whence +func (_m *mockMemoryReaderImpl) Seek(offset int64, whence int) (int64, error) { + ret := _m.Called(offset, whence) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64, int) int64); ok { + r0 = rf(offset, whence) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, int) error); ok { + r1 = rf(offset, whence) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Segment provides a mock function with given fields: +func (_m *mockMemoryReaderImpl) Segment() *MemorySegmentInfo { + ret := _m.Called() + + var r0 *MemorySegmentInfo + if rf, ok := ret.Get(0).(func() *MemorySegmentInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*MemorySegmentInfo) + } + } + + return r0 +} + +// newMockMemoryReaderImpl creates a new instance of mockMemoryReaderImpl. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func newMockMemoryReaderImpl(t testing.TB) *mockMemoryReaderImpl { + mock := &mockMemoryReaderImpl{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/procio/process_linux.go b/procio/process_linux.go index 52fcab0..f1ea483 100644 --- a/procio/process_linux.go +++ b/procio/process_linux.go @@ -186,7 +186,11 @@ func (p *processLinux) MemorySegments() ([]*MemorySegmentInfo, error) { } defer smaps.Close() - return parseSMEMFile(smaps) + segments, err := parseSMEMFile(smaps) + for _, seg := range segments { + sanitizeMappedFile(p, seg) + } + return segments, err } func (p *processLinux) Crash(m CrashMethod) error { diff --git a/procio/procio.go b/procio/procio.go new file mode 100644 index 0000000..b2f69fe --- /dev/null +++ b/procio/procio.go @@ -0,0 +1,2 @@ +//go:generate mockery --inpackage --testonly --name ".*" +package procio