Skip to content

fix: replace os.Exit(1) with error returns, fix /readyz, harden CopyFile#67

Open
tuxerrante wants to merge 1 commit into
mainfrom
fix/remove-os-exit-error-handling
Open

fix: replace os.Exit(1) with error returns, fix /readyz, harden CopyFile#67
tuxerrante wants to merge 1 commit into
mainfrom
fix/remove-os-exit-error-handling

Conversation

@tuxerrante
Copy link
Copy Markdown
Owner

Summary

  • Replace os.Exit(1) with proper error returns in compareLocalFiles, areProfilesReadable, and CopyFile. This shifts from fail-crash to fail-graceful, letting the poller retry on the next cycle instead of crashing the DaemonSet pod.
  • Fix /readyz endpoint silent false-positive: previously returned empty 200 when len(desired) != len(loaded) (no else branch). Now returns 503 with clear status message.
  • Add isSafePath validation at CopyFile entry — defense in depth before os.Stat on the source path.
  • Update tests: un-skip TestGetNewProfiles_NonexistentDir, add error path coverage for areProfilesReadable, compareLocalFiles, and CopyFile.

Security Context

This application runs as a privileged DaemonSet managing kernel AppArmor profiles. The /readyz bug meant kubelet could mark the pod as "ready" when it couldn't read its profile source — a false-positive readiness signal for a security control.

Error propagation verified through the full call chain:

compareLocalFiles → HasTheSameContent → calculateProfileChanges → loadNewProfiles → pollNow (logged, retried next cycle)
areProfilesReadable → getNewProfiles → loadNewProfiles → pollNow
CopyFile → loadProfile → loadNewProfiles → pollNow

Test plan

  • TestGetNewProfiles_NonexistentDir — un-skipped, validates (false, nil) return
  • TestAreProfilesReadable_NonexistentDir — new test for ReadDir error path
  • TestCompareLocalFiles_ReadError — new test for file read error path
  • Test_CopyFile_UnsafePath — validates isSafePath rejection
  • Test_CopyFile_NonexistentSource — validates stat error return
  • All pre-existing tests maintain same pass/fail status
  • CI passes (golangci-lint, unit tests, CodeQL)

Made with Cursor

…th to CopyFile

Replace os.Exit(1) calls in compareLocalFiles, areProfilesReadable, and
CopyFile with proper error returns. This shifts from fail-crash to
fail-graceful behavior, allowing the poller to retry on the next cycle
instead of crashing the entire DaemonSet pod.

Error propagation has been verified through the full call chain:
  compareLocalFiles → HasTheSameContent → calculateProfileChanges → loadNewProfiles → pollNow
  areProfilesReadable → getNewProfiles → loadNewProfiles → pollNow
  CopyFile → loadProfile → loadNewProfiles → pollNow

Additional fixes:
- Add isSafePath validation at CopyFile entry point (defense in depth
  before os.Stat on unvalidated source path)
- Fix /readyz endpoint: add else branch for len(desired) != len(loaded)
  and explicit nil-desired handling. Previously returned an empty 200
  response (false positive readiness) when profiles couldn't be read.

Test updates:
- Un-skip TestGetNewProfiles_NonexistentDir (was skipped because of os.Exit)
- Add TestAreProfilesReadable_NonexistentDir
- Add TestCompareLocalFiles_ReadError
- Add Test_CopyFile_UnsafePath and Test_CopyFile_NonexistentSource
- Fix test temp dir paths to use /tmp for isSafePath compatibility

Made-with: Cursor
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR shifts several filesystem/profile operations from process-terminating failures (os.Exit(1)) to error-returning behavior, fixes a /readyz false-positive scenario, and adds additional path hardening and test coverage to support graceful retries in the poll loop.

Changes:

  • Replace os.Exit(1) with error/false returns in compareLocalFiles, areProfilesReadable, and CopyFile.
  • Fix /readyz readiness logic to return 503 when desired vs loaded profiles are out of sync (and when desired profiles can’t be read).
  • Expand unit tests to cover newly reachable error paths and validate CopyFile path safety behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/app/filesystemOperations.go Converts fatal exits to returned errors/false; adds isSafePath guard at CopyFile entry and improves error wrapping.
src/app/healthz.go Updates /readyz to avoid silent 200s when desired/loaded mismatch or desired profiles aren’t readable.
src/app/t_profiles_ops_test.go Un-skips and updates nonexistent-dir test to assert graceful behavior.
src/app/t_hasTheSameContent_test.go Adds tests for areProfilesReadable error path and HasTheSameContent read error path.
src/app/t_copy_test.go Updates copy tests to use /tmp for isSafePath; adds unsafe-path and stat-error coverage.
src/app/t_profiles_test.go Updates temp directory creation to ensure isSafePath-allowed paths for load/unload tests.
src/app/t_main_extended_test.go Updates loadProfile success test temp path to comply with isSafePath.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app/healthz.go
Comment on lines 17 to +33
http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
_, desired := getNewProfiles(cfg)
_, loaded, _ := getLoadedProfiles(cfg)

inSync := true
if desired == nil {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("NOT_READY: unable to read desired profiles"))

if len(desired) == len(loaded) {
for profile := range desired {
if !loaded[profile] {
inSync = false
return
}

break
}
}
if len(desired) != len(loaded) {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("NOT_READY"))

return
}
Comment on lines +175 to +180
tmp := makeSafeDirForTest(t)
existing := filepath.Join(tmp, "existing.profile")
os.WriteFile(existing, []byte(testProfileData), 0o644)

missing := filepath.Join(tmp, "missing.profile")
_, err := HasTheSameContent(nil, existing, missing)
Comment thread src/app/t_copy_test.go
Comment on lines +55 to +64
func Test_CopyFile_NonexistentSource(t *testing.T) {
t.Parallel()

err := CopyFile("/tmp/nonexistent-file-abc123", "/tmp/dst")
if err == nil {
t.Fatal("expected error for nonexistent source")
}
if !strings.Contains(err.Error(), "stat source") {
t.Errorf("expected stat error, got: %v", err)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants