Compare commits

..

No commits in common. "d15586390a95a4b7577fb6b23f08f8fe033376ce" and "3672fd455b3f9c6730a8af28e666370841e676ae" have entirely different histories.

6 changed files with 140 additions and 564 deletions

View File

@ -1,304 +1,149 @@
// +build !withcgo
package ebpf package ebpf
/*
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <linux/unistd.h>
#include <linux/bpf.h>
// See the bpf man page for details on the following functions
static int bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
{
return syscall(__NR_bpf, cmd, attr, size);
}
static int bpf_create_map(
enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
int bpf_get_next_key(int fd, const void *key, void *next_key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.next_key = (__u64) (unsigned long) next_key};
return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}
int bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
int bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}
*/
import "C"
import ( import (
"fmt" "fmt"
"syscall" "syscall"
"unsafe" "unsafe"
) )
// All constants are taken from /usr/include/linux/bpf.h
const BPF_SYSCALL = 321
type bpf_cmd int
const (
BPF_MAP_CREATE bpf_cmd = iota
BPF_MAP_LOOKUP_ELEM
BPF_MAP_UPDATE_ELEM
BPF_MAP_DELETE_ELEM
BPF_MAP_GET_NEXT_KEY
BPF_PROG_LOAD
BPF_OBJ_PIN
BPF_OBJ_GET
BPF_PROG_ATTACH
BPF_PROG_DETACH
BPF_PROG_TEST_RUN
BPF_PROG_GET_NEXT_ID
BPF_MAP_GET_NEXT_ID
BPF_PROG_GET_FD_BY_ID
BPF_MAP_GET_FD_BY_ID
BPF_OBJ_GET_INFO_BY_FD
BPF_PROG_QUERY
BPF_RAW_TRACEPOINT_OPEN
BPF_BTF_LOAD
BPF_BTF_GET_FD_BY_ID
BPF_TASK_FD_QUERY
BPF_MAP_LOOKUP_AND_DELETE_ELEM
BPF_MAP_FREEZE
)
type bpf_map_type int
const (
BPF_MAP_TYPE_UNSPEC bpf_map_type = iota
BPF_MAP_TYPE_HASH
BPF_MAP_TYPE_ARRAY
BPF_MAP_TYPE_PROG_ARRAY
BPF_MAP_TYPE_PERF_EVENT_ARRAY
BPF_MAP_TYPE_PERCPU_HASH
BPF_MAP_TYPE_PERCPU_ARRAY
BPF_MAP_TYPE_STACK_TRACE
BPF_MAP_TYPE_CGROUP_ARRAY
BPF_MAP_TYPE_LRU_HASH
BPF_MAP_TYPE_LRU_PERCPU_HASH
BPF_MAP_TYPE_LPM_TRIE
BPF_MAP_TYPE_ARRAY_OF_MAPS
BPF_MAP_TYPE_HASH_OF_MAPS
BPF_MAP_TYPE_DEVMAP
BPF_MAP_TYPE_SOCKMAP
BPF_MAP_TYPE_CPUMAP
BPF_MAP_TYPE_XSKMAP
BPF_MAP_TYPE_SOCKHASH
BPF_MAP_TYPE_CGROUP_STORAGE
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE
BPF_MAP_TYPE_QUEUE
BPF_MAP_TYPE_STACK
BPF_MAP_TYPE_SK_STORAGE
)
// MapFD is a file descriptor representing a eBPF map // MapFD is a file descriptor representing a eBPF map
type MapFD uint32 type MapFD int
/*
All methods in this file implement a syscall to bpf one way or another. We
follow the C-API given in bpf(2) and have the corresponding C-functions
embedded as comments. Those all refer to the C-function bpf(), which is a
wrapper for the syscall:
static int bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
{
return syscall(__NR_bpf, cmd, attr, size);
}
Each ouf our methods calls the syscall directly, instead.
*/
// CreateMap creates an eBPF map from int->uint64. The file descriptor of the // CreateMap creates an eBPF map from int->uint64. The file descriptor of the
// created map is returned. // created map is returned.
func CreateMap() (MapFD, error) { func CreateMap() (MapFD, error) {
/*
static int bpf_create_map(
enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr)); r, errno := C.bpf_create_map(C.BPF_MAP_TYPE_HASH, 4, 8, 64)
}
*/
create_attr := struct { if r == -1 {
map_type uint32 return MapFD(r), fmt.Errorf("bpf_create_map with errno %d", errno)
key_size uint32
value_size uint32
max_entries uint32
}{
map_type: uint32(BPF_MAP_TYPE_HASH),
key_size: 4,
value_size: 8,
max_entries: 64,
} }
r, _, err := syscall.Syscall(
BPF_SYSCALL,
uintptr(BPF_MAP_CREATE),
uintptr(unsafe.Pointer(&create_attr)),
unsafe.Sizeof(create_attr),
)
if err != 0 {
return 0, err
}
return MapFD(r), nil return MapFD(r), nil
} }
func (mfd MapFD) bpf_get_next_key(key *int, next_key *int) error {
/*
int bpf_get_next_key(int fd, const void *key, void *next_key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.next_key = (__u64) (unsigned long) next_key};
return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}
*/
next_attr := struct {
map_fd MapFD
key uint64
next_key uint64
}{
map_fd: mfd,
key: uint64(uintptr(unsafe.Pointer(key))),
next_key: uint64(uintptr(unsafe.Pointer(next_key))),
}
r, _, err := syscall.Syscall(
BPF_SYSCALL,
uintptr(BPF_MAP_GET_NEXT_KEY),
uintptr(unsafe.Pointer(&next_attr)),
unsafe.Sizeof(next_attr),
)
if r != 0 {
return err
} else {
return nil
}
}
func (mfd MapFD) bpf_lookup_elem(key *int, value *uint64) error {
/*
int bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
*/
lookup_attr := struct {
map_fd MapFD
key uint64
value uint64
}{
map_fd: mfd,
key: uint64(uintptr(unsafe.Pointer(key))),
value: uint64(uintptr(unsafe.Pointer(value))),
}
r, _, err := syscall.Syscall(
BPF_SYSCALL,
uintptr(BPF_MAP_LOOKUP_ELEM),
uintptr(unsafe.Pointer(&lookup_attr)),
unsafe.Sizeof(lookup_attr),
)
if r != 0 {
return err
}
return nil
}
// GetMap gets the key/value pairs from the specified eBPF map as a Go map // GetMap gets the key/value pairs from the specified eBPF map as a Go map
func (mfd MapFD) GetMap() (map[int]uint64, error) { func (mfd MapFD) GetMap() (map[int]uint64, error) {
retMap := make(map[int]uint64) retMap := make(map[int]uint64)
var ( mapFDC := C.int(mfd)
key, next int keyC := C.int(0)
value uint64 nextKeyC := C.int(0)
) valC := C.uint64_t(0)
for { for {
err := mfd.bpf_get_next_key(&key, &next) r, errno := C.bpf_get_next_key(mapFDC, unsafe.Pointer(&keyC), unsafe.Pointer(&nextKeyC))
if err != nil { if r == -1 {
if err == syscall.ENOENT { if errno == syscall.ENOENT {
// The provided key was the last element. We're done iterating. // The provided key was the last element. We're done iterating.
return retMap, nil return retMap, nil
} }
return nil, fmt.Errorf("bpf_get_next_key failed with error %v", err) return nil, fmt.Errorf("bpf_get_next_key failed with errno %d", errno)
} }
err = mfd.bpf_lookup_elem(&next, &value) r = C.bpf_lookup_elem(mapFDC, unsafe.Pointer(&nextKeyC), unsafe.Pointer(&valC))
if err != nil { if r == -1 {
return nil, fmt.Errorf("bpf_lookup_elem failed with error %v", err) return nil, fmt.Errorf("bpf_lookup_elem failed")
} }
retMap[int(nextKeyC)] = uint64(valC)
retMap[next] = value keyC = nextKeyC
key = next
} }
} }
const (
BPF_ANY = iota
BPF_NOEXIST
BPF_EXIST
BPF_F_LOCK
)
// Add puts the (key, value) into the eBPF map, only if the key does not exist // Add puts the (key, value) into the eBPF map, only if the key does not exist
// yet in the map. It returns an error otherwise. // yet in the map. It returns an error otherwise.
func (mfd MapFD) Add(key int, value uint64) error { func (mfd MapFD) Add(key int, value uint64) error {
return mfd.updateElement(key, value, BPF_NOEXIST) return mfd.updateElement(key, value, C.BPF_NOEXIST)
} }
// Change changes the value to an existing key in the eBPF map. It returns an // Change changes the value to an existing key in the eBPF map. It returns an
// error otherwise. // error otherwise.
func (mfd MapFD) Change(key int, value uint64) error { func (mfd MapFD) Change(key int, value uint64) error {
return mfd.updateElement(key, value, BPF_EXIST) return mfd.updateElement(key, value, C.BPF_EXIST)
} }
// Set puts the (key, value) into the eBPF map. It will create or overwrite an // Set puts the (key, value) into the eBPF map. It will create or overwrite an
// existing entry for that key. // existing entry for that key.
func (mfd MapFD) Set(key int, value uint64) error { func (mfd MapFD) Set(key int, value uint64) error {
return mfd.updateElement(key, value, BPF_ANY) return mfd.updateElement(key, value, C.BPF_ANY)
} }
// updateElement is the low level wrapper to bpf_update_elem, used from Add(), // updateElement is the low level wrapper to bpf_update_elem, used from Add(),
// Set() and Change(). // Set() and Change().
func (mfd MapFD) updateElement(key int, value uint64, flag uint64) error { func (mfd MapFD) updateElement(key int, value uint64, flag C.uint64_t) error {
/*
int bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr)); r, errno := C.bpf_update_elem(C.int(mfd),
} unsafe.Pointer(&key),
*/ unsafe.Pointer(&value),
flag)
update_attr := struct { if r == -1 {
map_fd MapFD return fmt.Errorf("bpf_update_elem failed with errno %d", errno)
key uint64
value uint64
flags uint64
}{
map_fd: mfd,
key: uint64(uintptr(unsafe.Pointer(&key))),
value: uint64(uintptr(unsafe.Pointer(&value))),
flags: flag,
} }
r, _, err := syscall.Syscall(
BPF_SYSCALL,
uintptr(BPF_MAP_UPDATE_ELEM),
uintptr(unsafe.Pointer(&update_attr)),
unsafe.Sizeof(update_attr),
)
if r != 0 || err != 0 {
return fmt.Errorf("couldn't update element: %s", err.Error())
}
return nil return nil
} }

