-
Notifications
You must be signed in to change notification settings - Fork 23
feat(developer-mdm): run scans at background priority and report sleep-spanning runs #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shubham-stepsecurity
wants to merge
9
commits into
step-security:main
Choose a base branch
from
shubham-stepsecurity:sm/bg-priority-sleep-detection
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
eba9dfa
feat(developer-mdm): run telemetry scans at OS background priority
shubham-stepsecurity e608b34
feat(developer-mdm): detect and report sleep-spanning runs
shubham-stepsecurity 1c6ea27
feat(developer-mdm): relocate config.json into the install dir
shubham-stepsecurity bcfd332
fix(executor): native user-PATH lookups and TTY-less children to prev…
shubham-stepsecurity 97b119c
feat(developer-mdm): self-updating binary with native SSHSIG release …
shubham-stepsecurity 89ed4ad
fix(developer-mdm): resolve gosec findings in the self-update path
shubham-stepsecurity 8d9ec25
fix(developer-mdm): unwrap base64 transport around signed_checksum in…
shubham-stepsecurity d5b40e2
fix(developer-mdm): harden self-update floor and armor parsing, per-t…
shubham-stepsecurity 600c948
fix(developer-mdm): annotate safe tid conversions for gosec
shubham-stepsecurity File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| //go:build darwin | ||
|
|
||
| package bgpriority | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "syscall" | ||
| ) | ||
|
|
||
| // Darwin-specific setpriority(2) selectors, spelled out because | ||
| // golang.org/x/sys/unix doesn't define them (see sys/resource.h). | ||
| const ( | ||
| // prioDarwinProcess scopes the call to a whole process (PRIO_DARWIN_PROCESS). | ||
| prioDarwinProcess = 4 | ||
| // prioDarwinBG places the process in the Darwin background band | ||
| // (PRIO_DARWIN_BG): throttled CPU, disk IO, and network — the tier Time | ||
| // Machine and Spotlight indexing run at. Throttled means a reduced | ||
| // proportional share under contention, never starvation, and effectively | ||
| // full speed on an otherwise idle machine. | ||
| prioDarwinBG = 0x1000 | ||
| ) | ||
|
|
||
| func apply() (string, error) { | ||
| if err := syscall.Setpriority(prioDarwinProcess, 0, prioDarwinBG); err != nil { | ||
| return "", fmt.Errorf("setpriority(PRIO_DARWIN_PROCESS, PRIO_DARWIN_BG): %w", err) | ||
| } | ||
| return "macOS background task policy (throttled CPU/IO/network, inherited by child processes)", nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| //go:build linux | ||
|
|
||
| package bgpriority | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "strconv" | ||
| "strings" | ||
| "syscall" | ||
| ) | ||
|
|
||
| // ioprio_set(2) encoding (linux/ioprio.h), spelled out because neither the | ||
| // stdlib nor golang.org/x/sys/unix wraps the call itself. | ||
| const ( | ||
| ioprioWhoProcess = 1 // IOPRIO_WHO_PROCESS: with a tid, targets that thread | ||
| ioprioClassShift = 13 | ||
| // ioprioClassBE / ioprioBELowest: best-effort class, lowest slot. | ||
| // Deliberately NOT the idle class (3): idle IO is only serviced when the | ||
| // disk is otherwise quiet and can be starved indefinitely under a | ||
| // sustained workload (a running build), whereas best-effort prio 7 is the | ||
| // last slot in the round-robin and always makes progress. | ||
| ioprioClassBE = 2 | ||
| ioprioBELowest = 7 | ||
| ) | ||
|
|
||
| // apply lowers CPU and IO priority for EVERY current OS thread. On Linux both | ||
| // setpriority(PRIO_PROCESS, 0, …) and ioprio_set(IOPRIO_WHO_PROCESS, 0, …) | ||
| // act on the CALLING THREAD only (nice is per-task under NPTL), and the Go | ||
| // runtime multiplexes goroutines across many threads — a single self-targeted | ||
| // call would leave most scan work, and any child forked from another thread, | ||
| // at normal priority. Walking /proc/self/task covers every live thread; | ||
| // threads and child processes created afterwards inherit from their (covered) | ||
| // creator, so the whole process stays in the background band. | ||
| func apply() (string, error) { | ||
| tids, err := os.ReadDir("/proc/self/task") | ||
| if err != nil { | ||
| // Fall back to the calling thread — degraded but not useless. | ||
| return applyToThread(0) | ||
| } | ||
|
|
||
| niceCovered, ioCovered := 0, 0 | ||
| for _, ent := range tids { | ||
| tid, convErr := strconv.Atoi(ent.Name()) | ||
| if convErr != nil { | ||
| continue | ||
| } | ||
| // A thread can exit between the ReadDir and the calls (ESRCH) — | ||
| // count only what actually applied. | ||
| if syscall.Setpriority(syscall.PRIO_PROCESS, tid, 19) == nil { | ||
| niceCovered++ | ||
| } | ||
| ioprio := uintptr(ioprioClassBE<<ioprioClassShift | ioprioBELowest) | ||
| // #nosec G115 -- tid parses from a /proc/self/task entry name: a | ||
| // positive kernel thread id, always far below uintptr's range. | ||
| if _, _, errno := syscall.Syscall(syscall.SYS_IOPRIO_SET, ioprioWhoProcess, uintptr(tid), ioprio); errno == 0 { | ||
| ioCovered++ | ||
| } | ||
| } | ||
|
|
||
| var parts []string | ||
| if niceCovered > 0 { | ||
| parts = append(parts, fmt.Sprintf("nice 19 on %d/%d threads", niceCovered, len(tids))) | ||
| } | ||
| if ioCovered > 0 { | ||
| parts = append(parts, fmt.Sprintf("ionice best-effort 7 on %d/%d threads", ioCovered, len(tids))) | ||
| } | ||
| // Partial success still helps; report only what actually applied. | ||
| if len(parts) == 0 { | ||
| return "", fmt.Errorf("setpriority/ioprio_set applied to no thread") | ||
| } | ||
| return strings.Join(parts, ", ") + " (inherited by new threads and child processes)", nil | ||
| } | ||
|
|
||
| // applyToThread is the single-target fallback when /proc is unavailable. | ||
| func applyToThread(tid int) (string, error) { | ||
| var parts []string | ||
| niceErr := syscall.Setpriority(syscall.PRIO_PROCESS, tid, 19) | ||
| if niceErr == nil { | ||
| parts = append(parts, "nice 19") | ||
| } | ||
| ioprio := uintptr(ioprioClassBE<<ioprioClassShift | ioprioBELowest) | ||
| // #nosec G115 -- tid is 0 (calling thread) on this fallback path. | ||
| _, _, errno := syscall.Syscall(syscall.SYS_IOPRIO_SET, ioprioWhoProcess, uintptr(tid), ioprio) | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| if errno == 0 { | ||
| parts = append(parts, "ionice best-effort 7") | ||
| } | ||
| if len(parts) == 0 { | ||
| return "", fmt.Errorf("setpriority: %v; ioprio_set: %v", niceErr, errno) | ||
| } | ||
| return strings.Join(parts, ", ") + " (calling thread only; /proc/self/task unavailable)", nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| //go:build !darwin && !linux && !windows | ||
|
|
||
| package bgpriority | ||
|
|
||
| // apply is a no-op on platforms without a supported priority mechanism; | ||
| // Apply logs nothing (empty desc, nil error). | ||
| func apply() (string, error) { | ||
| return "", nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| //go:build windows | ||
|
|
||
| package bgpriority | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "golang.org/x/sys/windows" | ||
| ) | ||
|
|
||
| func apply() (string, error) { | ||
| h := windows.CurrentProcess() | ||
| var parts []string | ||
|
|
||
| // Class FIRST: the priority class can't be changed once the process is | ||
| // in background mode, and child processes of a BELOW_NORMAL parent | ||
| // inherit the class (background mode itself is never inherited). | ||
| classErr := windows.SetPriorityClass(h, windows.BELOW_NORMAL_PRIORITY_CLASS) | ||
| if classErr == nil { | ||
| parts = append(parts, "below-normal CPU class (inherited by children)") | ||
| } | ||
|
|
||
| // Background mode lowers this process's IO priority to Very Low (the | ||
| // defrag/indexer tier — deprioritized behind normal IO but continuously | ||
| // serviced) and its memory priority to the lowest band. | ||
| bgErr := windows.SetPriorityClass(h, windows.PROCESS_MODE_BACKGROUND_BEGIN) | ||
| if bgErr == nil { | ||
| parts = append(parts, "background IO/memory mode") | ||
| } | ||
|
|
||
| if len(parts) == 0 { | ||
| return "", fmt.Errorf("SetPriorityClass(BELOW_NORMAL): %v; SetPriorityClass(BACKGROUND_BEGIN): %v", classErr, bgErr) | ||
| } | ||
| return strings.Join(parts, ", "), nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Package bgpriority moves the current process into the operating system's | ||
| // background CPU/IO priority band so telemetry scans never compete with the | ||
| // interactive user for disk or CPU. Every mechanism used is throttled-but- | ||
| // guaranteed-progress (the tier Time Machine, Spotlight, and defrag run at), | ||
| // never an idle-only class that could be starved indefinitely under sustained | ||
| // load — so a scan on a busy machine gets slower, not stuck; the existing | ||
| // per-phase budgets and scan deadline bound the worst case. | ||
| // | ||
| // Inheritance: macOS task policy and Linux nice/ioprio are inherited across | ||
| // fork/exec, so every subprocess the scan spawns (npm ls, brew, spctl, ...) | ||
| // runs in the same band. On Windows, children inherit the below-normal CPU | ||
| // class but not the process's background IO mode. | ||
| package bgpriority | ||
|
|
||
| import ( | ||
| "github.com/step-security/dev-machine-guard/internal/executor" | ||
| "github.com/step-security/dev-machine-guard/internal/progress" | ||
| ) | ||
|
|
||
| // EnvDisable is the per-device escape hatch: set to "1" to keep the scan at | ||
| // normal priority (e.g. while timing a scan, or if a fleet's machines are | ||
| // dedicated build hosts where nothing interactive competes). | ||
| const EnvDisable = "STEPSEC_DISABLE_BACKGROUND_PRIORITY" | ||
|
|
||
| // applyImpl is the per-OS implementation; a var so tests can intercept it. | ||
| var applyImpl = apply | ||
|
|
||
| // Apply lowers the current process (and, where the OS inherits it, its | ||
| // children) to background priority. Best-effort by contract: failure to | ||
| // apply must never fail the run, so errors are logged and swallowed. | ||
| func Apply(exec executor.Executor, log *progress.Logger) { | ||
| if exec.Getenv(EnvDisable) == "1" { | ||
| log.Debug("background priority: disabled via %s", EnvDisable) | ||
| return | ||
| } | ||
| desc, err := applyImpl() | ||
| if err != nil { | ||
| log.Warn("background priority: not applied (%v) — continuing at normal priority", err) | ||
| return | ||
| } | ||
| if desc != "" { | ||
| log.Progress("Running at background priority: %s (disable with %s=1)", desc, EnvDisable) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package bgpriority | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/step-security/dev-machine-guard/internal/executor" | ||
| "github.com/step-security/dev-machine-guard/internal/progress" | ||
| ) | ||
|
|
||
| // swapApply replaces the per-OS implementation for one test and restores it. | ||
| func swapApply(t *testing.T, fn func() (string, error)) *int { | ||
| t.Helper() | ||
| calls := 0 | ||
| orig := applyImpl | ||
| applyImpl = func() (string, error) { | ||
| calls++ | ||
| return fn() | ||
| } | ||
| t.Cleanup(func() { applyImpl = orig }) | ||
| return &calls | ||
| } | ||
|
|
||
| func TestApply_EscapeHatchSkipsImplementation(t *testing.T) { | ||
| mock := executor.NewMock() | ||
| mock.SetEnv(EnvDisable, "1") | ||
| calls := swapApply(t, func() (string, error) { return "should not run", nil }) | ||
|
|
||
| Apply(mock, progress.NewLogger(progress.LevelInfo)) | ||
|
|
||
| if *calls != 0 { | ||
| t.Errorf("applyImpl called %d times with %s=1, want 0", *calls, EnvDisable) | ||
| } | ||
| } | ||
|
|
||
| func TestApply_ErrorIsToleratedAndDoesNotPanic(t *testing.T) { | ||
| mock := executor.NewMock() | ||
| calls := swapApply(t, func() (string, error) { return "", errors.New("EPERM") }) | ||
|
|
||
| Apply(mock, progress.NewLogger(progress.LevelInfo)) | ||
|
|
||
| if *calls != 1 { | ||
| t.Errorf("applyImpl called %d times, want 1", *calls) | ||
| } | ||
| } | ||
|
|
||
| func TestApply_AppliedPathInvokesImplementationOnce(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| env string | ||
| }{ | ||
| {name: "env unset", env: ""}, | ||
| {name: "env set to non-1", env: "0"}, | ||
| } | ||
| for _, tc := range tests { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| mock := executor.NewMock() | ||
| if tc.env != "" { | ||
| mock.SetEnv(EnvDisable, tc.env) | ||
| } | ||
| calls := swapApply(t, func() (string, error) { return "test band", nil }) | ||
|
|
||
| Apply(mock, progress.NewLogger(progress.LevelInfo)) | ||
|
|
||
| if *calls != 1 { | ||
| t.Errorf("applyImpl called %d times, want 1", *calls) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestApply_NoOpPlatformDescLogsNothing(t *testing.T) { | ||
| mock := executor.NewMock() | ||
| calls := swapApply(t, func() (string, error) { return "", nil }) | ||
|
|
||
| // Empty desc + nil error is the apply_other contract; Apply must accept it. | ||
| Apply(mock, progress.NewLogger(progress.LevelInfo)) | ||
|
|
||
| if *calls != 1 { | ||
| t.Errorf("applyImpl called %d times, want 1", *calls) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.