common/utility/utilty_unix.go (120 lines of code) (raw):
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//go:build freebsd || linux || netbsd || openbsd
package utility
import (
"fmt"
"github.com/aws/amazon-ssm-agent/agent/log"
"os"
"os/exec"
"os/user"
"runtime/debug"
"strings"
"syscall"
"time"
)
const (
// ExpectedServiceRunningUser is the user we expect the agent to be running as
ExpectedServiceRunningUser = "root"
cloudInitPath = "/usr/bin/cloud-init"
)
var (
userCurrent = user.Current
osStat = os.Stat
execCommand = exec.Command
)
func killProcessGroupOnTimeout(log log.T, command *exec.Cmd, timer *time.Timer, doneChan chan struct{}) {
defer func() {
if r := recover(); r != nil {
log.Errorf("kill process on timeout panic: \n%v", r)
log.Errorf("stacktrace:\n%s", debug.Stack())
}
}()
select {
case <-timer.C:
if command.Process == nil {
log.Warn("process already exited")
return
}
log.Errorf("timeout reached, killing process %v", command.Process.Pid)
if err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL); err != nil {
log.Errorf("failed to kill process %v. Err: %v", command.Process.Pid, err)
}
log.Tracef("process %v killed", command.Process.Pid)
case <-doneChan:
// Drain channel
<-timer.C
return
}
}
func sigTermProcessGroupOnTimeout(log log.T, command *exec.Cmd, timer *time.Timer, doneChan chan struct{}) {
select {
case <-timer.C:
if command.Process == nil {
log.Warn("process already exited")
return
}
log.Errorf("process %v timed out, sending SIGTERM", command.Process.Pid)
case <-doneChan:
// Drain channel
<-timer.C
return
}
if err := syscall.Kill(-command.Process.Pid, syscall.SIGTERM); err != nil {
log.Errorf("failed to send SIGTERM to pid %v. Killing process. Err: %v", command.Process.Pid, err)
if err := command.Process.Kill(); err != nil {
log.Errorf("failed to kill process %v. Err: %v", command.Process.Pid, err)
return
}
}
killTimeout := 30 * time.Second
killTimer := time.NewTimer(killTimeout)
go killProcessGroupOnTimeout(log, command, killTimer, doneChan)
}
// WaitForCloudInit waits for cloud-init to complete
func WaitForCloudInit(log log.T, timeoutSeconds int) error {
if timeoutSeconds <= 0 {
return fmt.Errorf("invalid timeout value %d", timeoutSeconds)
}
// Check if cloud-init is installed and executable
if fileInfo, err := osStat(cloudInitPath); err != nil {
return fmt.Errorf("cloud-init binary not found at %s. Err: %w", cloudInitPath, err)
} else if fileInfo.Mode().Perm()&0111 == 0 {
return fmt.Errorf("cloud-init binary is found but not executable")
} else {
// Wait for cloud-init
log.Debug("Waiting for cloud-init completion...")
command := execCommand(cloudInitPath, "status", "--wait")
// Set process group ID so the command and all its children become a new
// process group and all sub-process children can be sent SIGTERM and SIGKILL signals
// Group ID is set to the process ID of the command
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := command.Start(); err != nil || command.Process == nil {
return fmt.Errorf("failed to start cloud-init command. Err: %v", err)
}
timer := time.NewTimer(time.Duration(timeoutSeconds) * time.Second)
var doneChan = make(chan struct{})
go sigTermProcessGroupOnTimeout(log, command, timer, doneChan)
err = command.Wait()
timedOut := !timer.Stop()
if !timedOut {
doneChan <- struct{}{}
close(doneChan)
}
if err == nil {
log.Debug("cloud-init is finished execution.")
return nil
} else if strings.Contains(err.Error(), "no child processes") {
return fmt.Errorf("cloud-init timed out after %d seconds", timeoutSeconds)
}
log.Debugf("command returned error %v", err)
if exitErr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode := status.ExitStatus()
if exitCode == -1 && timedOut {
return fmt.Errorf("timed out waiting for cloud-init")
}
}
}
return fmt.Errorf("error response from cloud-init command: %w", err)
}
}
// IsRunningElevatedPermissions checks if current user is administrator
func IsRunningElevatedPermissions() error {
currentUser, err := userCurrent()
if err != nil {
return err
}
if currentUser.Username == ExpectedServiceRunningUser {
return nil
} else {
return fmt.Errorf("binary needs to be executed by %s", ExpectedServiceRunningUser)
}
}