Skip to content

Commit

Permalink
Improve getting process group leader
Browse files Browse the repository at this point in the history
  • Loading branch information
dhaavi committed Dec 21, 2023
1 parent 30fee07 commit 425a0be
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 154 deletions.
Binary file modified firewall/interception/ebpf/bandwidth/bpf_bpfeb.o
Binary file not shown.
Binary file modified firewall/interception/ebpf/bandwidth/bpf_bpfel.o
Binary file not shown.
Binary file modified firewall/interception/ebpf/connection_listener/bpf_bpfeb.o
Binary file not shown.
Binary file modified firewall/interception/ebpf/connection_listener/bpf_bpfel.o
Binary file not shown.
5 changes: 4 additions & 1 deletion firewall/interception/ebpf/exec/exec.go
Expand Up @@ -15,8 +15,9 @@ import (
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
"github.com/hashicorp/go-multierror"
"github.com/safing/portbase/log"
"golang.org/x/sys/unix"

"github.com/safing/portbase/log"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../programs/exec.c
Expand Down Expand Up @@ -67,6 +68,8 @@ type Event struct {
Comm string `json:"comm"`
}

// Tracer is the exec tracer itself.
// It must be closed after use.
type Tracer struct {
objs bpfObjects
tp link.Link
Expand Down
96 changes: 46 additions & 50 deletions process/api.go
@@ -1,81 +1,45 @@
package process

import (
"errors"
"fmt"
"net/http"
"strconv"

"github.com/safing/portbase/api"
"github.com/safing/portmaster/profile"
)

func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
Path: "process/tags",
Read: api.PermitUser,
BelongsTo: module,
StructFunc: handleProcessTagMetadata,
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
}); err != nil {
return err
}

if err := api.RegisterEndpoint(api.Endpoint{
Path: "process/by-profile",
Parameters: []api.Parameter{
{
Method: http.MethodGet,
Field: "scopedId",
Value: "",
Description: "The ID of the profile",
},
},
Read: api.PermitUser,
BelongsTo: module,
StructFunc: api.StructFunc(func(ar *api.Request) (any, error) {
id := ar.URL.Query().Get("scopedId")

if id == "" {
return nil, api.ErrorWithStatus(fmt.Errorf("missing profile id"), http.StatusBadRequest)
}

result := FindProcessesByProfile(ar.Context(), id)

return result, nil
}),
Description: "Get all running processes for a given profile",
Name: "Get Processes by Profile",
Description: "Get all recently active processes using the given profile",
Path: "process/list/by-profile/{source:[a-z]+}/{id:[A-z0-9-]+}",
Read: api.PermitUser,
BelongsTo: module,
StructFunc: handleGetProcessesByProfile,
}); err != nil {
return err
}

if err := api.RegisterEndpoint(api.Endpoint{
Path: "process/by-pid/{pid:[0-9]+}",
Parameters: []api.Parameter{
{
Method: http.MethodGet,
Field: "pid",
Value: "",
Description: "A PID of a process inside the requested process group",
},
},
Read: api.PermitUser,
BelongsTo: module,
StructFunc: api.StructFunc(func(ar *api.Request) (i interface{}, err error) {
pid, err := strconv.ParseInt(ar.URLVars["pid"], 10, 0)
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusBadRequest)
}

process, err := GetProcessGroupLeader(ar.Context(), int(pid))
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
}

return process, nil
}),
Description: "Load a process group leader by a child PID",
Name: "Get Process Group Leader By PID",
Description: "Load a process group leader by a child PID",
Path: "process/group-leader/{pid:[0-9]+}",
Read: api.PermitUser,
BelongsTo: module,
StructFunc: handleGetProcessGroupLeader,
}); err != nil {
return err
}
Expand All @@ -101,3 +65,35 @@ func handleProcessTagMetadata(ar *api.Request) (i interface{}, err error) {

return resp, nil
}

func handleGetProcessesByProfile(ar *api.Request) (any, error) {
source := ar.URLVars["source"]
id := ar.URLVars["id"]
if id == "" || source == "" {
return nil, api.ErrorWithStatus(fmt.Errorf("missing profile source/id"), http.StatusBadRequest)
}

result := GetProcessesWithProfile(ar.Context(), profile.ProfileSource(source), id, true)
return result, nil
}

func handleGetProcessGroupLeader(ar *api.Request) (any, error) {
pid, err := strconv.ParseInt(ar.URLVars["pid"], 10, 0)
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusBadRequest)
}

process, err := GetOrFindProcess(ar.Context(), int(pid))
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
}
err = process.FindProcessGroupLeader(ar.Context())
switch {
case process.Leader() != nil:
return process.Leader(), nil
case err != nil:
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
default:
return nil, api.ErrorWithStatus(errors.New("leader not found"), http.StatusNotFound)
}
}
54 changes: 26 additions & 28 deletions process/database.go
Expand Up @@ -3,15 +3,17 @@ package process
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"time"

processInfo "github.com/shirou/gopsutil/process"
"github.com/tevino/abool"
"golang.org/x/exp/maps"

"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
)

const processDatabaseNamespace = "network:tree"
Expand Down Expand Up @@ -48,37 +50,33 @@ func All() map[int]*Process {
return all
}

