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

crypto/x509: Get "https://google.com": tls: failed to verify certificate: SecPolicyCreateSSL error: 0 when running in child after parent exits #68557

Open
NorseGaud opened this issue Jul 23, 2024 · 5 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Darwin
Milestone

Comments

@NorseGaud
Copy link

NorseGaud commented Jul 23, 2024

THIS IS UNRELATED TO #66709

Go version

go version go1.22.5 darwin/arm64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/nathanpierce/Library/Caches/go-build'
GOENV='/Users/nathanpierce/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/nathanpierce/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/nathanpierce/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.22.5/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.22.5/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.22.5'
GCCGO='gccgo'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/nathanpierce/test/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/vt/byhkgjmd4pq6v8j2f9tsghf40000gn/T/go-build1722732525=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

I'm trying to create a child and then exit the parent so that I can create a daemon-like experience for my go application.

Here is the main.go you can reproduce with using go run main.go:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"syscall"
	"time"
)

// Mark of daemon process - system environment variable _GO_DAEMON=1
const (
	MARK_NAME  = "_GO_DAEMON"
	MARK_VALUE = "1"
)

// Default file permissions for log and pid files.
const FILE_PERM = os.FileMode(0640)

// WasReborn returns true in child process (daemon) and false in parent process.
func WasReborn() bool {
	return os.Getenv(MARK_NAME) == MARK_VALUE
}

// A Context describes daemon context.
type Context struct {
	// If PidFileName is non-empty, parent process will try to create and lock
	// pid file with given name. Child process writes process id to file.
	PidFileName string
	// Permissions for new pid file.
	PidFilePerm os.FileMode

	// If LogFileName is non-empty, parent process will create file with given name
	// and will link to fd 2 (stderr) for child process.
	LogFileName string
	// Permissions for new log file.
	LogFilePerm os.FileMode

	// If WorkDir is non-empty, the child changes into the directory before
	// creating the process.
	WorkDir string
	// If Chroot is non-empty, the child changes root directory
	Chroot string

	// If Env is non-nil, it gives the environment variables for the
	// daemon-process in the form returned by os.Environ.
	// If it is nil, the result of os.Environ will be used.
	Env []string
	// If Args is non-nil, it gives the command-line args for the
	// daemon-process. If it is nil, the result of os.Args will be used.
	Args []string

	// Credential holds user and group identities to be assumed by a daemon-process.
	Credential *syscall.Credential
	// If Umask is non-zero, the daemon-process call Umask() func with given value.
	Umask int

	// Struct contains only serializable public fields (!!!)
	abspath  string
	logFile  *os.File
	nullFile *os.File

	rpipe, wpipe *os.File
}

func (d *Context) SetLogFile(fd *os.File) {
	d.logFile = fd
}

func (d *Context) openFiles() (err error) {
	if d.PidFilePerm == 0 {
		d.PidFilePerm = FILE_PERM
	}
	if d.LogFilePerm == 0 {
		d.LogFilePerm = FILE_PERM
	}

	if d.nullFile, err = os.Open(os.DevNull); err != nil {
		return
	}

	if len(d.LogFileName) > 0 {
		if d.LogFileName == "/dev/stdout" {
			d.logFile = os.Stdout
		} else if d.LogFileName == "/dev/stderr" {
			d.logFile = os.Stderr
		} else if d.logFile, err = os.OpenFile(d.LogFileName,
			os.O_WRONLY|os.O_CREATE|os.O_APPEND, d.LogFilePerm); err != nil {
			return
		}
	}

	d.rpipe, d.wpipe, err = os.Pipe()
	return
}

func (d *Context) closeFiles() (err error) {
	fmt.Println("closeFiles")
	cl := func(file **os.File) {
		if *file != nil {
			(*file).Close()
			*file = nil
		}
	}
	cl(&d.rpipe)
	cl(&d.wpipe)
	cl(&d.logFile)
	cl(&d.nullFile)
	return
}

func (d *Context) prepareEnv() (err error) {
	if d.abspath, err = os.Executable(); err != nil {
		return
	}
	fmt.Println("abspath", d.abspath)

	if len(d.Args) == 0 {
		d.Args = os.Args
	}

	mark := fmt.Sprintf("%s=%s", MARK_NAME, MARK_VALUE)
	if len(d.Env) == 0 {
		d.Env = os.Environ()
	}
	d.Env = append(d.Env, mark)

	return
}

