//go:build darwin || linux package workspace import ( "bytes" "encoding/json" "errors" "io" "os" "os/exec" "path/filepath" "strings" "syscall" "golang.org/x/sys/unix" ) const ( commandRootFD = 3 commandRecordFD = 4 commandStatusFD = 5 commandShimExit = 125 ) func startCommandProcess(record commandLaunchRecord, directory *os.File, output *commandOutput) (*commandProcess, error) { encoded, err := json.Marshal(record) if err != nil || len(encoded) == 0 || len(encoded) > commandLaunchRecordLimit || directory == nil { return nil, errCommandLaunchInvalid } rootFD, err := unix.Dup(int(directory.Fd())) if err != nil { return nil, err } root := os.NewFile(uintptr(rootFD), "workspace-root") recordReader, recordWriter, err := os.Pipe() if err != nil { _ = root.Close() return nil, err } statusReader, statusWriter, err := os.Pipe() if err != nil { _ = root.Close() _ = recordReader.Close() _ = recordWriter.Close() return nil, err } closeAll := func() { _ = root.Close() _ = recordReader.Close() _ = recordWriter.Close() _ = statusReader.Close() _ = statusWriter.Close() } currentExecutable, err := os.Executable() if err != nil { closeAll() return nil, err } if !filepath.IsAbs(currentExecutable) { closeAll() return nil, errCommandLaunchInvalid } cmd := exec.Command(currentExecutable, commandShimArgument) cmd.Env = []string{commandShimEnvironment + "=1"} cmd.ExtraFiles = []*os.File{root, recordReader, statusWriter} cmd.Stdout = output.writer(false) cmd.Stderr = output.writer(true) cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err := cmd.Start(); err != nil { closeAll() return nil, err } _ = root.Close() _ = recordReader.Close() _ = statusWriter.Close() go func() { _, _ = io.Copy(recordWriter, bytes.NewReader(encoded)) _ = recordWriter.Close() }() launch := make(chan commandLaunchStatus, 1) go func() { data, readErr := io.ReadAll(io.LimitReader(statusReader, 2)) _ = statusReader.Close() launch <- commandLaunchStatus{started: readErr == nil && len(data) == 0} }() wait := make(chan error, 1) go func() { wait <- cmd.Wait() }() return &commandProcess{ wait: wait, launch: launch, pid: cmd.Process.Pid, exitCode: func() int32 { if cmd.ProcessState == nil { return -1 } return int32(cmd.ProcessState.ExitCode()) }, }, nil } func runCommandShim() int { status := os.NewFile(commandStatusFD, "workspace-command-status") fail := func() int { if status != nil { _, _ = status.Write([]byte{'F'}) _ = status.Close() } return commandShimExit } if status == nil { return commandShimExit } unix.CloseOnExec(commandStatusFD) recordFile := os.NewFile(commandRecordFD, "workspace-command-record") root := os.NewFile(commandRootFD, "workspace-root") if recordFile == nil || root == nil { return fail() } defer recordFile.Close() defer root.Close() encoded, err := io.ReadAll(io.LimitReader(recordFile, commandLaunchRecordLimit+1)) if err != nil || len(encoded) == 0 || len(encoded) > commandLaunchRecordLimit { return fail() } decoder := json.NewDecoder(bytes.NewReader(encoded)) decoder.DisallowUnknownFields() var record commandLaunchRecord if err := decoder.Decode(&record); err != nil { return fail() } if err := ensureJSONEOF(decoder); err != nil || !validLaunchRecord(record) { return fail() } var stat unix.Stat_t if err := unix.Fstat(commandRootFD, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != record.Device || uint64(stat.Ino) != record.Inode { return fail() } if err := unix.Fchdir(commandRootFD); err != nil { return fail() } _ = recordFile.Close() _ = root.Close() argv := make([]string, 1, len(record.Args)+1) argv[0] = record.Executable argv = append(argv, record.Args...) if err := unix.Exec(record.Executable, argv, record.Environment); err != nil { return fail() } return commandShimExit } func ensureJSONEOF(decoder *json.Decoder) error { var extra any if err := decoder.Decode(&extra); err != io.EOF { if err == nil { return errors.New("workspace command record has trailing data") } return err } return nil } func validLaunchRecord(record commandLaunchRecord) bool { if record.Version != commandLaunchVersion || !filepath.IsAbs(record.Executable) || filepath.Clean(record.Executable) != record.Executable || strings.IndexByte(record.Executable, 0) >= 0 { return false } for _, arg := range record.Args { if strings.IndexByte(arg, 0) >= 0 { return false } } seen := make(map[string]struct{}, len(record.Environment)) for _, item := range record.Environment { name, value, ok := strings.Cut(item, "=") if !ok || !validEnvironmentName(name) || name == commandShimEnvironment || strings.IndexByte(value, 0) >= 0 { return false } if _, duplicate := seen[name]; duplicate { return false } seen[name] = struct{}{} } return true } func terminateProcessGroup(pid int) { if pid <= 0 { return } _ = syscall.Kill(-pid, syscall.SIGTERM) _ = syscall.Kill(-pid, syscall.SIGKILL) }