Skip to content

Commit 68364d3

Browse files
committed
fix(symbolizer): Introduce per-process module maps
Having multiple processes share the same module maps is a call for chaos. For this reason, each process now has its distinct map and the export directory contains cached export entries.
1 parent 81c4832 commit 68364d3

File tree

8 files changed

+342
-221
lines changed

8 files changed

+342
-221
lines changed

internal/etw/source_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,7 @@ func (s *NoopPsSnapshotter) GetSnapshot() []*pstypes.PS
805805
func (s *NoopPsSnapshotter) AddThread(evt *event.Event) error { return nil }
806806
func (s *NoopPsSnapshotter) AddModule(evt *event.Event) error { return nil }
807807
func (s *NoopPsSnapshotter) FindModule(addr va.Address) (bool, *pstypes.Module) { return false, nil }
808+
func (s *NoopPsSnapshotter) FindAllModules() map[string]pstypes.Module { return nil }
808809
func (s *NoopPsSnapshotter) RemoveThread(pid uint32, tid uint32) error { return nil }
809810
func (s *NoopPsSnapshotter) RemoveModule(pid uint32, addr va.Address) error { return nil }
810811
func (s *NoopPsSnapshotter) WriteFromCapture(evt *event.Event) error { return nil }

pkg/ps/snapshotter.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type Snapshotter interface {
5555
// FindModule traverses loaded modules of all processes in the snapshot and
5656
// if there is module with the specified base address, it returns its metadata.
5757
FindModule(addr va.Address) (bool, *pstypes.Module)
58+
// FindAllModules finds all unique modules across the snapshotter state.
59+
FindAllModules() map[string]pstypes.Module
5860
// FindAndPut attempts to retrieve process' state for the specified process identifier.
5961
// If the process is found, the snapshotter state is updated with the new process.
6062
FindAndPut(pid uint32) *pstypes.PS

pkg/ps/snapshotter_mock.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ func (s *SnapshotterMock) FindModule(addr va.Address) (bool, *pstypes.Module) {
5757
return args.Bool(0), nil
5858
}
5959

60+
func (s *SnapshotterMock) FindAllModules() map[string]pstypes.Module {
61+
args := s.Called()
62+
return args.Get(0).(map[string]pstypes.Module)
63+
}
64+
6065
// FindAndPut method
6166
func (s *SnapshotterMock) FindAndPut(pid uint32) *pstypes.PS {
6267
args := s.Called(pid)

pkg/ps/snapshotter_windows.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,21 @@ func (s *snapshotter) FindModule(addr va.Address) (bool, *pstypes.Module) {
320320
return false, nil
321321
}
322322

323+
func (s *snapshotter) FindAllModules() map[string]pstypes.Module {
324+
s.mu.RLock()
325+
defer s.mu.RUnlock()
326+
mods := make(map[string]pstypes.Module)
327+
for _, proc := range s.procs {
328+
for _, mod := range proc.Modules {
329+
if _, ok := mods[mod.Name]; ok {
330+
continue
331+
}
332+
mods[mod.Name] = mod
333+
}
334+
}
335+
return mods
336+
}
337+
323338
func (s *snapshotter) AddMmap(e *event.Event) error {
324339
s.mu.Lock()
325340
defer s.mu.Unlock()

pkg/symbolize/exports.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2021-present by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package symbolize
20+
21+
import (
22+
"sync"
23+
"time"
24+
25+
"github.com/rabbitstack/fibratus/pkg/ps"
26+
"github.com/rabbitstack/fibratus/pkg/util/va"
27+
log "github.com/sirupsen/logrus"
28+
)
29+
30+
// ModuleExports contains exports for the specific module
31+
// indexed by RVA (Relative Virtual Address).
32+
type ModuleExports struct {
33+
exps map[uint32]string
34+
}
35+
36+
// SymbolFromRVA finds the closest export address before RVA.
37+
func (m *ModuleExports) SymbolFromRVA(rva va.Address) string {
38+
var exp uint32
39+
for f := range m.exps {
40+
if uint64(f) <= rva.Uint64() {
41+
if exp < f {
42+
exp = f
43+
}
44+
}
45+
}
46+
if exp != 0 {
47+
sym, ok := m.exps[exp]
48+
if ok && sym == "" {
49+
return "?"
50+
}
51+
return sym
52+
}
53+
return ""
54+
}
55+
56+
// ExportsDirectoryCache stores the cached module exports extracted
57+
// from the PE export directory.
58+
type ExportsDirectoryCache struct {
59+
sync.RWMutex
60+
exports map[string]*ModuleExports
61+
62+
purger *time.Ticker
63+
quit chan struct{}
64+
65+
psnap ps.Snapshotter
66+
}
67+
68+
// NewExportsDirectoryCache returns a fresh instance of the exports directory cache.
69+
func NewExportsDirectoryCache(psnap ps.Snapshotter) *ExportsDirectoryCache {
70+
c := &ExportsDirectoryCache{
71+
exports: make(map[string]*ModuleExports),
72+
purger: time.NewTicker(time.Minute * 20),
73+
quit: make(chan struct{}, 1),
74+
psnap: psnap,
75+
}
76+
return c
77+
}
78+
79+
// Exports returns the exports for the given module path. If
80+
// the exports can't be find, then the module PE is parsed
81+
// and the exports cache updated.
82+
func (c *ExportsDirectoryCache) Exports(mod string) (*ModuleExports, bool) {
83+
c.RLock()
84+
exports, ok := c.exports[mod]
85+
c.RUnlock()
86+
if ok {
87+
return exports, true
88+
}
89+
pe, err := parsePeFile(mod)
90+
if err != nil {
91+
return nil, false
92+
}
93+
c.Lock()
94+
defer c.Unlock()
95+
exports = &ModuleExports{exps: pe.Exports}
96+
c.exports[mod] = exports
97+
return exports, true
98+
}
99+
100+
// Clear removes all module exports from the directory cache.
101+
func (c *ExportsDirectoryCache) Clear() {
102+
c.Lock()
103+
defer c.Unlock()
104+
c.exports = make(map[string]*ModuleExports)
105+
}
106+
107+
// RemoveExports removes all exports associated with the module.
108+
func (c *ExportsDirectoryCache) RemoveExports(mod string) {
109+
c.Lock()
110+
defer c.Unlock()
111+
delete(c.exports, mod)
112+
}
113+
114+
func (c *ExportsDirectoryCache) purge() {
115+
for {
116+
select {
117+
case <-c.purger.C:
118+
c.clearExports()
119+
case <-c.quit:
120+
return
121+
}
122+
}
123+
}
124+
125+
// clearExports purges all module exports that
126+
// don't exist in the global snapshotter state.
127+
func (c *ExportsDirectoryCache) clearExports() {
128+
mods := c.psnap.FindAllModules()
129+
c.Lock()
130+
defer c.Unlock()
131+
for exp := range c.exports {
132+
if _, ok := mods[exp]; !ok {
133+
log.Debugf("removing stale export %s from directory cache", exp)
134+
delete(c.exports, exp)
135+
}
136+
}
137+
}

pkg/symbolize/exports_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2021-present by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package symbolize
20+
21+
import (
22+
"testing"
23+
24+
"github.com/rabbitstack/fibratus/pkg/util/va"
25+
"github.com/stretchr/testify/assert"
26+
)
27+
28+
func TestSymbolFromRVA(t *testing.T) {
29+
var tests = []struct {
30+
rva va.Address
31+
exports map[uint32]string
32+
expectedSymbol string
33+
}{
34+
{va.Address(317949), map[uint32]string{
35+
9824: "SHCreateScopeItemFromShellItem",
36+
23248: "SHCreateScopeItemFromIDList",
37+
165392: "DllGetClassObject",
38+
186368: "SHCreateSearchIDListFromAutoList",
39+
238048: "DllCanUnloadNow",
40+
240112: "IsShellItemInSearchIndex",
41+
240304: "IsMSSearchEnabled",
42+
272336: "SHSaveBinaryAutoListToStream",
43+
310672: "DllMain",
44+
317920: "",
45+
320864: "",
46+
434000: "SHCreateAutoList",
47+
434016: "SHCreateAutoListWithID",
48+
555040: "CreateDefaultProviderResolver",
49+
571136: "GetGatherAdmin",
50+
572592: "SEARCH_RemoteLocationsCscStateCache_IsRemoteLocationInCsc"},
51+
"?",
52+
},
53+
{va.Address(434011), map[uint32]string{
54+
9824: "SHCreateScopeItemFromShellItem",
55+
23248: "SHCreateScopeItemFromIDList",
56+
165392: "DllGetClassObject",
57+
186368: "SHCreateSearchIDListFromAutoList",
58+
238048: "DllCanUnloadNow",
59+
240112: "IsShellItemInSearchIndex",
60+
240304: "IsMSSearchEnabled",
61+
272336: "SHSaveBinaryAutoListToStream",
62+
310672: "DllMain",
63+
317920: "",
64+
320864: "",
65+
434000: "SHCreateAutoList",
66+
434016: "SHCreateAutoListWithID",
67+
555040: "CreateDefaultProviderResolver",
68+
571136: "GetGatherAdmin",
69+
572592: "SEARCH_RemoteLocationsCscStateCache_IsRemoteLocationInCsc"},
70+
"SHCreateAutoList",
71+
},
72+
{va.Address(4532), map[uint32]string{
73+
9824: "SHCreateScopeItemFromShellItem",
74+
23248: "SHCreateScopeItemFromIDList",
75+
165392: "DllGetClassObject",
76+
186368: "SHCreateSearchIDListFromAutoList",
77+
238048: "DllCanUnloadNow",
78+
240112: "IsShellItemInSearchIndex",
79+
240304: "IsMSSearchEnabled",
80+
572592: "SEARCH_RemoteLocationsCscStateCache_IsRemoteLocationInCsc"},
81+
"",
82+
},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.expectedSymbol, func(t *testing.T) {
87+
exps := &ModuleExports{exps: tt.exports}
88+
assert.Equal(t, tt.expectedSymbol, exps.SymbolFromRVA(tt.rva))
89+
})
90+
}
91+
}

0 commit comments

Comments
 (0)