bd6e4ae215
Go 1.15.7 contained a security fix for CVE-2021-3115, which allowed arbitrary
code to be executed at build time when using cgo on Windows.
This issue was not limited to the go command itself, and could also affect binaries
that use `os.Command`, `os.LookPath`, etc.
From the related blogpost (https://blog.golang.org/path-security):
> Are your own programs affected?
>
> If you use exec.LookPath or exec.Command in your own programs, you only need to
> be concerned if you (or your users) run your program in a directory with untrusted
> contents. If so, then a subprocess could be started using an executable from dot
> instead of from a system directory. (Again, using an executable from dot happens
> always on Windows and only with uncommon PATH settings on Unix.)
>
> If you are concerned, then we’ve published the more restricted variant of os/exec
> as golang.org/x/sys/execabs. You can use it in your program by simply replacing
At time of the go1.15 release, the Go team considered changing the behavior of
`os.LookPath()` and `exec.LookPath()` to be a breaking change, and made the
behavior "opt-in" by providing the `golang.org/x/sys/execabs` package as a
replacement.
However, for the go1.19 release, this changed, and the default behavior of
`os.LookPath()` and `exec.LookPath()` was changed. From the release notes:
https://go.dev/doc/go1.19#os-exec-path
> Command and LookPath no longer allow results from a PATH search to be found
> relative to the current directory. This removes a common source of security
> problems but may also break existing programs that depend on using, say,
> exec.Command("prog") to run a binary named prog (or, on Windows, prog.exe)
> in the current directory. See the os/exec package documentation for information
> about how best to update such programs.
>
> On Windows, Command and LookPath now respect the NoDefaultCurrentDirectoryInExePath
> environment variable, making it possible to disable the default implicit search
> of “.” in PATH lookups on Windows systems.
A result of this change was that registering the daemon as a Windows service
no longer worked when done from within the directory of the binary itself:
C:\> cd "Program Files\Docker\Docker\resources"
C:\Program Files\Docker\Docker\resources> dockerd --register-service
exec: "dockerd": cannot run executable found relative to current directory
Note that using an absolute path would work around the issue:
C:\Program Files\Docker\Docker>resources\dockerd.exe --register-service
This patch changes `registerService()` to use `os.Executable()`, instead of
depending on `os.Args[0]` and `exec.LookPath()` for resolving the absolute
path of the binary.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 3e8fda0a70
)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
387 lines
8.2 KiB
Go
387 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/pflag"
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/svc"
|
|
"golang.org/x/sys/windows/svc/debug"
|
|
"golang.org/x/sys/windows/svc/eventlog"
|
|
"golang.org/x/sys/windows/svc/mgr"
|
|
)
|
|
|
|
var (
|
|
flServiceName *string
|
|
flRegisterService *bool
|
|
flUnregisterService *bool
|
|
flRunService *bool
|
|
|
|
oldStderr windows.Handle
|
|
panicFile *os.File
|
|
|
|
service *handler
|
|
)
|
|
|
|
const (
|
|
// These should match the values in event_messages.mc.
|
|
eventInfo = 1
|
|
eventWarn = 1
|
|
eventError = 1
|
|
eventDebug = 2
|
|
eventPanic = 3
|
|
eventFatal = 4
|
|
|
|
eventExtraOffset = 10 // Add this to any event to get a string that supports extended data
|
|
)
|
|
|
|
func installServiceFlags(flags *pflag.FlagSet) {
|
|
flServiceName = flags.String("service-name", "docker", "Set the Windows service name")
|
|
flRegisterService = flags.Bool("register-service", false, "Register the service and exit")
|
|
flUnregisterService = flags.Bool("unregister-service", false, "Unregister the service and exit")
|
|
flRunService = flags.Bool("run-service", false, "")
|
|
_ = flags.MarkHidden("run-service")
|
|
}
|
|
|
|
type handler struct {
|
|
tosvc chan bool
|
|
fromsvc chan error
|
|
daemonCli *DaemonCli
|
|
}
|
|
|
|
type etwHook struct {
|
|
log *eventlog.Log
|
|
}
|
|
|
|
func (h *etwHook) Levels() []logrus.Level {
|
|
return []logrus.Level{
|
|
logrus.PanicLevel,
|
|
logrus.FatalLevel,
|
|
logrus.ErrorLevel,
|
|
logrus.WarnLevel,
|
|
logrus.InfoLevel,
|
|
logrus.DebugLevel,
|
|
}
|
|
}
|
|
|
|
func (h *etwHook) Fire(e *logrus.Entry) error {
|
|
var (
|
|
etype uint16
|
|
eid uint32
|
|
)
|
|
|
|
switch e.Level {
|
|
case logrus.PanicLevel:
|
|
etype = windows.EVENTLOG_ERROR_TYPE
|
|
eid = eventPanic
|
|
case logrus.FatalLevel:
|
|
etype = windows.EVENTLOG_ERROR_TYPE
|
|
eid = eventFatal
|
|
case logrus.ErrorLevel:
|
|
etype = windows.EVENTLOG_ERROR_TYPE
|
|
eid = eventError
|
|
case logrus.WarnLevel:
|
|
etype = windows.EVENTLOG_WARNING_TYPE
|
|
eid = eventWarn
|
|
case logrus.InfoLevel:
|
|
etype = windows.EVENTLOG_INFORMATION_TYPE
|
|
eid = eventInfo
|
|
case logrus.DebugLevel:
|
|
etype = windows.EVENTLOG_INFORMATION_TYPE
|
|
eid = eventDebug
|
|
default:
|
|
return errors.New("unknown level")
|
|
}
|
|
|
|
// If there is additional data, include it as a second string.
|
|
exts := ""
|
|
if len(e.Data) > 0 {
|
|
fs := bytes.Buffer{}
|
|
for k, v := range e.Data {
|
|
fs.WriteString(k)
|
|
fs.WriteByte('=')
|
|
fmt.Fprint(&fs, v)
|
|
fs.WriteByte(' ')
|
|
}
|
|
|
|
exts = fs.String()[:fs.Len()-1]
|
|
eid += eventExtraOffset
|
|
}
|
|
|
|
if h.log == nil {
|
|
fmt.Fprintf(os.Stderr, "%s [%s]\n", e.Message, exts)
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
ss [2]*uint16
|
|
err error
|
|
)
|
|
|
|
ss[0], err = windows.UTF16PtrFromString(e.Message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
count := uint16(1)
|
|
if exts != "" {
|
|
ss[1], err = windows.UTF16PtrFromString(exts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
count++
|
|
}
|
|
|
|
return windows.ReportEvent(h.log.Handle, etype, 0, eid, 0, count, 0, &ss[0], nil)
|
|
}
|
|
|
|
func registerService() error {
|
|
p, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
|
|
c := mgr.Config{
|
|
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
|
|
StartType: mgr.StartAutomatic,
|
|
ErrorControl: mgr.ErrorNormal,
|
|
Dependencies: []string{},
|
|
DisplayName: "Docker Engine",
|
|
}
|
|
|
|
// Configure the service to launch with the arguments that were just passed.
|
|
args := []string{"--run-service"}
|
|
for _, a := range os.Args[1:] {
|
|
if a != "--register-service" && a != "--unregister-service" {
|
|
args = append(args, a)
|
|
}
|
|
}
|
|
|
|
s, err := m.CreateService(*flServiceName, p, c, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.Close()
|
|
|
|
err = s.SetRecoveryActions(
|
|
[]mgr.RecoveryAction{
|
|
{Type: mgr.ServiceRestart, Delay: 15 * time.Second},
|
|
{Type: mgr.ServiceRestart, Delay: 15 * time.Second},
|
|
{Type: mgr.NoAction},
|
|
},
|
|
uint32(24*time.Hour/time.Second),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return eventlog.Install(*flServiceName, p, false, eventlog.Info|eventlog.Warning|eventlog.Error)
|
|
}
|
|
|
|
func unregisterService() error {
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
|
|
s, err := m.OpenService(*flServiceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.Close()
|
|
|
|
eventlog.Remove(*flServiceName)
|
|
err = s.Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// initService is the entry point for running the daemon as a Windows
|
|
// service. It returns an indication to stop (if registering/un-registering);
|
|
// an indication of whether it is running as a service; and an error.
|
|
func initService(daemonCli *DaemonCli) (bool, bool, error) {
|
|
if *flUnregisterService {
|
|
if *flRegisterService {
|
|
return true, false, errors.New("--register-service and --unregister-service cannot be used together")
|
|
}
|
|
return true, false, unregisterService()
|
|
}
|
|
|
|
if *flRegisterService {
|
|
return true, false, registerService()
|
|
}
|
|
|
|
if !*flRunService {
|
|
return false, false, nil
|
|
}
|
|
|
|
// Check if we're running as a Windows service or interactively.
|
|
isService, err := svc.IsWindowsService()
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
|
|
h := &handler{
|
|
tosvc: make(chan bool),
|
|
fromsvc: make(chan error),
|
|
daemonCli: daemonCli,
|
|
}
|
|
|
|
var log *eventlog.Log
|
|
if isService {
|
|
log, err = eventlog.Open(*flServiceName)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
}
|
|
|
|
logrus.AddHook(&etwHook{log})
|
|
logrus.SetOutput(io.Discard)
|
|
|
|
service = h
|
|
go func() {
|
|
if isService {
|
|
err = svc.Run(*flServiceName, h)
|
|
} else {
|
|
err = debug.Run(*flServiceName, h)
|
|
}
|
|
|
|
h.fromsvc <- err
|
|
}()
|
|
|
|
// Wait for the first signal from the service handler.
|
|
err = <-h.fromsvc
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
return false, true, nil
|
|
}
|
|
|
|
func (h *handler) started() error {
|
|
// This must be delayed until daemonCli initializes Config.Root
|
|
err := initPanicFile(filepath.Join(h.daemonCli.Config.Root, "panic.log"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.tosvc <- false
|
|
return nil
|
|
}
|
|
|
|
func (h *handler) stopped(err error) {
|
|
logrus.Debugf("Stopping service: %v", err)
|
|
h.tosvc <- err != nil
|
|
<-h.fromsvc
|
|
}
|
|
|
|
func (h *handler) Execute(_ []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) {
|
|
s <- svc.Status{State: svc.StartPending, Accepts: 0}
|
|
// Unblock initService()
|
|
h.fromsvc <- nil
|
|
|
|
// Wait for initialization to complete.
|
|
failed := <-h.tosvc
|
|
if failed {
|
|
logrus.Debug("Aborting service start due to failure during initialization")
|
|
return true, 1
|
|
}
|
|
|
|
s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.Accepted(windows.SERVICE_ACCEPT_PARAMCHANGE)}
|
|
logrus.Debug("Service running")
|
|
Loop:
|
|
for {
|
|
select {
|
|
case failed = <-h.tosvc:
|
|
break Loop
|
|
case c := <-r:
|
|
switch c.Cmd {
|
|
case svc.Cmd(windows.SERVICE_CONTROL_PARAMCHANGE):
|
|
h.daemonCli.reloadConfig()
|
|
case svc.Interrogate:
|
|
s <- c.CurrentStatus
|
|
case svc.Stop, svc.Shutdown:
|
|
s <- svc.Status{State: svc.StopPending, Accepts: 0}
|
|
h.daemonCli.stop()
|
|
}
|
|
}
|
|
}
|
|
|
|
removePanicFile()
|
|
if failed {
|
|
return true, 1
|
|
}
|
|
return false, 0
|
|
}
|
|
|
|
func initPanicFile(path string) error {
|
|
var err error
|
|
panicFile, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o200)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
st, err := panicFile.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If there are contents in the file already, move the file out of the way
|
|
// and replace it.
|
|
if st.Size() > 0 {
|
|
panicFile.Close()
|
|
os.Rename(path, path+".old")
|
|
panicFile, err = os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update STD_ERROR_HANDLE to point to the panic file so that Go writes to
|
|
// it when it panics. Remember the old stderr to restore it before removing
|
|
// the panic file.
|
|
h, err := windows.GetStdHandle(windows.STD_ERROR_HANDLE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldStderr = h
|
|
|
|
err = windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(panicFile.Fd()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reset os.Stderr to the panic file (so fmt.Fprintf(os.Stderr,...) actually gets redirected)
|
|
os.Stderr = os.NewFile(panicFile.Fd(), "/dev/stderr")
|
|
|
|
// Force threads that panic to write to stderr (the panicFile handle now), otherwise it will go into the ether
|
|
log.SetOutput(os.Stderr)
|
|
|
|
return nil
|
|
}
|
|
|
|
func removePanicFile() {
|
|
if st, err := panicFile.Stat(); err == nil {
|
|
if st.Size() == 0 {
|
|
windows.SetStdHandle(windows.STD_ERROR_HANDLE, oldStderr)
|
|
panicFile.Close()
|
|
os.Remove(panicFile.Name())
|
|
}
|
|
}
|
|
}
|