internal/command/command_linux.go (120 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build linux package command import ( "context" "fmt" "net" "os" "os/user" "path/filepath" "runtime" "strconv" "syscall" "github.com/GoogleCloudPlatform/galog" "github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg" "github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file" ) // PipeName returns the pipe for a given listener. Its basically a full pipe // path as [cfg.Unstable.CommandPipePath] being the directory and id name. func PipeName(listener KnownListeners) string { base := cfg.Retrieve().Unstable.CommandPipePath return filepath.Join(base, listener.String()) } func mkdirpWithPerms(dir string, p os.FileMode, uid, gid int) error { stat, err := os.Stat(dir) parent := filepath.Dir(dir) if err == nil { if parent != "/" && parent != "" { statT, ok := stat.Sys().(*syscall.Stat_t) if !ok { return fmt.Errorf("could not determine owner of %s", dir) } if !stat.IsDir() { return fmt.Errorf("%s exists and is not a directory", dir) } if morePermissive(int(stat.Mode()), int(p)) { if err := os.Chmod(dir, p); err != nil { return fmt.Errorf("could not correct %s permissions to %d: %v", dir, p, err) } } if statT.Uid != 0 && statT.Uid != uint32(uid) { if err := os.Chown(dir, uid, -1); err != nil { return fmt.Errorf("could not correct %s owner to %d: %v", dir, uid, err) } } if statT.Gid != 0 && statT.Gid != uint32(gid) { if err := os.Chown(dir, -1, gid); err != nil { return fmt.Errorf("could not correct %s group to %d: %v", dir, gid, err) } } } } else { if parent != "/" && parent != "" { if err := mkdirpWithPerms(parent, p, uid, gid); err != nil { return err } } if err := os.Mkdir(dir, p); err != nil { return err } } return nil } func morePermissive(i, j int) bool { return i&^j != 0 } func listen(ctx context.Context, pipe string, filemode int, grp string) (net.Listener, error) { if file.Exists(pipe, file.TypeFile) { // Unix sockets must be unlinked `listener.Close()` before it can be reused // again. If file already exist bind can fail. In case process crashes or // was being killed cleanup might not happen leaving behind the socket file. galog.Debugf("Command pipe %q already exists, cleaning up", pipe) if err := os.Remove(pipe); err != nil { return nil, fmt.Errorf("could not remove command pipe %q: %w", pipe, err) } } // If grp is an int, use it as a GID. gid, err := strconv.Atoi(grp) if err != nil { // Otherwise lookup GID. group, err := user.LookupGroup(grp) if err != nil { galog.Warnf("guest agent command pipe group %s is not a GID nor a valid group, not changing socket ownership", grp) gid = -1 } else { gid, err = strconv.Atoi(group.Gid) if err != nil { galog.Warnf("os reported group %s has gid %s which is not a valid int, not changing socket ownership. this should never happen", grp, group.Gid) gid = -1 } } } // Socket owner group does not need to have permissions to everything in the // directory containing it, whatever user and group we are should own that. user, err := user.Current() if err != nil { return nil, fmt.Errorf("could not lookup current user") } currentuid, err := strconv.Atoi(user.Uid) if err != nil { return nil, fmt.Errorf("os reported user %s has uid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Uid) } currentgid, err := strconv.Atoi(user.Gid) if err != nil { return nil, fmt.Errorf("os reported user %s has gid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Gid) } if err := mkdirpWithPerms(filepath.Dir(pipe), os.FileMode(filemode), currentuid, currentgid); err != nil { return nil, err } // Mutating the umask of the process for this is not ideal, but tightening // permissions with chown after creation is not really secure. // Lock OS thread while mutating umask so we don't lose a thread with a // mutated mask. runtime.LockOSThread() oldmask := syscall.Umask(777 - filemode) var lc net.ListenConfig commandListener, err := lc.Listen(ctx, "unix", pipe) syscall.Umask(oldmask) runtime.UnlockOSThread() if err != nil { return nil, fmt.Errorf("could not start listener on %q: %w", pipe, err) } // But we need to chown anyway to loosen permissions to include whatever group // the user has configured. err = os.Chown(pipe, int(currentuid), gid) if err != nil { if err := commandListener.Close(); err != nil { galog.Errorf("error closing socket listener after failing to set ownership: %v", err) } return nil, err } return commandListener, nil } func dialPipe(ctx context.Context, pipe string) (net.Conn, error) { var dialer net.Dialer return dialer.DialContext(ctx, "unix", pipe) }