github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/userns/userns_maps_linux.go (about) 1 //go:build linux 2 3 package userns 4 5 import ( 6 "bufio" 7 "bytes" 8 "fmt" 9 "io" 10 "os" 11 "unsafe" 12 13 "github.com/opencontainers/runc/libcontainer/configs" 14 "github.com/sirupsen/logrus" 15 ) 16 17 /* 18 #include <stdlib.h> 19 extern int spawn_userns_cat(char *userns_path, char *path, int outfd, int errfd); 20 */ 21 import "C" 22 23 func parseIdmapData(data []byte) (ms []configs.IDMap, err error) { 24 scanner := bufio.NewScanner(bytes.NewReader(data)) 25 for scanner.Scan() { 26 var m configs.IDMap 27 line := scanner.Text() 28 if _, err := fmt.Sscanf(line, "%d %d %d", &m.ContainerID, &m.HostID, &m.Size); err != nil { 29 return nil, fmt.Errorf("parsing id map failed: invalid format in line %q: %w", line, err) 30 } 31 ms = append(ms, m) 32 } 33 if err := scanner.Err(); err != nil { 34 return nil, fmt.Errorf("parsing id map failed: %w", err) 35 } 36 return ms, nil 37 } 38 39 // Do something equivalent to nsenter --user=<nsPath> cat <path>, but more 40 // efficiently. Returns the contents of the requested file from within the user 41 // namespace. 42 func spawnUserNamespaceCat(nsPath string, path string) ([]byte, error) { 43 rdr, wtr, err := os.Pipe() 44 if err != nil { 45 return nil, fmt.Errorf("create pipe for userns spawn failed: %w", err) 46 } 47 defer rdr.Close() 48 defer wtr.Close() 49 50 errRdr, errWtr, err := os.Pipe() 51 if err != nil { 52 return nil, fmt.Errorf("create error pipe for userns spawn failed: %w", err) 53 } 54 defer errRdr.Close() 55 defer errWtr.Close() 56 57 cNsPath := C.CString(nsPath) 58 defer C.free(unsafe.Pointer(cNsPath)) 59 cPath := C.CString(path) 60 defer C.free(unsafe.Pointer(cPath)) 61 62 childPid := C.spawn_userns_cat(cNsPath, cPath, C.int(wtr.Fd()), C.int(errWtr.Fd())) 63 64 if childPid < 0 { 65 return nil, fmt.Errorf("failed to spawn fork for userns") 66 } else if childPid == 0 { 67 // this should never happen 68 panic("runc executing inside fork child -- unsafe state!") 69 } 70 71 // We are in the parent -- close the write end of the pipe before reading. 72 wtr.Close() 73 output, err := io.ReadAll(rdr) 74 rdr.Close() 75 if err != nil { 76 return nil, fmt.Errorf("reading from userns spawn failed: %w", err) 77 } 78 79 // Ditto for the error pipe. 80 errWtr.Close() 81 errOutput, err := io.ReadAll(errRdr) 82 errRdr.Close() 83 if err != nil { 84 return nil, fmt.Errorf("reading from userns spawn error pipe failed: %w", err) 85 } 86 errOutput = bytes.TrimSpace(errOutput) 87 88 // Clean up the child. 89 child, err := os.FindProcess(int(childPid)) 90 if err != nil { 91 return nil, fmt.Errorf("could not find userns spawn process: %w", err) 92 } 93 state, err := child.Wait() 94 if err != nil { 95 return nil, fmt.Errorf("failed to wait for userns spawn process: %w", err) 96 } 97 if !state.Success() { 98 errStr := string(errOutput) 99 if errStr == "" { 100 errStr = fmt.Sprintf("unknown error (status code %d)", state.ExitCode()) 101 } 102 return nil, fmt.Errorf("userns spawn: %s", errStr) 103 } else if len(errOutput) > 0 { 104 // We can just ignore weird output in the error pipe if the process 105 // didn't bail(), but for completeness output for debugging. 106 logrus.Debugf("userns spawn succeeded but unexpected error message found: %s", string(errOutput)) 107 } 108 // The subprocess succeeded, return whatever it wrote to the pipe. 109 return output, nil 110 } 111 112 func GetUserNamespaceMappings(nsPath string) (uidMap, gidMap []configs.IDMap, err error) { 113 var ( 114 pid int 115 extra rune 116 tryFastPath bool 117 ) 118 119 // nsPath is usually of the form /proc/<pid>/ns/user, which means that we 120 // already have a pid that is part of the user namespace and thus we can 121 // just use the pid to read from /proc/<pid>/*id_map. 122 // 123 // Note that Sscanf doesn't consume the whole input, so we check for any 124 // trailing data with %c. That way, we can be sure the pattern matched 125 // /proc/$pid/ns/user _exactly_ iff n === 1. 126 if n, _ := fmt.Sscanf(nsPath, "/proc/%d/ns/user%c", &pid, &extra); n == 1 { 127 tryFastPath = pid > 0 128 } 129 130 for _, mapType := range []struct { 131 name string 132 idMap *[]configs.IDMap 133 }{ 134 {"uid_map", &uidMap}, 135 {"gid_map", &gidMap}, 136 } { 137 var mapData []byte 138 139 if tryFastPath { 140 path := fmt.Sprintf("/proc/%d/%s", pid, mapType.name) 141 data, err := os.ReadFile(path) 142 if err != nil { 143 // Do not error out here -- we need to try the slow path if the 144 // fast path failed. 145 logrus.Debugf("failed to use fast path to read %s from userns %s (error: %s), falling back to slow userns-join path", mapType.name, nsPath, err) 146 } else { 147 mapData = data 148 } 149 } else { 150 logrus.Debugf("cannot use fast path to read %s from userns %s, falling back to slow userns-join path", mapType.name, nsPath) 151 } 152 153 if mapData == nil { 154 // We have to actually join the namespace if we cannot take the 155 // fast path. The path is resolved with respect to the child 156 // process, so just use /proc/self. 157 data, err := spawnUserNamespaceCat(nsPath, "/proc/self/"+mapType.name) 158 if err != nil { 159 return nil, nil, err 160 } 161 mapData = data 162 } 163 idMap, err := parseIdmapData(mapData) 164 if err != nil { 165 return nil, nil, fmt.Errorf("failed to parse %s of userns %s: %w", mapType.name, nsPath, err) 166 } 167 *mapType.idMap = idMap 168 } 169 170 return uidMap, gidMap, nil 171 } 172 173 // IsSameMapping returns whether or not the two id mappings are the same. Note 174 // that if the order of the mappings is different, or a mapping has been split, 175 // the mappings will be considered different. 176 func IsSameMapping(a, b []configs.IDMap) bool { 177 if len(a) != len(b) { 178 return false 179 } 180 for idx := range a { 181 if a[idx] != b[idx] { 182 return false 183 } 184 } 185 return true 186 }