func FindProcessesByProfile(ctx context.Context, scopedID string) []*Process {
all := All()

pids := make([]int, 0, len(all))

log.Infof("[DEBUG] searchin processes belonging to %s", scopedID)

for _, p := range all {
p.Lock()
if p.profile != nil && p.profile.LocalProfile().ScopedID() == scopedID {
pids = append(pids, p.Pid)
// GetProcessesWithProfile returns all processes that use the given profile.
// If preferProcessGroupLeader is set, it returns the process group leader instead, if available.
func GetProcessesWithProfile(ctx context.Context, profileSource profile.ProfileSource, profileID string, preferProcessGroupLeader bool) []*Process {
log.Tracer(ctx).Debugf("process: searching for processes belonging to %s", profile.MakeScopedID(profileSource, profileID))

// Get all processes that match the given profile.
procs := make([]*Process, 0, 8)
for _, p := range All() {
lp := p.profile.LocalProfile()
if lp != nil && lp.Source == profileSource && lp.ID == profileID {
if preferProcessGroupLeader && p.Leader() != nil {
procs = append(procs, p.Leader())
} else {
procs = append(procs, p)
}
}
p.Unlock()
}

m := make(map[int]*Process)

for _, pid := range pids {
if _, ok := m[pid]; ok {
continue
}

process, err := GetProcessGroupLeader(ctx, pid)
if err != nil {
continue
}

m[process.Pid] = process
}
// Sort and compact.
slices.SortFunc[[]*Process, *Process](procs, func(a, b *Process) int {
return strings.Compare(a.processKey, b.processKey)
})
slices.CompactFunc[[]*Process, *Process](procs, func(a, b *Process) bool {
return a.processKey == b.processKey
})

return maps.Values(m)
return procs
}

// Save saves the process to the internal state and pushes an update.
Expand Down
7 changes: 7 additions & 0 deletions process/find.go
Expand Up @@ -29,6 +29,13 @@ func GetProcessWithProfile(ctx context.Context, pid int) (process *Process, err
return GetUnidentifiedProcess(ctx), err
}

// Get process group leader, which is the process "nearest" to the user and
// will have more/better information for finding names ans icons, for example.
err = process.FindProcessGroupLeader(ctx)
if err != nil {
log.Warningf("process: failed to get process group leader for %s: %s", process, err)
}

changed, err := process.GetProfile(ctx)
if err != nil {
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
Expand Down
59 changes: 42 additions & 17 deletions process/process.go
Expand Up @@ -30,21 +30,26 @@ type Process struct {
// Process attributes.
// Don't change; safe for concurrent access.

Name string
UserID int
UserName string
UserHome string
Pid int
Pgid int // linux only
CreatedAt int64
Name string
UserID int
UserName string
UserHome string

Pid int
CreatedAt int64

ParentPid int
ParentCreatedAt int64
Path string
ExecName string
Cwd string
CmdLine string
FirstArg string
Env map[string]string

LeaderPid int
leader *Process

Path string
ExecName string
Cwd string
CmdLine string
FirstArg string
Env map[string]string

// unique process identifier ("Pid-CreatedAt")
processKey string
Expand Down Expand Up @@ -92,6 +97,16 @@ func (p *Process) Profile() *profile.LayeredProfile {
return p.profile
}

// Leader returns the process group leader that is attached to the process.
// This will not trigger a new search for the process group leader, it only
// returns existing data.
func (p *Process) Leader() *Process {
p.Lock()
defer p.Unlock()

return p.leader
}

This comment has been minimized.

Copy link
@ppacher

ppacher Dec 21, 2023

Contributor

We could load the leader process lazily, i.e. when Leader() is called but .leader is nil we call FindProcessGroupLeader automatically (but an unlocked, lower-case version of course)

This comment has been minimized.

Copy link
@ppacher

ppacher Dec 21, 2023

Contributor

or even wrap it in a sync.Once

// IsIdentified returns whether the process has been identified or if it
// represents some kind of unidentified process.
func (p *Process) IsIdentified() bool {
Expand Down Expand Up @@ -213,12 +228,9 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
return process, nil
}

pgid, _ := GetProcessGroupID(ctx, int(pInfo.Pid))

// Create new a process object.
process = &Process{
Pid: int(pInfo.Pid),
Pgid: pgid,
FirstSeen: time.Now().Unix(),
processKey: key,
}
Expand Down Expand Up @@ -250,7 +262,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
// TODO: User Home
// new.UserHome, err =

// Parent process id
// Parent process ID
ppid, err := pInfo.PpidWithContext(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pInfo.Pid, err)
Expand All @@ -267,6 +279,19 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
process.ParentCreatedAt = parentCreatedAt
}

// Leader process ID
// Get process group ID to find group leader, which is the process "nearest"
// to the user and will have more/better information for finding names and
// icons, for example.
leaderPid, err := GetProcessGroupID(ctx, process.Pid)
if err != nil {
// Fail gracefully.
log.Warningf("process: failed to get process group ID for p%d: %s", process.Pid, err)
process.LeaderPid = UndefinedProcessID
} else {
process.LeaderPid = leaderPid
}

// Path
process.Path, err = pInfo.ExeWithContext(ctx)
if err != nil {
Expand Down
11 changes: 7 additions & 4 deletions process/process_default.go
Expand Up @@ -10,11 +10,14 @@ import (
// SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 0

func GetProcessGroupLeader(ctx context.Context, pid int) (*Process, error) {
// On systems other than linux we just return the process with PID == pid
return GetOrFindProcess(ctx, pid)
// GetProcessGroupLeader returns the process that leads the process group.
// Returns nil on unsupported platforms.
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
return nil
}

// GetProcessGroupID returns the process group ID of the given PID.
// Returns undefined process ID on unsupported platforms.
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
return 0
return UndefinedProcessID, nil
}

1 comment on commit 425a0be

@ppacher
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Please sign in to comment.