View File

@ -1,151 +0,0 @@
// +build withcgo
package ebpf
/*
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <linux/unistd.h>
#include <linux/bpf.h>
// See the bpf man page for details on the following functions
static int bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
{
return syscall(__NR_bpf, cmd, attr, size);
}
static int bpf_create_map(
enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
int bpf_get_next_key(int fd, const void *key, void *next_key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.next_key = (__u64) (unsigned long) next_key};
return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}
int bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
int bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
)
// MapFD is a file descriptor representing a eBPF map
type MapFD int
// CreateMap creates an eBPF map from int->uint64. The file descriptor of the
// created map is returned.
func CreateMap() (MapFD, error) {
r, errno := C.bpf_create_map(C.BPF_MAP_TYPE_HASH, 4, 8, 64)
if r == -1 {
return MapFD(r), fmt.Errorf("bpf_create_map with errno %d", errno)
}
return MapFD(r), nil
}
// GetMap gets the key/value pairs from the specified eBPF map as a Go map
func (mfd MapFD) GetMap() (map[int]uint64, error) {
retMap := make(map[int]uint64)
mapFDC := C.int(mfd)
keyC := C.int(0)
nextKeyC := C.int(0)
valC := C.uint64_t(0)
for {
r, errno := C.bpf_get_next_key(mapFDC, unsafe.Pointer(&keyC), unsafe.Pointer(&nextKeyC))
if r == -1 {
if errno == syscall.ENOENT {
// The provided key was the last element. We're done iterating.
return retMap, nil
}
return nil, fmt.Errorf("bpf_get_next_key failed with errno %d", errno)
}
r = C.bpf_lookup_elem(mapFDC, unsafe.Pointer(&nextKeyC), unsafe.Pointer(&valC))
if r == -1 {
return nil, fmt.Errorf("bpf_lookup_elem failed")
}
retMap[int(nextKeyC)] = uint64(valC)
keyC = nextKeyC
}
}
// Add puts the (key, value) into the eBPF map, only if the key does not exist
// yet in the map. It returns an error otherwise.
func (mfd MapFD) Add(key int, value uint64) error {
return mfd.updateElement(key, value, C.BPF_NOEXIST)
}
// Change changes the value to an existing key in the eBPF map. It returns an
// error otherwise.
func (mfd MapFD) Change(key int, value uint64) error {
return mfd.updateElement(key, value, C.BPF_EXIST)
}
// Set puts the (key, value) into the eBPF map. It will create or overwrite an
// existing entry for that key.
func (mfd MapFD) Set(key int, value uint64) error {
return mfd.updateElement(key, value, C.BPF_ANY)
}
// updateElement is the low level wrapper to bpf_update_elem, used from Add(),
// Set() and Change().
func (mfd MapFD) updateElement(key int, value uint64, flag C.uint64_t) error {
r, errno := C.bpf_update_elem(C.int(mfd),
unsafe.Pointer(&key),
unsafe.Pointer(&value),
flag)
if r == -1 {
return fmt.Errorf("bpf_update_elem failed with errno %d", errno)
}
return nil
}

View File

@ -4,7 +4,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"time"
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/ebpf" "github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/ebpf"
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/symbolyze" "github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/symbolyze"
@ -29,14 +28,11 @@ func main() {
scanner := symbolyze.NewScanner(*symbol, *glob) scanner := symbolyze.NewScanner(*symbol, *glob)
scanner.OnFound(mapFD.Set) scanner.OnFound(mapFD.Set)
scanner.Debug(*debug) if *debug {
scanner.DebugOn()
}
go scanner.RunEvery(time.Second) err = scanner.Run()
time.Sleep(10 * time.Second)
scanner.Stop()
err = scanner.Errors()
if err != nil { if err != nil {
fmt.Printf("Failed to run the symbolyze scanner: %s", err) fmt.Printf("Failed to run the symbolyze scanner: %s", err)
os.Exit(1) os.Exit(1)

View File

@ -1,26 +0,0 @@
// Copyright note...
/*
Package symbolyze provides a mechanism to search for all occurences of a
certain symbol in certain mmap'ed ELF binaries of all running processes.
NewScanner(symbol string, glob string) will setup a Scanner, which will search
for the given symbol name in all mmap'ed ELF-files that match the given
glob-pattern. The glob-pattern is a shell file name pattern, see
filepath.Match.
The scanner should be populated with callback functions via calls to OnFound(),
before starting the scan by calling Run().
For example:
scanner := symbolyze.NewScanner("_PyRuntime", "*python3*")
scanner.OnFound(func(pid int, offset uint64) error {
fmt.Println("found symbol in", pid, "in offset", offset")
return nil
})
err := scanner.Run()
*/
package symbolyze

