Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,19 @@ PARROT_TOOL ?= $(TOOL_BIN)/parrot$(exe_suffix)
PARROT_TOOL_CONTAINER_BINARY ?= $(TOOL_BIN)/parrot_c
CONTAINER_PROBE_TOOL_CONTAINER_BINARY ?= $(TOOL_BIN)/container_probe_c
TERMCHILD_TOOL ?= $(TOOL_BIN)/termchild$(exe_suffix)
SIGNAL_DISPOSITION_TOOL ?= $(TOOL_BIN)/signal-disposition$(exe_suffix)
GO_LICENSES ?= $(TOOL_BIN)/go-licenses$(exe_suffix)
PROTOC ?= $(TOOL_BIN)/protoc/bin/protoc$(exe_suffix)
GO_RUNTIME_OVERLAY_DIR ?= $(TOOL_BIN)/go-runtime-overlay
GO_RUNTIME_OVERLAY_FILE ?= $(GO_RUNTIME_OVERLAY_DIR)/overlay.json

ifeq ($(build_os),darwin)
GO_RUNTIME_OVERLAY_ARG := -overlay="$(GO_RUNTIME_OVERLAY_FILE)"
GO_RUNTIME_OVERLAY_PREREQ := $(GO_RUNTIME_OVERLAY_FILE)
else
GO_RUNTIME_OVERLAY_ARG :=
GO_RUNTIME_OVERLAY_PREREQ :=
endif

# Tool Versions
PROTOC_VERSION ?= 33.5
Expand Down Expand Up @@ -167,6 +178,9 @@ help: ## Display this help.

##@ Code generation

.PHONY: generate-go-runtime-overlay
generate-go-runtime-overlay: $(GO_RUNTIME_OVERLAY_PREREQ) ## Generate the Darwin Go runtime overlay used by DCP builds

.PHONY: generate
generate: generate-object-methods generate-openapi generate-goversioninfo generate-grpc ## Generate artifacts needed for DCP binary build: object copy methods, OpenAPI definitions, binary version info, and gRPC files.

Expand Down Expand Up @@ -293,8 +307,8 @@ build-ci: generate-ci release ## Runs codegen, including license/notice files, t

.PHONY: build-dcp
build-dcp: $(DCP_BINARY) ## Builds DCP CLI binary
$(DCP_BINARY): $(GO_SOURCES) go.mod | ${OUTPUT_BIN}
$(GO_BIN) build -o $(DCP_BINARY) $(BUILD_ARGS) ./cmd/dcp
$(DCP_BINARY): $(GO_SOURCES) go.mod $(GO_RUNTIME_OVERLAY_PREREQ) | ${OUTPUT_BIN}
$(GO_BIN) build -o $(DCP_BINARY) $(GO_RUNTIME_OVERLAY_ARG) $(BUILD_ARGS) ./cmd/dcp

.PHONY: build-dcptun-containerexe
build-dcptun-containerexe: $(DCPTUN_CLIENT_BINARY) ## Builds DCP reverse network tunnel client binary for Linux (to be used in containers)
Expand Down Expand Up @@ -344,6 +358,9 @@ TEST_PREREQS := generate-grpc .WAIT build-dcp build-dcptun-containerexe containe
else
TEST_PREREQS := generate-grpc build-dcp build-dcptun-containerexe container-probe-tool-containerexe delay-tool lfwriter-tool parrot-tool parrot-tool-containerexe termchild-tool
endif
ifeq ($(build_os),darwin)
TEST_PREREQS := $(TEST_PREREQS) signal-disposition-tool
endif

.PHONY: test-prereqs
test-prereqs: BUILD_ARGS := $(BUILD_ARGS) -gcflags="all=-N -l" -ldflags "$(version_values)"
Expand All @@ -361,13 +378,13 @@ TEST_OPTS := $(COMMON_TEST_OPTS) -race
endif

.PHONY: test
test: test-prereqs ## Run all tests in the repository
$(GO_BIN) test ./... $(TEST_OPTS) -parallel 32
test: test-prereqs $(GO_RUNTIME_OVERLAY_PREREQ) ## Run all tests in the repository
$(GO_BIN) test ./... $(GO_RUNTIME_OVERLAY_ARG) $(TEST_OPTS) -parallel 32