func (d *Context) files() (f []*os.File) {
	log := d.nullFile
	if d.logFile != nil {
		log = d.logFile
	}

	f = []*os.File{
		d.rpipe,    // (0) stdin
		log,        // (1) stdout
		log,        // (2) stderr
		d.nullFile, // (3) dup on fd 0 after initialization
	}

	return
}

func (d *Context) reborn() (child *os.Process, err error) {
	if !WasReborn() {
		child, err = d.parent()
	} else {
		err = d.child()
	}
	return
}

func (d *Context) parent() (child *os.Process, err error) {
	if err = d.prepareEnv(); err != nil {
		return
	}

	defer d.closeFiles()
	if err = d.openFiles(); err != nil {
		return
	}

	attr := &os.ProcAttr{
		Dir:   d.WorkDir,
		Env:   d.Env,
		Files: d.files(),
		Sys:   &syscall.SysProcAttr{},
	}

	fmt.Println("parent startProcess")
	if child, err = os.StartProcess(d.abspath, d.Args, attr); err != nil {
		return
	}

	d.rpipe.Close()
	encoder := json.NewEncoder(d.wpipe)
	err = encoder.Encode(d)

	fmt.Println("parent done")

	// if this sleep keeps the parent around, it will work.
	// time.Sleep(3 * time.Second)

	return
}

func (d *Context) child() (err error) {

	decoder := json.NewDecoder(os.Stdin)
	if err = decoder.Decode(d); err != nil {
		return
	}

	return
}

func main() {

	daemonContext := &Context{
		PidFileName: "test.pid",
		PidFilePerm: 0644,
		LogFileName: "test.log",
		LogFilePerm: 0640,
		WorkDir:     "/tmp",
		Umask:       027,
		Args:        []string{"SecPolicyCreateSSL"},
	}

	d, err := daemonContext.reborn()
	if err != nil {
		log.Fatalln(err)
	}
	if d != nil { // return the parent process since it's now forked into a child
		return
	}

	time.Sleep(2 * time.Second)

	fmt.Println("doing get...")
	resp, err := http.Get("https://google.com")
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
		return
	}
	fmt.Println(string(body)[:100])

	return
}

What did you see happen?

Once ran, you'll see in test.log:

doing get...
2024/07/23 13:07:21 Get "https://google.com": tls: failed to verify certificate: SecPolicyCreateSSL error: 0

Now under the parent function, uncomment // time.Sleep(3 * time.Second) and then run again.

You should then see it working:

doing get...
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en"><head><meta content

It seems as if the parent closing too soon is causing the child to not have, what I can only guess, is the proper environment available on linux and macOS.

Though, I'm not entirely sure why. I've tried digging into this as low level as I am aware of and nothing is clicking for me as to the cause. Does anyone know why this isn't possible?

What did you expect to see?

I would like the parent to be able to exit well before the child, so that I can use the golang binary as a controller for the daemon/service. But I also can't guarantee that https calls will happen before the parent exits.

@seankhliao
Copy link
Member

Sounds like you reported this before as #66709 with the conclusion that it's an Apple bug. I don't think there's much Go can do about it.

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Jul 23, 2024
@NorseGaud
Copy link
Author

NorseGaud commented Jul 23, 2024

@seankhliao, no this is entirely different. That was a path issue which has been fixed. Please reopen.

@ianlancetaylor
Copy link
Member

Reopening. The error returned by crypto/x509 in this case isn't helpful. It's peculiar that most of the code in crypto/x509/internal/macos/security.go returns an OSStatus error if the Darwin function returns non-zero, but SecPolicyCreateSSL and SecTrustGetCertificateAtIndex return an OSStatus error if the Darwin function returns zero. The result is an unhelpful error: 0. We should do better in those cases.

CC @golang/security

That said this error may just be another case of #54590.

@NorseGaud
Copy link
Author

Thanks @ianlancetaylor , I could definitely be misunderstanding something and it's possibly another case of 54590, but I thought I solved that by just calling the main.go with an absolute path. At least it went away until I started doing this daemonization stuff in the example above.

@dmitshur dmitshur added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Jul 24, 2024
@dmitshur dmitshur changed the title Get "https://google.com": tls: failed to verify certificate: SecPolicyCreateSSL error: 0 when running in child after parent exits crypto/x509: Get "https://google.com": tls: failed to verify certificate: SecPolicyCreateSSL error: 0 when running in child after parent exits Jul 24, 2024
@dmitshur dmitshur added this to the Backlog milestone Jul 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Darwin
Projects
None yet
Development

No branches or pull requests

5 participants