package process import ( "context" "errors" "fmt" "path/filepath" "runtime" "strings" "sync" "time" processInfo "github.com/shirou/gopsutil/process" "golang.org/x/sync/singleflight" "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portmaster/profile" ) const onLinux = runtime.GOOS == "linux" var getProcessSingleInflight singleflight.Group // A Process represents a process running on the operating system. type Process struct { record.Base sync.Mutex // Process attributes. // Don't change; safe for concurrent access. 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 // unique process identifier ("Pid-CreatedAt") processKey string // Profile attributes. // Once set, these don't change; safe for concurrent access. // Tags holds extended information about the (virtual) process, which is used // to find a profile. Tags []profile.Tag // MatchingPath holds an alternative binary path that can be used to find a // profile. MatchingPath string // PrimaryProfileID holds the scoped ID of the primary profile. PrimaryProfileID string // profile holds the layered profile based on the primary profile. profile *profile.LayeredProfile // Mutable attributes. FirstSeen int64 LastSeen int64 Error string // Cache errors ExecHashes map[string]string } // GetTag returns the process tag with the given ID. func (p *Process) GetTag(tagID string) (profile.Tag, bool) { for _, t := range p.Tags { if t.Key == tagID { return t, true } } return profile.Tag{}, false } // Profile returns the assigned layered profile. func (p *Process) Profile() *profile.LayeredProfile { if p == nil { return nil } return p.profile } // IsIdentified returns whether the process has been identified or if it // represents some kind of unidentified process. func (p *Process) IsIdentified() bool { // Check if process exists. if p == nil { return false } // Check for special PIDs. switch p.Pid { case UndefinedProcessID: return false case UnidentifiedProcessID: return false case UnsolicitedProcessID: return false default: return true } } // IsLocal returns whether the process has been identified as a local process. func (p *Process) IsLocal() bool { // Check if process exists. if p == nil { return false } return p.Pid >= 0 } // Equal returns if the two processes are both identified and have the same PID. func (p *Process) Equal(other *Process) bool { return p.IsIdentified() && other.IsIdentified() && p.Pid == other.Pid } const systemResolverScopedID = string(profile.SourceLocal) + "/" + profile.SystemResolverProfileID // IsSystemResolver is a shortcut to check if the process is or belongs to the // system resolver and needs special handling. func (p *Process) IsSystemResolver() bool { // Check if process exists. if p == nil { return false } // Check ID. return p.PrimaryProfileID == systemResolverScopedID } // GetLastSeen returns the unix timestamp when the process was last seen. func (p *Process) GetLastSeen() int64 { p.Lock() defer p.Unlock() return p.LastSeen } // SetLastSeen sets the unix timestamp when the process was last seen. func (p *Process) SetLastSeen(lastSeen int64) { p.Lock() defer p.Unlock() p.LastSeen = lastSeen } // String returns a string representation of process. func (p *Process) String() string { if p == nil { return "?" } return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid) } // GetOrFindProcess returns the process for the given PID. func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting process for PID %d", pid) // Check for special processes switch pid { case UnidentifiedProcessID: return GetUnidentifiedProcess(ctx), nil case UnsolicitedProcessID: return GetUnsolicitedProcess(ctx), nil case SystemProcessID: return GetSystemProcess(ctx), nil } // Get pid and creation time for identification. pInfo, err := processInfo.NewProcessWithContext(ctx, int32(pid)) if err != nil { return nil, err } createdAt, err := pInfo.CreateTimeWithContext(ctx) if err != nil { return nil, err } key := getProcessKey(int32(pid), createdAt) // Load process and make sure it is only loaded once. p, err, _ := getProcessSingleInflight.Do(key, func() (interface{}, error) { return loadProcess(ctx, key, pInfo) }) if err != nil { return nil, err } if p == nil { return nil, errors.New("process getter returned nil") } return p.(*Process), nil // nolint:forcetypeassert // Can only be a *Process. } func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*Process, error) { // Check if we already have the process. process, ok := GetProcessFromStorage(key) if ok { return process, nil } // Create new a process object. process = &Process{ Pid: int(pInfo.Pid), FirstSeen: time.Now().Unix(), processKey: key, } // Get creation time of process. (The value should be cached by the library.) var err error process.CreatedAt, err = pInfo.CreateTimeWithContext(ctx) if err != nil { return nil, err } // UID // TODO: implemented for windows if onLinux { var uids []int32 uids, err = pInfo.UidsWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get UID for p%d: %w", pInfo.Pid, err) } process.UserID = int(uids[0]) } // Username process.UserName, err = pInfo.UsernameWithContext(ctx) if err != nil { return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pInfo.Pid, err) } // TODO: User Home // new.UserHome, err = // 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) } process.ParentPid = int(ppid) // Parent created time parentPInfo, err := processInfo.NewProcessWithContext(ctx, ppid) if err == nil { parentCreatedAt, err := parentPInfo.CreateTimeWithContext(ctx) if err != nil { return nil, err } process.ParentCreatedAt = parentCreatedAt } // Path process.Path, err = pInfo.ExeWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get Path for p%d: %w", pInfo.Pid, err) } // remove linux " (deleted)" suffix for deleted files if onLinux { process.Path = strings.TrimSuffix(process.Path, " (deleted)") } // Executable Name _, process.ExecName = filepath.Split(process.Path) // Current working directory // not yet implemented for windows if runtime.GOOS != "windows" { process.Cwd, err = pInfo.CwdWithContext(ctx) if err != nil { log.Warningf("process: failed to get current working dir (PID %d): %s", pInfo.Pid, err) } } // Command line arguments process.CmdLine, err = pInfo.CmdlineWithContext(ctx) if err != nil { log.Tracer(ctx).Warningf("process: failed to get cmdline (PID %d): %s", pInfo.Pid, err) } // Name process.Name, err = pInfo.NameWithContext(ctx) if err != nil { log.Tracer(ctx).Warningf("process: failed to get process name (PID %d): %s", pInfo.Pid, err) } if process.Name == "" { process.Name = process.ExecName } // Get all environment variables env, err := pInfo.EnvironWithContext(ctx) if err == nil { // Split env variables in key and value. process.Env = make(map[string]string, len(env)) for _, entry := range env { splitted := strings.SplitN(entry, "=", 2) if len(splitted) == 2 { process.Env[strings.Trim(splitted[0], `'"`)] = strings.Trim(splitted[1], `'"`) } } } else { log.Tracer(ctx).Warningf("process: failed to get the process environment (PID %d): %s", pInfo.Pid, err) } // Add process tags. process.addTags() if len(process.Tags) > 0 { log.Tracer(ctx).Debugf("profile: added tags: %+v", process.Tags) } process.Save() return process, nil } // GetKey returns the key that is used internally to identify the process. // The key consists of the PID and the start time of the process as reported by // the system. func (p *Process) GetKey() string { return p.processKey } // Builds a unique identifier for a processes. func getProcessKey(pid int32, createdTime int64) string { return fmt.Sprintf("%d-%d", pid, createdTime) } // MatchingData returns the matching data for the process. func (p *Process) MatchingData() *MatchingData { return &MatchingData{p} } // MatchingData provides a interface compatible view on the process for profile matching. type MatchingData struct { p *Process } // Tags returns process.Tags. func (md *MatchingData) Tags() []profile.Tag { return md.p.Tags } // Env returns process.Env. func (md *MatchingData) Env() map[string]string { return md.p.Env } // Path returns process.Path. func (md *MatchingData) Path() string { return md.p.Path } // MatchingPath returns process.MatchingPath. func (md *MatchingData) MatchingPath() string { return md.p.MatchingPath } // Cmdline returns the command line of the process. func (md *MatchingData) Cmdline() string { return md.p.CmdLine }