github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/instance/instance.go (about)

     1  // Copyright (c) 2018, Sylabs Inc. All rights reserved.
     2  // This software is licensed under a 3-clause BSD license. Please consult the
     3  // LICENSE.md file distributed with the sources of this project regarding your
     4  // rights to use or distribute this software.
     5  
     6  package instance
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  	"syscall"
    17  
    18  	"github.com/sylabs/singularity/internal/pkg/sylog"
    19  
    20  	specs "github.com/opencontainers/runtime-spec/specs-go"
    21  
    22  	"github.com/sylabs/singularity/internal/pkg/util/user"
    23  	"github.com/sylabs/singularity/pkg/util/fs/proc"
    24  )
    25  
    26  const (
    27  	// OciSubDir represents directory where OCI instance files are stored
    28  	OciSubDir = "oci"
    29  	// SingSubDir represents directory where Singularity instance files are stored
    30  	SingSubDir = "sing"
    31  )
    32  
    33  const (
    34  	privPath        = "/var/run/singularity/instances"
    35  	unprivPath      = ".singularity/instances"
    36  	authorizedChars = `^[a-zA-Z0-9._-]+$`
    37  	prognameFormat  = "Singularity instance: %s [%s]"
    38  )
    39  
    40  var nsMap = map[specs.LinuxNamespaceType]string{
    41  	specs.PIDNamespace:     "pid",
    42  	specs.UTSNamespace:     "uts",
    43  	specs.IPCNamespace:     "ipc",
    44  	specs.MountNamespace:   "mnt",
    45  	specs.CgroupNamespace:  "cgroup",
    46  	specs.NetworkNamespace: "net",
    47  	specs.UserNamespace:    "user",
    48  }
    49  
    50  // File represents an instance file storing instance information
    51  type File struct {
    52  	Path       string `json:"-"`
    53  	Pid        int    `json:"pid"`
    54  	PPid       int    `json:"ppid"`
    55  	Name       string `json:"name"`
    56  	User       string `json:"user"`
    57  	Image      string `json:"image"`
    58  	Privileged bool   `json:"privileged"`
    59  	Config     []byte `json:"config"`
    60  }
    61  
    62  // ProcName returns processus name based on instance name
    63  // and username
    64  func ProcName(name string, username string) string {
    65  	return fmt.Sprintf(prognameFormat, username, name)
    66  }
    67  
    68  // ExtractName extracts instance name from an instance:// URI
    69  func ExtractName(name string) string {
    70  	return strings.Replace(name, "instance://", "", 1)
    71  }
    72  
    73  // CheckName checks if name is a valid instance name
    74  func CheckName(name string) error {
    75  	r := regexp.MustCompile(authorizedChars)
    76  	if !r.MatchString(name) {
    77  		return fmt.Errorf("%s is not a valid instance name", name)
    78  	}
    79  	return nil
    80  }
    81  
    82  // getPath returns the path where searching for instance files
    83  func getPath(privileged bool, username string, subDir string) (string, error) {
    84  	path := ""
    85  	var pw *user.User
    86  	var err error
    87  
    88  	if username == "" {
    89  		if pw, err = user.GetPwUID(uint32(os.Getuid())); err != nil {
    90  			return path, err
    91  		}
    92  	} else {
    93  		if pw, err = user.GetPwNam(username); err != nil {
    94  			return path, err
    95  		}
    96  	}
    97  
    98  	if privileged {
    99  		path = filepath.Join(privPath, subDir, pw.Name)
   100  		return path, nil
   101  	}
   102  
   103  	containerID, hostID, err := proc.ReadIDMap("/proc/self/uid_map")
   104  	if containerID == 0 && containerID != hostID {
   105  		if pw, err = user.GetPwUID(hostID); err != nil {
   106  			return path, err
   107  		}
   108  	}
   109  
   110  	hostname, err := os.Hostname()
   111  	if err != nil {
   112  		return path, err
   113  	}
   114  
   115  	path = filepath.Join(pw.Dir, unprivPath, subDir, hostname, pw.Name)
   116  	return path, nil
   117  }
   118  
   119  func getDir(privileged bool, name string, subDir string) (string, error) {
   120  	if err := CheckName(name); err != nil {
   121  		return "", err
   122  	}
   123  	path, err := getPath(privileged, "", subDir)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  	return filepath.Join(path, name), nil
   128  }
   129  
   130  // GetDirPrivileged returns directory where instances file will be stored
   131  // if instance is run with privileges
   132  func GetDirPrivileged(name string, subDir string) (string, error) {
   133  	return getDir(true, name, subDir)
   134  }
   135  
   136  // GetDirUnprivileged returns directory where instances file will be stored
   137  // if instance is run without privileges
   138  func GetDirUnprivileged(name string, subDir string) (string, error) {
   139  	return getDir(false, name, subDir)
   140  }
   141  
   142  // Get returns the instance file corresponding to instance name
   143  func Get(name string, subDir string) (*File, error) {
   144  	if err := CheckName(name); err != nil {
   145  		return nil, err
   146  	}
   147  	list, err := List("", name, subDir)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	if len(list) != 1 {
   152  		return nil, fmt.Errorf("no instance found with name %s", name)
   153  	}
   154  	return list[0], nil
   155  }
   156  
   157  // Add creates an instance file for a named instance in a privileged
   158  // or unprivileged path
   159  func Add(name string, privileged bool, subDir string) (*File, error) {
   160  	if err := CheckName(name); err != nil {
   161  		return nil, err
   162  	}
   163  	_, err := Get(name, subDir)
   164  	if err == nil {
   165  		return nil, fmt.Errorf("instance %s already exists", name)
   166  	}
   167  	i := &File{Name: name, Privileged: privileged}
   168  	i.Path, err = getPath(privileged, "", subDir)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	jsonFile := name + ".json"
   173  	i.Path = filepath.Join(i.Path, name, jsonFile)
   174  	return i, nil
   175  }
   176  
   177  // List returns instance files matching username and/or name pattern
   178  func List(username string, name string, subDir string) ([]*File, error) {
   179  	list := make([]*File, 0)
   180  	privileged := true
   181  
   182  	for {
   183  		path, err := getPath(privileged, username, subDir)
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  		pattern := filepath.Join(path, name, name+".json")
   188  		files, err := filepath.Glob(pattern)
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  		for _, file := range files {
   193  			r, err := os.Open(file)
   194  			if os.IsNotExist(err) {
   195  				continue
   196  			}
   197  			if err != nil {
   198  				return nil, err
   199  			}
   200  			b, err := ioutil.ReadAll(r)
   201  			r.Close()
   202  			if err != nil {
   203  				return nil, err
   204  			}
   205  			f := &File{Path: file}
   206  			if err := json.Unmarshal(b, f); err != nil {
   207  				return nil, err
   208  			}
   209  			list = append(list, f)
   210  		}
   211  		privileged = !privileged
   212  		if privileged {
   213  			break
   214  		}
   215  	}
   216  
   217  	return list, nil
   218  }
   219  
   220  // PrivilegedPath returns if instance file is stored in privileged path or not
   221  func (i *File) PrivilegedPath() bool {
   222  	return strings.HasPrefix(i.Path, privPath)
   223  }
   224  
   225  // Delete deletes instance file
   226  func (i *File) Delete() error {
   227  	path := filepath.Dir(i.Path)
   228  
   229  	nspath := filepath.Join(path, "ns")
   230  	if _, err := os.Stat(nspath); err == nil {
   231  		if err := syscall.Unmount(nspath, syscall.MNT_DETACH); err != nil {
   232  			sylog.Errorf("can't umount %s: %s", nspath, err)
   233  		}
   234  	}
   235  
   236  	return os.RemoveAll(path)
   237  }
   238  
   239  // Update stores instance information in associated instance file
   240  func (i *File) Update() error {
   241  	b, err := json.Marshal(i)
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	path := filepath.Dir(i.Path)
   247  
   248  	oldumask := syscall.Umask(0)
   249  	defer syscall.Umask(oldumask)
   250  
   251  	if err := os.MkdirAll(path, 0755); err != nil {
   252  		return err
   253  	}
   254  	if i.PrivilegedPath() {
   255  		pw, err := user.GetPwNam(i.User)
   256  		if err != nil {
   257  			return err
   258  		}
   259  		if err := os.Chmod(path, 0550); err != nil {
   260  			return err
   261  		}
   262  		if err := os.Chown(path, int(pw.UID), 0); err != nil {
   263  			return err
   264  		}
   265  	}
   266  	file, err := os.OpenFile(i.Path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	defer file.Close()
   271  
   272  	b = append(b, '\n')
   273  	if n, err := file.Write(b); err != nil || n != len(b) {
   274  		return fmt.Errorf("failed to write instance file %s: %s", i.Path, err)
   275  	}
   276  
   277  	return file.Sync()
   278  }
   279  
   280  // MountNamespaces binds /proc/<pid>/ns directory into instance folder
   281  func (i *File) MountNamespaces() error {
   282  	path := filepath.Join(filepath.Dir(i.Path), "ns")
   283  
   284  	oldumask := syscall.Umask(0)
   285  	defer syscall.Umask(oldumask)
   286  
   287  	if err := os.Mkdir(path, 0755); err != nil {
   288  		return err
   289  	}
   290  
   291  	nspath, err := filepath.EvalSymlinks(path)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	src := fmt.Sprintf("/proc/%d/ns", i.Pid)
   297  	if err := syscall.Mount(src, nspath, "", syscall.MS_BIND, ""); err != nil {
   298  		return fmt.Errorf("mounting %s in instance folder failed: %s", src, err)
   299  	}
   300  
   301  	return nil
   302  }
   303  
   304  // UpdateNamespacesPath updates namespaces path for the provided configuration
   305  func (i *File) UpdateNamespacesPath(configNs []specs.LinuxNamespace) error {
   306  	path := filepath.Join(filepath.Dir(i.Path), "ns")
   307  	nspath, err := filepath.EvalSymlinks(path)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	nsBase := filepath.Join(fmt.Sprintf("/proc/%d/root", i.PPid), nspath)
   312  
   313  	procPath := fmt.Sprintf("/proc/%d/cmdline", i.PPid)
   314  
   315  	if i.PrivilegedPath() {
   316  		var st syscall.Stat_t
   317  
   318  		if err := syscall.Stat(procPath, &st); err != nil {
   319  			return err
   320  		}
   321  		if st.Uid != 0 || st.Gid != 0 {
   322  			return fmt.Errorf("not an instance process")
   323  		}
   324  
   325  		uid := os.Geteuid()
   326  		taskPath := fmt.Sprintf("/proc/%d/task", i.PPid)
   327  		if err := syscall.Stat(taskPath, &st); err != nil {
   328  			return err
   329  		}
   330  		if int(st.Uid) != uid {
   331  			return fmt.Errorf("you do not own the instance")
   332  		}
   333  	}
   334  
   335  	data, err := ioutil.ReadFile(procPath)
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	cmdline := string(data[:len(data)-1])
   341  	procName := ProcName(i.Name, i.User)
   342  	if cmdline != procName {
   343  		return fmt.Errorf("no command line match found")
   344  	}
   345  
   346  	for i, n := range configNs {
   347  		ns, ok := nsMap[n.Type]
   348  		if !ok {
   349  			configNs[i].Path = ""
   350  			continue
   351  		}
   352  		if n.Path != "" {
   353  			configNs[i].Path = filepath.Join(nsBase, ns)
   354  		}
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  // SetLogFile replaces stdout/stderr streams and redirect content
   361  // to log file
   362  func SetLogFile(name string, uid int, subDir string) (*os.File, *os.File, error) {
   363  	path, err := getPath(false, "", subDir)
   364  	if err != nil {
   365  		return nil, nil, err
   366  	}
   367  	stderrPath := filepath.Join(path, name+".err")
   368  	stdoutPath := filepath.Join(path, name+".out")
   369  
   370  	oldumask := syscall.Umask(0)
   371  	defer syscall.Umask(oldumask)
   372  
   373  	if err := os.MkdirAll(filepath.Dir(stderrPath), 0755); err != nil {
   374  		return nil, nil, err
   375  	}
   376  	if err := os.MkdirAll(filepath.Dir(stdoutPath), 0755); err != nil {
   377  		return nil, nil, err
   378  	}
   379  
   380  	stderr, err := os.OpenFile(stderrPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
   381  	if err != nil {
   382  		return nil, nil, err
   383  	}
   384  
   385  	stdout, err := os.OpenFile(stdoutPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
   386  	if err != nil {
   387  		return nil, nil, err
   388  	}
   389  
   390  	if uid != os.Getuid() || uid == 0 {
   391  		if err := stderr.Chown(uid, os.Getgid()); err != nil {
   392  			return nil, nil, err
   393  		}
   394  		if err := stdout.Chown(uid, os.Getgid()); err != nil {
   395  			return nil, nil, err
   396  		}
   397  	}
   398  
   399  	return stdout, stderr, nil
   400  }