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 }