Go: os/exec: $HOME not updated when running command as a different UID

Created on 22 Aug 2019  Â·  7Comments  Â·  Source: golang/go

What version of Go are you using (go version)?

$ go version
go version go1.12.5 linux/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

go env Output

$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/wanrui/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/wanrui/workspace"
GOPROXY="https://goproxy.cn"
GORACE=""
GOROOT="/home/wanrui/go"
GOTMPDIR=""
GOTOOLDIR="/home/wanrui/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build173649310=/tmp/go-build -gno-record-gcc-switches"

What did you do?

there is testfile asynccmd .go

package main

import (
        "bufio"
        "context"
        "flag"
        "fmt"
        _ "net/http/pprof"
        "os/exec"
        "os/user"
        _ "runtime/pprof"
        "strconv"
        "syscall"
        "time"
)

const omg = `ls -al /tmp/ && nohup sleep 1000 &`

func main() {
        timeout := flag.Int("timeout", 120, "")
        command := flag.String("cmd", omg, "")
        runUser := flag.String("user", "www", "")
        pgid := flag.Bool("pgid", true, "Setpgid 开关。")

        flag.Parse()

        ctx, cancle := context.WithTimeout(context.Background(), time.Second*time.Duration(*timeout))
        defer cancle()
        cmd := exec.CommandContext(ctx, "/bin/bash", "-c", *command)

        user, _ := user.Lookup(*runUser)
        uid, _ := strconv.Atoi(user.Uid)
        gid, _ := strconv.Atoi(user.Gid)
        sysProcAttr := &syscall.SysProcAttr{
                Setpgid: *pgid,
                Credential: &syscall.Credential{
                        Uid: uint32(uid),
                        Gid: uint32(gid),
                },
        }
        cmd.SysProcAttr = sysProcAttr

        stdout, _ := cmd.StdoutPipe()
        stderr, _ := cmd.StderrPipe()
        if err := cmd.Start(); err != nil {
                fmt.Println("cmd.start occur error", err)
                return
        }

        go func() {
                b := bufio.NewReader(stdout)
                for {
                        buf, err := b.ReadBytes('\n')
                        if err != nil {
                                fmt.Println("read stdout err:", string(buf), err)
                                return
                        }
                        fmt.Println("read stdout buff:", string(buf))
                }

        }()

        go func() {
                b := bufio.NewReader(stderr)
                for {
                        buf, err := b.ReadBytes('\n')
                        if err != nil {
                                fmt.Println("read stderr err:", string(buf), err)
                                return
                        }
                        fmt.Println("read stderr buff:", string(buf))
                }

        }()

        if err := cmd.Wait(); err != nil {
                syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
                fmt.Println("cmd.wait occur error ", err)
        }

}

there is file : /home/www/.bash_profile

# .bash_profile
# Get the aliases and functions
echo $HOME
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi
export PATH

in /root dir run follow command:

./asynccmd -user="www" -cmd=" source /home/www/.bash_profile"

What did you expect to see?

read stdout buff: /home/www

read stdout buff: 

read stdout err: java: read |0: file already closed
read stderr err:  read |0: file already closed

What did you see instead?

out is:

read stdout buff: /root

read stdout buff: 

read stdout err: java: read |0: file already closed
read stderr err:  read |0: file already closed
Documentation NeedsInvestigation

Most helpful comment

I don't think os/exec should be in the business of adjusting the environment too much. We do a bit on Windows to make sure there's a minimally functional environment for loading DLLs, but I don't think we should do much more than that.

All 7 comments

some properties doesn't set?

Per http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03 (emphasis mine):

The system shall initialize this variable at the time of login to be a pathname of the user's home directory.


os/exec is not a login operation, so it doesn't update $HOME for you. I'm not sure whether it updates USER, either.

You can set both explicitly for the command using something like:

cmd.Env = append(os.Environ(), "USER="+user.Username, "HOME="+user.HomeDir)

We probably need clearer documentation about it either way.

CC @bradfitz @kevinburke @ianlancetaylor; see previously #26463.

I don't think os/exec should be in the business of adjusting the environment too much. We do a bit on Windows to make sure there's a minimally functional environment for loading DLLs, but I don't think we should do much more than that.

we hope it can simulate working of shell, as we specified the user. like subprocess in python, it works well.

when Current User is root run shell as another User like flow

sudo -u www  sh -c "source /home/www/.bash_profile  whereis java"

output like

/home/www

as @bcmills says we can code
like follow

        fmt.Println("current:cmd.env", cmd.Env)
        fmt.Println("current:os.Environ", os.Environ())
        cmd.Env = append(os.Environ(), "USER="+user.Username, "HOME="+user.HomeDir)
        sysProcAttr := &syscall.SysProcAttr{
                Setpgid: *pgid,
                Credential: &syscall.Credential{
                        Uid: uint32(uid),
                        Gid: uint32(gid),
                },
        }

os.Environ() is current User root 's ENV , May be cmd.Env should set like follow:

cmd.Env = append(cmd.Env, "USER="+user.Username, "HOME="+user.HomeDir)

I think the most we'll do here is add a small bit of documentation to SysProcAttr or syscall.Credential to say that advanced users are on their own in this regard and we don't do this automatically. Even setting USER and HOME may not be sufficient for all cases. It might be both too much and not enough magic. Better to stay out of it and let advanced users choose exactly what happens without surprises.

Was this page helpful?
0 / 5 - 0 ratings