# NOTE: Keep scripts/test-ci.ps1 in sync with test-ci (see comment above TEST_PREREQS).
.PHONY: test-ci
test-ci: test-ci-prereqs ## Runs tests in a way appropriate for CI pipeline, with linting etc.
$(GO_BIN) test ./... $(TEST_OPTS)
test-ci: test-ci-prereqs $(GO_RUNTIME_OVERLAY_PREREQ) ## Runs tests in a way appropriate for CI pipeline, with linting etc.
$(GO_BIN) test ./... $(GO_RUNTIME_OVERLAY_ARG) $(TEST_OPTS)

## Development and test support targets

Expand All @@ -381,6 +398,15 @@ ${OUTPUT_BIN}/ext/bin/: | ${OUTPUT_BIN}
$(TOOL_BIN):
$(mkdir) $(TOOL_BIN)

ifeq ($(build_os),darwin)
.PHONY: force-go-runtime-overlay
force-go-runtime-overlay:
# Apply the upstream fix for golang/go#81009 to the selected toolchain without
# replacing unrelated standard-library source.
$(GO_RUNTIME_OVERLAY_FILE): force-go-runtime-overlay $(wildcard ./internal/tools/goruntimeoverlay/*) | $(TOOL_BIN)
$(CLEAR_GOARGS) $(GO_BIN) run ./internal/tools/goruntimeoverlay --output-dir "$(GO_RUNTIME_OVERLAY_DIR)"
endif

$(DCP_DIR):
$(mkdir) $(DCP_DIR)

Expand All @@ -405,26 +431,35 @@ endif
# delay-tool is used for process package testing
.PHONY: delay-tool
delay-tool: $(DELAY_TOOL)
$(DELAY_TOOL): $(wildcard ./test/delay/*.go) | $(TOOL_BIN)
$(GO_BIN) build -o $(DELAY_TOOL) github.com/microsoft/dcp/test/delay
$(DELAY_TOOL): $(wildcard ./test/delay/*.go) $(GO_RUNTIME_OVERLAY_PREREQ) | $(TOOL_BIN)
$(GO_BIN) build -o $(DELAY_TOOL) $(GO_RUNTIME_OVERLAY_ARG) github.com/microsoft/dcp/test/delay

# termchild-tool is used for internal/termpty pseudo-terminal tests
.PHONY: termchild-tool
termchild-tool: $(TERMCHILD_TOOL)
$(TERMCHILD_TOOL): $(wildcard ./test/termchild/*.go) | $(TOOL_BIN)
$(GO_BIN) build -o $(TERMCHILD_TOOL) github.com/microsoft/dcp/test/termchild
$(TERMCHILD_TOOL): $(wildcard ./test/termchild/*.go) $(GO_RUNTIME_OVERLAY_PREREQ) | $(TOOL_BIN)
$(GO_BIN) build -o $(TERMCHILD_TOOL) $(GO_RUNTIME_OVERLAY_ARG) github.com/microsoft/dcp/test/termchild

# signal-disposition captures the signal state inherited by an exec'd child before
# its Go runtime initializes.
ifeq ($(build_os),darwin)
.PHONY: signal-disposition-tool
signal-disposition-tool: $(SIGNAL_DISPOSITION_TOOL)
$(SIGNAL_DISPOSITION_TOOL): $(wildcard ./test/signaldisposition/*.go) $(GO_RUNTIME_OVERLAY_PREREQ) | $(TOOL_BIN)
CGO_ENABLED=1 $(GO_BIN) build -o $(SIGNAL_DISPOSITION_TOOL) $(GO_RUNTIME_OVERLAY_ARG) github.com/microsoft/dcp/test/signaldisposition
endif

# lfwriter tool is used for testing lockfile package
.PHONY: lfwriter-tool
lfwriter-tool: $(LFWRITER_TOOL)
$(LFWRITER_TOOL): $(wildcard ./test/lfwriter/*.go) | $(TOOL_BIN)
$(GO_BIN) build -o $(LFWRITER_TOOL) github.com/microsoft/dcp/test/lfwriter
$(LFWRITER_TOOL): $(wildcard ./test/lfwriter/*.go) $(GO_RUNTIME_OVERLAY_PREREQ) | $(TOOL_BIN)
$(GO_BIN) build -o $(LFWRITER_TOOL) $(GO_RUNTIME_OVERLAY_ARG) github.com/microsoft/dcp/test/lfwriter

# parrot tool is used for testing network connectivity
.PHONY: parrot-tool
parrot-tool: $(PARROT_TOOL)
$(PARROT_TOOL): $(wildcard ./test/parrot/*.go) | $(TOOL_BIN)
$(GO_BIN) build -o $(PARROT_TOOL) github.com/microsoft/dcp/test/parrot
$(PARROT_TOOL): $(wildcard ./test/parrot/*.go) $(GO_RUNTIME_OVERLAY_PREREQ) | $(TOOL_BIN)
$(GO_BIN) build -o $(PARROT_TOOL) $(GO_RUNTIME_OVERLAY_ARG) github.com/microsoft/dcp/test/parrot

# Builds a static parrot binary suitable for the scratch-based test container image.
.PHONY: parrot-tool-containerexe
Expand Down
6 changes: 0 additions & 6 deletions internal/dcp/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,6 @@ func NewRootCmd(log *logger.Logger) (*cobra.Command, error) {
rootCmd.AddCommand(cmd)
}

if cmd, err = dcpproc_cmds.NewForkProcessExecCommand(log.Logger); err != nil {
return nil, fmt.Errorf("could not set up '%s' command: %w", dcpproc_cmds.ForkProcessExecCmdName, err)
} else {
rootCmd.AddCommand(cmd)
}

// Add dcptun sub-commands
rootCmd.AddCommand(dcptun_cmds.NewRunServerCommand(log.Logger))

Expand Down
139 changes: 1 addition & 138 deletions internal/dcpproc/commands/fork_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ package commands
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"syscall"

"github.com/go-logr/logr"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -61,14 +57,6 @@ func forkProcess(log logr.Logger) func(cmd *cobra.Command, args []string) error
logger.WithSessionId(childCmd)
process.ForkFromParent(childCmd)

execShim, shimErr := useExecShim(childCmd)
if shimErr != nil {
return shimErr
}
if execShim != nil {
defer execShim.close()
}

monitorEnabled := cmd.Flags().Changed("monitor")
var monitorCtx context.Context
var monitorCtxCancel context.CancelFunc
Expand All @@ -83,7 +71,7 @@ func forkProcess(log logr.Logger) func(cmd *cobra.Command, args []string) error
}
}

pid, childExitInfoCh, disposeChildExecutor, startErr := startForkedProcess(cmd, childCmd, execShim, monitorEnabled, log)
pid, childExitInfoCh, disposeChildExecutor, startErr := startForkedProcess(cmd, childCmd, monitorEnabled, log)
if startErr != nil {
return startErr
}
Expand Down Expand Up @@ -127,7 +115,6 @@ func forkProcess(log logr.Logger) func(cmd *cobra.Command, args []string) error
func startForkedProcess(
cmd *cobra.Command,
childCmd *exec.Cmd,
execShim *execShimHandshake,
observeExit bool,
log logr.Logger,
) (process.Pid_t, <-chan process.ProcessExitInfo, func(), error) {
Expand All @@ -154,18 +141,6 @@ func startForkedProcess(
return process.UnknownPID, nil, nil, fmt.Errorf("could not start forked process: %w", startErr)
}

// Starting the shim only means dcp itself started. The PID must not be reported before the
// requested program is known to be running, so that a program which cannot be executed is
// still reported as a start failure.
if execShim != nil {
if execErr := execShim.wait(); execErr != nil {
// The logger already carries the command and arguments.
log.Error(execErr, "Failed to execute forked process")
executor.Dispose()
return process.UnknownPID, nil, nil, fmt.Errorf("could not start forked process: %w", execErr)
}
}

pid := handle.Pid
if _, writeErr := fmt.Fprintln(cmd.OutOrStdout(), pid); writeErr != nil {
log.Error(writeErr, "Failed to write forked process PID", "PID", pid)
Expand All @@ -188,115 +163,3 @@ func trimForkProcessArgSeparator(args []string) []string {

return args
}

// Redirects the child through the 'fork-process-exec' command on platforms where the child would
// otherwise inherit an invalid SIGUSR1 disposition from the Go runtime. The shim cleans that
// disposition and then execs the original program, which keeps the process ID, session, standard
// streams, and exit code that the caller of 'fork-process' expects.
//
// The reset cannot be done here: the Go runtime restores its own signal dispositions in the
// forked child before it reaches execve, so it has to happen in the process that calls exec.
//
// Returns the handshake that reports whether the shim reached the requested program, or nil when
// the child is started directly. The caller owns the returned handshake and must close it.
func useExecShim(childCmd *exec.Cmd) (*execShimHandshake, error) {
if !process.NeedsExecSignalDispositionWorkaround() {
return nil, nil
}

if childCmd.Err != nil {
// The program could not be located. Leave the command untouched so that starting it
// reports that original failure rather than one from the shim.
return nil, nil
}

callerSIGUSR1Ignored, dispositionErr := process.InheritedSIGUSR1Ignored()
if dispositionErr != nil {
return nil, fmt.Errorf("could not determine the inherited SIGUSR1 disposition: %w", dispositionErr)
}

return useExecShimWithDisposition(childCmd, callerSIGUSR1Ignored)
}

func useExecShimWithDisposition(
childCmd *exec.Cmd,
callerSIGUSR1Ignored bool,
) (*execShimHandshake, error) {
dcpPath, dcpPathErr := os.Executable()
if dcpPathErr != nil {
return nil, fmt.Errorf("could not determine the path of the current executable: %w", dcpPathErr)
}

statusR, statusW, pipeErr := os.Pipe()
if pipeErr != nil {
return nil, fmt.Errorf("could not create the exec status pipe: %w", pipeErr)
}

shimArgs := []string{
dcpPath,
ForkProcessExecCmdName,
"--" + execPathFlagName, childCmd.Path,
"--" + callerSIGUSR1IgnoredFlagName + "=" + strconv.FormatBool(callerSIGUSR1Ignored),
}
shimArgs = append(shimArgs, "--")
childCmd.Args = append(shimArgs, childCmd.Args...)
childCmd.Path = dcpPath

// The shim reports the outcome of the exec on this descriptor. It is the only extra file, so
// the shim sees it as execStatusFd.
childCmd.ExtraFiles = append(childCmd.ExtraFiles, statusW)

return &execShimHandshake{statusR: statusR, statusW: statusW}, nil
}

// execShimHandshake reports whether the shim managed to exec the requested program. Starting the
// shim only proves that dcp itself could be started, so without this the caller would be told
// that a program which never ran had started successfully.
//
// The shim inherits the write end. A successful execve closes it and the read end reports EOF,
// while a failure sends the errno before the shim exits.
type execShimHandshake struct {
statusR *os.File
statusW *os.File
}

// wait blocks until the shim either replaces itself with the requested program or reports why it
// could not. It returns the failure that a direct start would have reported.
func (h *execShimHandshake) wait() error {
// The write end is now owned by the shim. The parent's copy has to go, because the read below
// only reports EOF once every writer is closed.
h.closeWriteEnd()

status, readErr := io.ReadAll(h.statusR)
if readErr != nil {
return fmt.Errorf("could not read the exec status: %w", readErr)
}

if len(status) == 0 {
// EOF with nothing written: the descriptor was closed by a successful execve.
return nil
}

errnoValue, parseErr := strconv.Atoi(strings.TrimSpace(string(status)))
if parseErr != nil {
return fmt.Errorf("the exec status %q could not be parsed: %w", status, parseErr)
}

return syscall.Errno(errnoValue)
}

func (h *execShimHandshake) closeWriteEnd() {
if h.statusW != nil {
_ = h.statusW.Close()
h.statusW = nil
}
}

func (h *execShimHandshake) close() {
h.closeWriteEnd()

if h.statusR != nil {
_ = h.statusR.Close()
h.statusR = nil
}
}
Loading
Loading