Skip to content
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

[NixOS support] Run patchelf after autoupdate download #1468

Merged
merged 11 commits into from
Nov 22, 2023
9 changes: 6 additions & 3 deletions ee/tuf/autoupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,12 @@ func (ta *TufAutoupdater) checkForUpdate() error {

// If launcher was updated, we want to exit and reload
if updatedVersion, ok := updatesDownloaded[binaryLauncher]; ok {
level.Debug(ta.logger).Log("msg", "launcher updated -- exiting to load new version", "new_binary_version", updatedVersion)
ta.signalRestart <- NewLauncherReloadNeededErr(updatedVersion)
return nil
// Only reload if we're not using a localdev path
if ta.knapsack.LocalDevelopmentPath() == "" {
level.Debug(ta.logger).Log("msg", "launcher updated -- exiting to load new version", "new_binary_version", updatedVersion)
ta.signalRestart <- NewLauncherReloadNeededErr(updatedVersion)
return nil
}
}

// For non-launcher binaries (i.e. osqueryd), call any reload functions we have saved
Expand Down
1 change: 1 addition & 0 deletions ee/tuf/autoupdate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func TestExecute_launcherUpdate(t *testing.T) {
mockKnapsack.On("TufServerURL").Return(tufServerUrl)
mockKnapsack.On("UpdateDirectory").Return("")
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
mockKnapsack.On("LocalDevelopmentPath").Return("")
mockQuerier := newMockQuerier(t)

// Set up autoupdater
Expand Down
82 changes: 82 additions & 0 deletions ee/tuf/finalize_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//go:build linux
// +build linux

package tuf

import (
"bytes"
"context"
"debug/elf"
"errors"
"fmt"
"path/filepath"
"time"

"github.com/kolide/launcher/pkg/allowedcmd"
)

// On NixOS, we have to set the interpreter for any non-NixOS executable we want to
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved
// run. This means the binaries that our updater downloads.
// See: https://unix.stackexchange.com/a/522823
func patchExecutable(executableLocation string) error {
if !allowedcmd.IsNixOS() {
return nil
}

interpreter, err := getInterpreter(executableLocation)
if err != nil {
return fmt.Errorf("getting interpreter for %s: %w", executableLocation, err)
}
interpreterLocation, err := findInterpreterInNixStore(interpreter)
Fixed Show fixed Hide fixed
if err != nil {
return fmt.Errorf("finding interpreter %s in nix store: %w", interpreter, err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

cmd, err := allowedcmd.Patchelf(ctx, "--set-interpreter", interpreterLocation, executableLocation)
if err != nil {
return fmt.Errorf("creating patchelf command: %w", err)
}

if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("running patchelf: output `%s`, error `%w`", string(out), err)
}

return nil
}

func getInterpreter(executableLocation string) (string, error) {
f, err := elf.Open(executableLocation)
if err != nil {
return "", fmt.Errorf("opening ELF file: %w", err)
}
defer f.Close()

interpSection := f.Section(".interp")
if interpSection == nil {
return "", errors.New("no .interp section")
}

interpData, err := interpSection.Data()
if err != nil {
return "", fmt.Errorf("reading .interp section: %w", err)
}

trimmedInterpData := bytes.TrimRight(interpData, "\x00")

// interpData should look something like "/lib64/ld-linux-x86-64.so.2" -- grab just the filename
return filepath.Base(string(trimmedInterpData)), nil
}

func findInterpreterInNixStore(interpreter string) (string, error) {
storeLocationPattern := filepath.Join("/nix/store/*glibc*/lib", interpreter)
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved

matches, err := filepath.Glob(storeLocationPattern)
if err != nil {
return "", fmt.Errorf("globbing for interpreter %s at %s: %w", interpreter, storeLocationPattern, err)
}

return matches[0], nil
}
24 changes: 24 additions & 0 deletions ee/tuf/finalize_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build linux
// +build linux

package tuf

import (
"os"
"testing"

"github.com/stretchr/testify/require"
)

func Test_getInterpreter(t *testing.T) {
t.Parallel()

// Use the current executable in our test
currentRunningExecutable, err := os.Executable()
require.NoError(t, err, "getting current executable")

// Confirm we pick the expected interpreter
interpreter, err := getInterpreter(currentRunningExecutable)
require.NoError(t, err, "expected no error getting interpreter")
require.Equal(t, "ld-linux-x86-64.so.2", interpreter)
}
8 changes: 8 additions & 0 deletions ee/tuf/finalize_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !linux
// +build !linux

package tuf

func patchExecutable(executableLocation string) error {
return nil
}
17 changes: 17 additions & 0 deletions ee/tuf/finalize_other_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !linux
// +build !linux

package tuf

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_patchExecutable(t *testing.T) {
t.Parallel()

// patchExecutable is a no-op on windows and darwin
require.NoError(t, patchExecutable(""))
}
5 changes: 5 additions & 0 deletions ee/tuf/library_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ func (ulm *updateLibraryManager) moveVerifiedUpdate(binary autoupdatableBinary,
return fmt.Errorf("could not set +x permissions on executable: %w", err)
}

// If necessary, patch the executable (NixOS only)
if err := patchExecutable(executableLocation(stagedVersionedDirectory, binary)); err != nil {
return fmt.Errorf("could not patch executable: %w", err)
}

// Validate the executable
if err := autoupdate.CheckExecutable(context.TODO(), executableLocation(stagedVersionedDirectory, binary), "--version"); err != nil {
return fmt.Errorf("could not verify executable: %w", err)
Expand Down
22 changes: 16 additions & 6 deletions pkg/allowedcmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
)

type AllowedCommand func(ctx context.Context, arg ...string) (*exec.Cmd, error)
Expand Down Expand Up @@ -38,14 +37,25 @@ func validatedCommand(ctx context.Context, knownPath string, arg ...string) (*ex
}

func allowSearchPath() bool {
if runtime.GOOS != "linux" {
return false
return IsNixOS()
}

// Save results of lookup so we don't have to stat for /etc/NIXOS every time
// we want to know.
var (
checkedIsNixOS = false
isNixOS = false
Comment on lines +46 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

Could also use a pointer to a bool. But this is fine

)

func IsNixOS() bool {
if checkedIsNixOS {
return isNixOS
}

// We only allow searching for binaries in PATH on NixOS
if _, err := os.Stat("/etc/NIXOS"); err == nil {
return true
isNixOS = true
}

return false
checkedIsNixOS = true
return isNixOS
}
4 changes: 4 additions & 0 deletions pkg/allowedcmd/cmd_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func Pacman(ctx context.Context, arg ...string) (*exec.Cmd, error) {
return validatedCommand(ctx, "/usr/bin/pacman", arg...)
}

func Patchelf(ctx context.Context, arg ...string) (*exec.Cmd, error) {
return validatedCommand(ctx, "/run/current-system/sw/bin/patchelf", arg...)
}

func Ps(ctx context.Context, arg ...string) (*exec.Cmd, error) {
return validatedCommand(ctx, "/usr/bin/ps", arg...)
}
Expand Down
Loading