View File

@ -15,7 +15,7 @@ import (
// test-binary `simple` that is linked against python3.7. If this fails, make // test-binary `simple` that is linked against python3.7. If this fails, make
// necessary adjustments to the Makefile and/or your environment and try again. // necessary adjustments to the Makefile and/or your environment and try again.
func buildSimple() error { func buildSimple() error {
cmd := exec.Command("make", "-s") cmd := exec.Command("make")
cmd.Dir = "testdata" cmd.Dir = "testdata"
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -115,6 +115,5 @@ func extractOffsetWithGdb(pid int, t *testing.T) (offset uint64, err error) {
} }
} }
return 0, fmt.Errorf("Symbol not found with gdb.\n" + return 0, fmt.Errorf("Symbol not found with gdb")
"Maybe /proc/sys/kernel/yama/ptrace_scope must be set to 0? Or try to run the test as root!")
} }

View File

@ -10,7 +10,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
) )
// Scanner represents an engine for scanning for a specific symbol in all // Scanner represents an engine for scanning for a specific symbol in all
@ -25,60 +24,34 @@ import (
// /proc. Whenever a match is found, all observers will be called with the // /proc. Whenever a match is found, all observers will be called with the
// (pid, offset), concurrently. // (pid, offset), concurrently.
type Scanner struct { type Scanner struct {
rwmutex
symbol string symbol string
pathglob string pathglob string
cache map[string]uint64 // Contains (pathname, offset) cache map[string]uint64 // Contains (pathname, offset)
observers []Observer // Callbacks observers []Observer // Callbacks
logger // Embedded logger
// Instead of using a boolean to indicate debugging, we use function // Instead of using a boolean to indicate debugging, we use function
// members. This way we can populate them with noop-functions in the // members. This way we can populate them with noop-functions in the
// non-debug case and not polute the code with if-statements. // non-debug case and not polute the code with if-statements.
debugf func(format string, v ...interface{}) debugf func(format string, v ...interface{})
debugln func(v ...interface{}) debugln func(v ...interface{})
logger // Embedded logger
errors errors err error // error state of the scanner.
ticker *time.Ticker // Used to run the scanner repeatedly
}
type errors []error
func (e errors) Error() string {
switch len(e) {
case 0:
return "nil"
case 1:
return e[0].Error()
default:
list := make([]string, len(e))
for i := range e {
list[i] = e[i].Error()
}
return fmt.Sprintf("multiple errors:\n%s", strings.Join(list, "\n"))
}
} }
// We use a lowercase type alias for *log.Logger so that we can embedd it in // We use a lowercase type alias for *log.Logger so that we can embedd it in
// Scanner without exporting it. // Scanner without exporting it.
type logger = *log.Logger type logger = *log.Logger
type rwmutex = sync.RWMutex
var ( // An Observer is a callback that can be registerd with Scanner.OnFound. It
nodebugf = func(format string, v ...interface{}) {}
nodebugln = func(v ...interface{}) {}
)
// An Observer is a callback that can be registered with Scanner.OnFound. It
// will be called with a pid and an offset. Observers are called concurrently. // will be called with a pid and an offset. Observers are called concurrently.
// They have to be thread-safe. // They have to be thread-safe.
type Observer func(pid int, offset uint64) error type Observer func(pid int, offset uint64) error
// NewScanner returns a new Scanner that scans all running processes for the // NewScanner returns a new Scanner that scans all running processes for the
// given symbol name in all memory-mapped files matching the given pathglob. // given symbol name in all memory-mapped files matching the given pathglob.
// To be useful, one or more Observer functions should be registered with // To be useful, one or more Observer functions should be registerd with
// Scanner.OnFound(). The scanning starts with a call of Scanner.Run(). // Scanner.OnFound(). The scanning starts with a call of Scanner.Run().
func NewScanner(symbol, pathglob string) *Scanner { func NewScanner(symbol, pathglob string) *Scanner {
return &Scanner{ return &Scanner{
@ -89,97 +62,62 @@ func NewScanner(symbol, pathglob string) *Scanner {
logger: log.New(os.Stderr, "[symbolyze] ", log.Ltime|log.Lmicroseconds), logger: log.New(os.Stderr, "[symbolyze] ", log.Ltime|log.Lmicroseconds),
// debugging is off per default. // debugging is off per default.
debugf: nodebugf, debugf: func(string, ...interface{}) {},
debugln: nodebugln, debugln: func(...interface{}) {},
} }
} }
// Debug sets the scanner into debugging mode. It must be called only once // Debug sets the scanner into debugging mode. It must called only once before
// before a call to Scanner.Run(). // a call to Scanner.Run().
func (S *Scanner) Debug(on bool) { func (S *Scanner) DebugOn() {
S.Lock() // Use the embedded *log.Logger for debugging.
defer S.Unlock() S.debugf = S.Printf
S.debugln = S.Println
if on { S.debugln("starting in debug-mode")
// Use the embedded *log.Logger for debugging.
S.debugf = S.Printf
S.debugln = S.Println
S.debugln("starting in debug-mode")
} else {
S.debugf = nodebugf
S.debugln = nodebugln
}
} }
// setErrorf puts the Scanner into an error state with the given error // setErrorf puts the Scanner into an error state with the given error
// statement. It also logs the error. setErrorf is not thread-safe. // statement. It also logs the error.
func (S *Scanner) setErrorf(format string, a ...interface{}) { func (S *Scanner) setErrorf(format string, a ...interface{}) {
S.Lock() S.err = fmt.Errorf(format, a...)
S.errors = append(S.errors, fmt.Errorf(format, a...))
S.Unlock()
S.Printf(format, a...) S.Printf(format, a...)
} }
func (S *Scanner) HasErrors() bool { // OnFound puts an Observer function into the interal queue. The functions are
S.RLock() // called in sequence in their own goroutine whenever the scanner finds the
defer S.RUnlock() // symbol in the a running program. That implies that an Observer has to be
return len(S.errors) > 0 // thread-safe. Errors from the observers will be logged.
} //
// Calling OnFound is not thread-safe.
func (S *Scanner) Errors() error { func (S *Scanner) OnFound(fun Observer) {
S.RLock() S.observers = append(S.observers, fun)
defer S.RUnlock()
if len(S.errors) == 0 {
return nil
} else {
e2 := make(errors, len(S.errors))
copy(e2, S.errors)
return e2
}
}
func (S *Scanner) addError(err error) {
S.Lock()
defer S.Unlock()
S.errors = append(S.errors, err)
}
// OnFound puts Observer functions at the end of the internal queue. All
// Observer functions are called in sequence in their own goroutine whenever
// the scanner finds the symbol in a running program. That implies that an
// Observer has to be thread-safe. Errors from the observers will be logged.
func (S *Scanner) OnFound(fun ...Observer) {
S.Lock()
defer S.Unlock()
S.observers = append(S.observers, fun...)
return return
} }
// Run starts the scanning process. It scans the entries of all /proc/NNN/maps // Run starts the scanning process. It scans the maps file all processes in
// files for pathnames that match the provided path-glob and are executables or // /proc for pathnames that match the provided pathglob and that are ELF
// shared libraries in ELF format. It searches for the provided symbol in // executables or shared libraries. It searches for the provided symbol in
// those files and calls the registered Observer functions, concurrently, with // those files and calls the registered Observer functions concurrently with
// the pid and offset of the symbol. // the pid and offset of the symbol.
// //
// Run will return an error if it couldn't read the proc filesystem. Otherwise // Run will return an error if it couldn't read the proc filesystem. Otherwise
// it will try to continue to loop over all pids, writing potential errors to // it will try to continue to loop over all pids, writing potential errors to
// the console. Errors from the observer functions are logged. // the console. Errors from the observer functions are logged.
func (S *Scanner) Run() error { func (S *Scanner) Run() error {
if S.HasErrors() { if S.err != nil {
return S.Errors() return S.err
} }
proc, err := os.Open("/proc") proc, err := os.Open("/proc")
if err != nil { if err != nil {
S.setErrorf("Failed to open /proc: %v\n", err) S.setErrorf("Failed to open /proc: %v\n", err)
return S.Errors() return S.err
} }
infos, err := proc.Readdir(-1) infos, err := proc.Readdir(-1)
if err != nil { if err != nil {
S.setErrorf("Failed to read /proc: %v\n", err) S.setErrorf("Failed to read /proc: %v\n", err)
return S.Errors() return S.err
} }
proc.Close() proc.Close()
@ -194,7 +132,7 @@ func (S *Scanner) Run() error {
continue continue
} else if pid, err := strconv.Atoi(pid_s); err != nil { } else if pid, err := strconv.Atoi(pid_s); err != nil {
continue continue
} else if offset, found := S.searchSymbolInPid(pid); !found { } else if offset, found := S.searchSymbolIn(pid); !found {
continue continue
} else { } else {
// Call the observers with (pid, offset), in the // Call the observers with (pid, offset), in the
@ -205,7 +143,8 @@ func (S *Scanner) Run() error {
go func() { go func() {
err = observer(pid, offset) err = observer(pid, offset)
if err != nil { if err != nil {
S.addError(fmt.Errorf("S.observer[%d](%d, %d) error: %v", n, pid, offset, err)) S.Printf("S.observer[%d](%d, %d) error: %v", n, pid, offset, err)
// TODO: accumulate errors from all Observers.
} }
wg.Done() wg.Done()
}() }()
@ -215,31 +154,10 @@ func (S *Scanner) Run() error {
} }
wg.Wait() // Wait for all observers to finish wg.Wait() // Wait for all observers to finish
return S.Errors() return S.err
} }
// RunEvery() starts a scanning process and repeats at the given time step. // searchSymbolIn loops over the entries in /proc/<pid>/maps and searches for
func (S *Scanner) RunEvery(step time.Duration) {
S.ticker = time.NewTicker(step)
for {
select {
case <-S.ticker.C:
err := S.Run()
if err != nil {
S.Println(err)
}
}
}
}
func (S *Scanner) Stop() {
if S.ticker != nil {
S.debugln("Stopping ticker")
S.ticker.Stop()
}
}
// searchSymbolInPid loops over the entries in /proc/<pid>/maps and searches for
// the symbol in the mapped files. // the symbol in the mapped files.
// //
// The current implementation makes the following assumptions: // The current implementation makes the following assumptions:
@ -249,12 +167,12 @@ func (S *Scanner) Stop() {
// 4. The symbol is present at most in one mapped file at the same time. // 4. The symbol is present at most in one mapped file at the same time.
// //
// It returns the offsets in memory of the running program, if found. // It returns the offsets in memory of the running program, if found.
func (S *Scanner) searchSymbolInPid(pid int) (offset uint64, found bool) { func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) {
path := filepath.Join("/proc", strconv.Itoa(pid), "maps") path := filepath.Join("/proc", strconv.Itoa(pid), "maps")
maps, err := os.Open(path) maps, err := os.Open(path)
if err != nil { if err != nil {
S.debugf("%v\n", err) S.Printf("%v\n", err)
return 0, false return 0, false
} }
@ -312,7 +230,7 @@ func (S *Scanner) searchSymbolInPid(pid int) (offset uint64, found bool) {
// Finally, find the symbol in the binary. If found, // Finally, find the symbol in the binary. If found,
// findSymbol returns the offset of the symbol in memory, // findSymbol returns the offset of the symbol in memory,
// taking alignment into account. // taking alignment into account.
memOffset, found := S.findSymbolInELF(pathname) memOffset, found := S.findSymbol(pathname)
if !found { if !found {
continue continue
} }
@ -324,23 +242,20 @@ func (S *Scanner) searchSymbolInPid(pid int) (offset uint64, found bool) {
return 0, false return 0, false
} }
// findSymbolInELF searches for the provided symbol in the given pathname to an // findSymbol searches for the provided symbol in the given pathname to an
// ELF-file. If found, it returns the offset of the symbol in the virtual // ELF-file. If found, it returns the offset of the symbol in the virtual
// memory according to the fomula: // memory according to the fomula:
// //
// vmOffset = alignedOffset(section) + offsetInSection(symbol) // vmOffset = alignedOffset(section) + offsetInSection(symbol)
// //
// The result will be cached so that subsequent calls to findSymbolInELF with // The result will be cached so that subsequent calls to findSymbol with the
// the same pathname can quickly return. // same pathname can quickly return.
func (S *Scanner) findSymbolInELF(pathname string) (offset uint64, found bool) { func (S *Scanner) findSymbol(pathname string) (offset uint64, found bool) {
// 0. Return the value from the cache, if found. // 0. Return the value from the cache, if found.
S.RLock()
if offset, found = S.cache[pathname]; found { if offset, found = S.cache[pathname]; found {
S.RUnlock()
return offset, found return offset, found
} }
S.RUnlock()
// 1. Open the file with the ELF-parser // 1. Open the file with the ELF-parser
file, err := elf.Open(pathname) file, err := elf.Open(pathname)
@ -393,9 +308,7 @@ func (S *Scanner) findSymbolInELF(pathname string) (offset uint64, found bool) {
// 6. Store this calculation in our cache so that we don't to touch // 6. Store this calculation in our cache so that we don't to touch
// this file again. // this file again.
S.Lock()
S.cache[pathname] = vmOffset S.cache[pathname] = vmOffset
S.Unlock()
return vmOffset, true return vmOffset, true
} }