Compare commits
No commits in common. "d15586390a95a4b7577fb6b23f08f8fe033376ce" and "3672fd455b3f9c6730a8af28e666370841e676ae" have entirely different histories.
d15586390a
...
3672fd455b
@ -1,102 +1,23 @@
|
|||||||
// +build !withcgo
|
|
||||||
|
|
||||||
package ebpf
|
package ebpf
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"syscall"
|
|
||||||
"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
|
|
||||||
type MapFD uint32
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <linux/unistd.h>
|
||||||
|
#include <linux/bpf.h>
|
||||||
|
|
||||||
All methods in this file implement a syscall to bpf one way or another. We
|
// See the bpf man page for details on the following functions
|
||||||
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)
|
static int bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
|
||||||
{
|
{
|
||||||
return syscall(__NR_bpf, cmd, attr, size);
|
return syscall(__NR_bpf, cmd, attr, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
Each ouf our methods calls the syscall directly, instead.
|
static int bpf_create_map(
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// CreateMap creates an eBPF map from int->uint64. The file descriptor of the
|
|
||||||
// created map is returned.
|
|
||||||
func CreateMap() (MapFD, error) {
|
|
||||||
/*
|
|
||||||
static int bpf_create_map(
|
|
||||||
enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size,
|
enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size,
|
||||||
unsigned int max_entries)
|
unsigned int max_entries)
|
||||||
{
|
{
|
||||||
union bpf_attr attr = {
|
union bpf_attr attr = {
|
||||||
.map_type = map_type,
|
.map_type = map_type,
|
||||||
.key_size = key_size,
|
.key_size = key_size,
|
||||||
@ -104,75 +25,21 @@ func CreateMap() (MapFD, error) {
|
|||||||
.max_entries = max_entries};
|
.max_entries = max_entries};
|
||||||
|
|
||||||
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
|
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
create_attr := struct {
|
|
||||||
map_type uint32
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
/*
|
{
|
||||||
int bpf_get_next_key(int fd, const void *key, void *next_key)
|
|
||||||
{
|
|
||||||
union bpf_attr attr = {
|
union bpf_attr attr = {
|
||||||
.map_fd = fd,
|
.map_fd = fd,
|
||||||
.key = (__u64) (unsigned long) key,
|
.key = (__u64) (unsigned long) key,
|
||||||
.next_key = (__u64) (unsigned long) next_key};
|
.next_key = (__u64) (unsigned long) next_key};
|
||||||
|
|
||||||
return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
|
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)
|
||||||
int bpf_lookup_elem(int fd, const void *key, void *value)
|
{
|
||||||
{
|
|
||||||
union bpf_attr attr = {
|
union bpf_attr attr = {
|
||||||
.map_fd = fd,
|
.map_fd = fd,
|
||||||
.key = (__u64) (unsigned long) key,
|
.key = (__u64) (unsigned long) key,
|
||||||
@ -180,93 +47,10 @@ func (mfd MapFD) bpf_lookup_elem(key *int, value *uint64) error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
|
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
|
int bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags)
|
||||||
func (mfd MapFD) GetMap() (map[int]uint64, error) {
|
{
|
||||||
retMap := make(map[int]uint64)
|
|
||||||
|
|
||||||
var (
|
|
||||||
key, next int
|
|
||||||
value uint64
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
err := mfd.bpf_get_next_key(&key, &next)
|
|
||||||
if err != nil {
|
|
||||||
if err == 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 error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mfd.bpf_lookup_elem(&next, &value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("bpf_lookup_elem failed with error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
retMap[next] = value
|
|
||||||
|
|
||||||
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
|
|
||||||
// yet in the map. It returns an error otherwise.
|
|
||||||
func (mfd MapFD) Add(key int, value uint64) error {
|
|
||||||
return mfd.updateElement(key, value, 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, 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, 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 uint64) error {
|
|
||||||
/*
|
|
||||||
int bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags)
|
|
||||||
{
|
|
||||||
union bpf_attr attr = {
|
union bpf_attr attr = {
|
||||||
.map_fd = fd,
|
.map_fd = fd,
|
||||||
.key = (__u64) (unsigned long) key,
|
.key = (__u64) (unsigned long) key,
|
||||||
@ -275,30 +59,91 @@ func (mfd MapFD) updateElement(key int, value uint64, flag uint64) error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
|
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
update_attr := struct {
|
*/
|
||||||
map_fd MapFD
|
import "C"
|
||||||
key uint64
|
import (
|
||||||
value uint64
|
"fmt"
|
||||||
flags uint64
|
"syscall"
|
||||||
}{
|
"unsafe"
|
||||||
map_fd: mfd,
|
)
|
||||||
key: uint64(uintptr(unsafe.Pointer(&key))),
|
|
||||||
value: uint64(uintptr(unsafe.Pointer(&value))),
|
// MapFD is a file descriptor representing a eBPF map
|
||||||
flags: flag,
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _, err := syscall.Syscall(
|
return MapFD(r), nil
|
||||||
BPF_SYSCALL,
|
}
|
||||||
uintptr(BPF_MAP_UPDATE_ELEM),
|
|
||||||
uintptr(unsafe.Pointer(&update_attr)),
|
|
||||||
unsafe.Sizeof(update_attr),
|
|
||||||
)
|
|
||||||
|
|
||||||
if r != 0 || err != 0 {
|
// GetMap gets the key/value pairs from the specified eBPF map as a Go map
|
||||||
return fmt.Errorf("couldn't update element: %s", err.Error())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -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
|
|
@ -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("[31mSymbol not found with gdb.[0m\n" +
|
return 0, fmt.Errorf("Symbol not found with gdb")
|
||||||
"[35mMaybe /proc/sys/kernel/yama/ptrace_scope must be set to 0? Or try to run the test as root![0m")
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
defer S.Unlock()
|
|
||||||
|
|
||||||
if on {
|
|
||||||
// Use the embedded *log.Logger for debugging.
|
// Use the embedded *log.Logger for debugging.
|
||||||
S.debugf = S.Printf
|
S.debugf = S.Printf
|
||||||
S.debugln = S.Println
|
S.debugln = S.Println
|
||||||
S.debugln("starting in debug-mode")
|
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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user