|
| 1 | +// Copyright (c) 2020, Control Command 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 | +// +build singularity_engine |
| 7 | + |
| 8 | +package unpacker |
| 9 | + |
| 10 | +import ( |
| 11 | + "bytes" |
| 12 | + "debug/elf" |
| 13 | + "fmt" |
| 14 | + "io" |
| 15 | + "io/ioutil" |
| 16 | + "os" |
| 17 | + "os/exec" |
| 18 | + "path/filepath" |
| 19 | + "regexp" |
| 20 | + "strings" |
| 21 | + |
| 22 | + "github.com/sylabs/singularity/internal/pkg/buildcfg" |
| 23 | +) |
| 24 | + |
| 25 | +func init() { |
| 26 | + cmdFunc = unsquashfsSandboxCmd |
| 27 | +} |
| 28 | + |
| 29 | +// getLibraries returns the libraries required by the elf binary, |
| 30 | +// the binary path must be absolute. |
| 31 | +func getLibraries(binary string) ([]string, error) { |
| 32 | + libs := make([]string, 0) |
| 33 | + |
| 34 | + exe, err := elf.Open(binary) |
| 35 | + if err != nil { |
| 36 | + return nil, err |
| 37 | + } |
| 38 | + defer exe.Close() |
| 39 | + |
| 40 | + interp := "" |
| 41 | + |
| 42 | + // look for the interpreter |
| 43 | + for _, p := range exe.Progs { |
| 44 | + if p.Type != elf.PT_INTERP { |
| 45 | + continue |
| 46 | + } |
| 47 | + buf := make([]byte, 4096) |
| 48 | + n, err := p.ReadAt(buf, 0) |
| 49 | + if err != nil && err != io.EOF { |
| 50 | + return nil, err |
| 51 | + } else if n > cap(buf) { |
| 52 | + return nil, fmt.Errorf("buffer too small to store interpreter") |
| 53 | + } |
| 54 | + // trim null byte to avoid an execution failure with |
| 55 | + // an invalid argument error |
| 56 | + interp = string(bytes.Trim(buf, "\x00")) |
| 57 | + } |
| 58 | + |
| 59 | + // this is a static binary, nothing to do |
| 60 | + if interp == "" { |
| 61 | + return libs, nil |
| 62 | + } |
| 63 | + |
| 64 | + // run interpreter to list library dependencies for the |
| 65 | + // corresponding binary, eg: |
| 66 | + // /lib64/ld-linux-x86-64.so.2 --list <program> |
| 67 | + // /lib/ld-musl-x86_64.so.1 --list <program> |
| 68 | + errBuf := new(bytes.Buffer) |
| 69 | + buf := new(bytes.Buffer) |
| 70 | + |
| 71 | + cmd := exec.Command(interp, "--list", binary) |
| 72 | + cmd.Stdout = buf |
| 73 | + cmd.Stderr = errBuf |
| 74 | + |
| 75 | + if err := cmd.Run(); err != nil { |
| 76 | + return nil, fmt.Errorf("while getting library dependencies: %s\n%s", err, errBuf.String()) |
| 77 | + } |
| 78 | + |
| 79 | + // parse the output to get matches for ' /an/absolute/path (' |
| 80 | + re := regexp.MustCompile(`[[:blank:]]?(\/.*)[[:blank:]]\(`) |
| 81 | + |
| 82 | + match := re.FindAllStringSubmatch(buf.String(), -1) |
| 83 | + for _, m := range match { |
| 84 | + if len(m) < 2 { |
| 85 | + continue |
| 86 | + } |
| 87 | + lib := m[1] |
| 88 | + has := false |
| 89 | + for _, l := range libs { |
| 90 | + if l == lib { |
| 91 | + has = true |
| 92 | + break |
| 93 | + } |
| 94 | + } |
| 95 | + if !has { |
| 96 | + libs = append(libs, lib) |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + return libs, nil |
| 101 | +} |
| 102 | + |
| 103 | +// unsquashfsSandboxCmd is the command instance for executing unsquashfs command |
| 104 | +// in a sandboxed environment with singularity. |
| 105 | +func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, rootless bool) (*exec.Cmd, error) { |
| 106 | + const ( |
| 107 | + // will contain both dest and filename inside the sandbox |
| 108 | + rootfsImageDir = "/image" |
| 109 | + ) |
| 110 | + |
| 111 | + // create the sandbox temporary directory |
| 112 | + tmpdir := filepath.Dir(dest) |
| 113 | + rootfs, err := ioutil.TempDir(tmpdir, "tmp-rootfs-") |
| 114 | + if err != nil { |
| 115 | + return nil, fmt.Errorf("failed to create chroot directory: %s", err) |
| 116 | + } |
| 117 | + |
| 118 | + overwrite := false |
| 119 | + |
| 120 | + // remove the destination directory if any, if the directory is |
| 121 | + // not empty (typically during image build), the unsafe option -f is |
| 122 | + // set, this is unfortunately required by image build |
| 123 | + if err := os.Remove(dest); err != nil && !os.IsNotExist(err) { |
| 124 | + if !os.IsExist(err) { |
| 125 | + return nil, fmt.Errorf("failed to remove %s: %s", dest, err) |
| 126 | + } |
| 127 | + overwrite = true |
| 128 | + } |
| 129 | + |
| 130 | + // map destination into the sandbox |
| 131 | + rootfsDest := filepath.Join(rootfsImageDir, filepath.Base(dest)) |
| 132 | + |
| 133 | + // sandbox required directories |
| 134 | + rootfsDirs := []string{ |
| 135 | + // unsquashfs get available CPU from /sys/devices/system/cpu/online |
| 136 | + filepath.Join(rootfs, "/sys"), |
| 137 | + filepath.Join(rootfs, "/dev"), |
| 138 | + filepath.Join(rootfs, rootfsImageDir), |
| 139 | + } |
| 140 | + |
| 141 | + for _, d := range rootfsDirs { |
| 142 | + if err := os.Mkdir(d, 0700); err != nil { |
| 143 | + return nil, fmt.Errorf("while creating %s: %s", d, err) |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + // the decision to use user namespace is left to singularity |
| 148 | + // which will detect automatically depending of the configuration |
| 149 | + // what workflow it could use |
| 150 | + args := []string{ |
| 151 | + "-q", |
| 152 | + "exec", |
| 153 | + "--no-home", |
| 154 | + "--no-nv", |
| 155 | + "--no-rocm", |
| 156 | + "-C", |
| 157 | + "--no-init", |
| 158 | + "--writable", |
| 159 | + "-B", fmt.Sprintf("%s:%s", tmpdir, rootfsImageDir), |
| 160 | + } |
| 161 | + |
| 162 | + if filename != stdinFile { |
| 163 | + filename = filepath.Join(rootfsImageDir, filepath.Base(filename)) |
| 164 | + } |
| 165 | + |
| 166 | + // get the library dependencies of unsquashfs |
| 167 | + libs, err := getLibraries(unsquashfs) |
| 168 | + if err != nil { |
| 169 | + return nil, err |
| 170 | + } |
| 171 | + libraryPath := make([]string, 0) |
| 172 | + |
| 173 | + roFiles := []string{ |
| 174 | + unsquashfs, |
| 175 | + } |
| 176 | + |
| 177 | + // add libraries for bind mount and also generate |
| 178 | + // LD_LIBRARY_PATH |
| 179 | + for _, l := range libs { |
| 180 | + dir := filepath.Dir(l) |
| 181 | + roFiles = append(roFiles, l) |
| 182 | + has := false |
| 183 | + for _, lp := range libraryPath { |
| 184 | + if lp == dir { |
| 185 | + has = true |
| 186 | + break |
| 187 | + } |
| 188 | + } |
| 189 | + if !has { |
| 190 | + libraryPath = append(libraryPath, dir) |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + // create files and directories in the sandbox and |
| 195 | + // add singularity bind mount options |
| 196 | + for _, b := range roFiles { |
| 197 | + file := filepath.Join(rootfs, b) |
| 198 | + dir := filepath.Dir(file) |
| 199 | + if err := os.MkdirAll(dir, 0700); err != nil { |
| 200 | + return nil, fmt.Errorf("while creating %s: %s", dir, err) |
| 201 | + } |
| 202 | + if err := ioutil.WriteFile(file, []byte(""), 0600); err != nil { |
| 203 | + return nil, fmt.Errorf("while creating %s: %s", file, err) |
| 204 | + } |
| 205 | + args = append(args, "-B", fmt.Sprintf("%s:%s:ro", b, b)) |
| 206 | + } |
| 207 | + |
| 208 | + // singularity sandbox |
| 209 | + args = append(args, rootfs) |
| 210 | + |
| 211 | + // unsquashfs execution arguments |
| 212 | + args = append(args, unsquashfs) |
| 213 | + if rootless { |
| 214 | + args = append(args, "-user-xattrs") |
| 215 | + } |
| 216 | + if overwrite { |
| 217 | + args = append(args, "-f") |
| 218 | + } |
| 219 | + args = append(args, "-d", rootfsDest, filename) |
| 220 | + |
| 221 | + cmd := exec.Command(filepath.Join(buildcfg.BINDIR, "singularity"), args...) |
| 222 | + cmd.Dir = "/" |
| 223 | + cmd.Env = []string{ |
| 224 | + fmt.Sprintf("LD_LIBRARY_PATH=%s", strings.Join(libraryPath, string(os.PathListSeparator))), |
| 225 | + } |
| 226 | + |
| 227 | + return cmd, nil |
| 228 | +} |
0 commit comments