github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/redirector/main.go (about) 1 // Copyright 2018 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 // 5 //go:build !windows 6 // +build !windows 7 8 package main 9 10 import ( 11 "context" 12 "fmt" 13 "os" 14 "os/exec" 15 "os/signal" 16 "os/user" 17 "path/filepath" 18 "runtime" 19 "sort" 20 "strconv" 21 "strings" 22 "sync" 23 "syscall" 24 "time" 25 26 "bazil.org/fuse" 27 "bazil.org/fuse/fs" 28 "github.com/keybase/client/go/utils" 29 "github.com/keybase/gomounts" 30 ) 31 32 var kbfusePath = fuse.OSXFUSEPaths{ 33 DevicePrefix: "/dev/kbfuse", 34 Load: "/Library/Filesystems/kbfuse.fs/Contents/Resources/load_kbfuse", 35 Mount: "/Library/Filesystems/kbfuse.fs/Contents/Resources/mount_kbfuse", 36 DaemonVar: "_FUSE_DAEMON_PATH", 37 } 38 39 const ( 40 mountpointTimeout = 5 * time.Second 41 notRunningName = "KBFS_NOT_RUNNING" 42 mountAsUser = "root" 43 ) 44 45 type symlink struct { 46 link string 47 } 48 49 func (s symlink) Attr(ctx context.Context, a *fuse.Attr) (err error) { 50 a.Mode = os.ModeSymlink | a.Mode | 0555 51 a.Valid = 0 52 return nil 53 } 54 55 func (s symlink) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) ( 56 link string, err error) { 57 return s.link, nil 58 } 59 60 type cacheEntry struct { 61 mountpoint string 62 time time.Time 63 } 64 65 type root struct { 66 runmodeStr string 67 runmodeStrFancy string 68 69 lock sync.RWMutex 70 mountpointCache map[uint32]cacheEntry 71 72 getMountsLock sync.Mutex 73 74 shutdownCh chan struct{} 75 } 76 77 func newRoot() *root { 78 runmodeStr := "keybase" 79 runmodeStrFancy := "Keybase" 80 switch os.Getenv("KEYBASE_RUN_MODE") { 81 case "staging": 82 runmodeStr = "keybase.staging" 83 runmodeStrFancy = "KeybaseStaging" 84 case "devel": 85 runmodeStr = "keybase.devel" 86 runmodeStrFancy = "KeybaseDevel" 87 } 88 89 return &root{ 90 runmodeStr: runmodeStr, 91 runmodeStrFancy: runmodeStrFancy, 92 mountpointCache: make(map[uint32]cacheEntry), 93 shutdownCh: make(chan struct{}), 94 } 95 } 96 97 func (r *root) Root() (fs.Node, error) { 98 return r, nil 99 } 100 101 func (r *root) Attr(ctx context.Context, attr *fuse.Attr) error { 102 attr.Mode = os.ModeDir | 0555 103 return nil 104 } 105 106 func (r *root) getCachedMountpoint(uid uint32) string { 107 r.lock.RLock() 108 defer r.lock.RUnlock() 109 entry, ok := r.mountpointCache[uid] 110 if !ok { 111 return "" 112 } 113 now := time.Now() 114 if now.Sub(entry.time) > mountpointTimeout { 115 // Don't bother deleting the entry, since the caller should 116 // just overwrite it. 117 return "" 118 } 119 return entry.mountpoint 120 } 121 122 func (r *root) getMountedVolumes() ([]gomounts.Volume, error) { 123 r.getMountsLock.Lock() 124 defer r.getMountsLock.Unlock() 125 return gomounts.GetMountedVolumes() 126 } 127 128 // mountpointMatchesRunmode returns true if `mp` contains `runmode` at 129 // the end of a component of the path, or followed by a space. 130 func mountpointMatchesRunmode(mp, runmode string) bool { 131 i := strings.Index(mp, runmode) 132 if i < 0 { 133 return false 134 } 135 if len(mp) == i+len(runmode) || mp[i+len(runmode)] == '/' || 136 mp[i+len(runmode)] == ' ' { 137 return true 138 } 139 return false 140 } 141 142 func (r *root) findKBFSMount(ctx context.Context) ( 143 mountpoint string, err error) { 144 // Get the UID, and crash intentionally if it's not set, because 145 // that means we're not compiled against the correct version of 146 // bazil.org/fuse. 147 uid := ctx.Value(fs.CtxHeaderUIDKey).(uint32) 148 // Don't let the root see anything here; we don't want a symlink 149 // loop back to this mount. 150 if uid == 0 { 151 return "", fuse.ENOENT 152 } 153 154 mountpoint = r.getCachedMountpoint(uid) 155 if mountpoint != "" { 156 return mountpoint, nil 157 } 158 159 defer func() { 160 if err != nil { 161 return 162 } 163 // Cache the entry if we didn't hit an error. 164 r.lock.Lock() 165 defer r.lock.Unlock() 166 r.mountpointCache[uid] = cacheEntry{ 167 mountpoint: mountpoint, 168 time: time.Now(), 169 } 170 }() 171 172 u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10)) 173 if err != nil { 174 return "", err 175 } 176 177 vols, err := r.getMountedVolumes() 178 if err != nil { 179 return "", err 180 } 181 fuseType := "fuse" 182 if runtime.GOOS == "darwin" { 183 fuseType = "kbfuse" 184 } 185 var fuseMountPoints []string 186 for _, v := range vols { 187 if v.Type != fuseType { 188 continue 189 } 190 if v.Owner != u.Uid { 191 continue 192 } 193 fuseMountPoints = append(fuseMountPoints, v.Path) 194 } 195 196 if len(fuseMountPoints) == 0 { 197 return "", fuse.ENOENT 198 } 199 200 // Pick the first one alphabetically that has "keybase" in the 201 // path. 202 sort.Strings(fuseMountPoints) 203 for _, mp := range fuseMountPoints { 204 // Find mountpoints like "/home/user/.local/share/keybase/fs", 205 // or "/Volumes/Keybase (user)", and make sure it doesn't 206 // match mounts for another run mode, say 207 // "/Volumes/KeybaseStaging (user)". 208 if mountpointMatchesRunmode(mp, r.runmodeStr) || 209 mountpointMatchesRunmode(mp, r.runmodeStrFancy) { 210 return mp, nil 211 } 212 } 213 214 // Give up. 215 return "", fuse.ENOENT 216 } 217 218 func (r *root) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 219 select { 220 case <-r.shutdownCh: 221 return nil, nil 222 default: 223 } 224 225 _, err := r.findKBFSMount(ctx) 226 if err != nil { 227 if err == fuse.ENOENT { 228 // Put a symlink in the directory for someone who's not 229 // logged in, so that the directory is non-empty and 230 // future redirector calls as root won't try to mount over 231 // us. 232 return []fuse.Dirent{ 233 { 234 Type: fuse.DT_Link, 235 Name: notRunningName, 236 }, 237 }, nil 238 } 239 return []fuse.Dirent{}, err 240 } 241 242 // TODO: show the `kbfs.error.txt" and "kbfs.nologin.txt" files if 243 // they exist? As root, it is hard to figure out if they're 244 // there, though. 245 return []fuse.Dirent{ 246 { 247 Type: fuse.DT_Link, 248 Name: "private", 249 }, 250 { 251 Type: fuse.DT_Link, 252 Name: "public", 253 }, 254 { 255 Type: fuse.DT_Link, 256 Name: "team", 257 }, 258 }, nil 259 } 260 261 func (r *root) Lookup( 262 ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) ( 263 n fs.Node, err error) { 264 select { 265 case <-r.shutdownCh: 266 return nil, fuse.ENOENT 267 default: 268 } 269 270 mountpoint, err := r.findKBFSMount(ctx) 271 if err != nil { 272 if req.Name == notRunningName { 273 return symlink{"/dev/null"}, nil 274 } 275 return nil, err 276 } 277 278 resp.EntryValid = 0 279 switch req.Name { 280 case "private", "public", "team", ".kbfs_error", ".kbfs_metrics", 281 ".kbfs_profiles", ".kbfs_reset_caches", ".kbfs_status", 282 "kbfs.error.txt", "kbfs.nologin.txt", ".kbfs_enable_auto_journals", 283 ".kbfs_disable_auto_journals", ".kbfs_enable_block_prefetching", 284 ".kbfs_disable_block_prefetching", ".kbfs_enable_debug_server", 285 ".kbfs_disable_debug_server", ".kbfs_edit_history", 286 ".kbfs_open_file_count": 287 return symlink{filepath.Join(mountpoint, req.Name)}, nil 288 } 289 return nil, fuse.ENOENT 290 } 291 292 func unmount(currUID, mountAsUID uint64, dir string) { 293 if currUID != mountAsUID { 294 // Unmounting requires escalating the effective user to the 295 // mounting user. But we leave the real user ID the same. 296 err := syscall.Seteuid(int(mountAsUID)) 297 if err != nil { 298 fmt.Fprintf(os.Stderr, "Can't setuid: %+v\n", err) 299 os.Exit(1) 300 } 301 } 302 303 err := fuse.Unmount(dir) 304 if err != nil { 305 fmt.Fprintf(os.Stderr, "Couldn't unmount cleanly: %+v\n", err) 306 } 307 308 // Set it back. 309 if currUID != mountAsUID { 310 err := syscall.Seteuid(int(currUID)) 311 if err != nil { 312 fmt.Fprintf(os.Stderr, "Can't setuid: %+v\n", err) 313 os.Exit(1) 314 } 315 } 316 } 317 318 func main() { 319 if len(os.Args) != 2 { 320 fmt.Fprintf(os.Stderr, "Usage: %s <mountpoint>\n", os.Args[0]) 321 os.Exit(1) 322 } 323 324 // Restrict the mountpoint to paths starting with "/keybase". 325 // Since this is a suid binary, it is dangerous to allow arbitrary 326 // mountpoints. TODO: Read a redirector mountpoint from a 327 // root-owned config file. 328 r := newRoot() 329 if os.Args[1] != fmt.Sprintf("/%s", r.runmodeStr) && 330 os.Args[1] != fmt.Sprintf("/Volumes/%s", r.runmodeStrFancy) { 331 fmt.Fprintf(os.Stderr, "ERROR: The redirector may only mount at "+ 332 "/%s or /Volumes/%s; %s is an invalid mountpoint\n", 333 r.runmodeStr, r.runmodeStrFancy, os.Args[1]) 334 os.Exit(1) 335 } 336 337 u, err := user.Lookup(mountAsUser) 338 if err != nil { 339 fmt.Fprintf(os.Stderr, "ERROR: can't find %s user: %v\n", 340 mountAsUser, err) 341 os.Exit(1) 342 } 343 // Refuse to accept uids with high bits set for now. They could overflow 344 // int on 32-bit platforms. However the underlying C type is unsigned, so 345 // no permanent harm was done (expect perhaps for -1/0xFFFFFFFF). 346 mountAsUID, err := strconv.ParseUint(u.Uid, 10, 31) 347 if err != nil { 348 fmt.Fprintf(os.Stderr, "ERROR: can't convert %s's UID %s: %v", 349 mountAsUser, u.Uid, err) 350 os.Exit(1) 351 } 352 353 currUser, err := user.Current() 354 if err != nil { 355 fmt.Fprintf(os.Stderr, "ERROR: can't get the current user: %v", err) 356 os.Exit(1) 357 } 358 currUID, err := strconv.ParseUint(currUser.Uid, 10, 31) 359 if err != nil { 360 fmt.Fprintf(os.Stderr, "ERROR: can't convert %s's UID %s: %v", 361 currUser.Username, currUser.Uid, err) 362 os.Exit(1) 363 } 364 365 options := []fuse.MountOption{fuse.AllowOther()} 366 options = append(options, fuse.FSName("keybase-redirector")) 367 options = append(options, fuse.ReadOnly()) 368 switch runtime.GOOS { 369 case "darwin": 370 options = append(options, fuse.OSXFUSELocations(kbfusePath)) 371 options = append(options, fuse.VolumeName("keybase")) 372 options = append(options, fuse.NoBrowse()) 373 // Without NoLocalCaches(), OSX will cache symlinks for a long time. 374 options = append(options, fuse.NoLocalCaches()) 375 case "linux": 376 err := disableDumpable() 377 if err != nil { 378 fmt.Fprintf(os.Stderr, "Unable to prctl: %v", err) 379 } 380 } 381 382 // Clear the environment to harden ourselves against any 383 // unforeseen environmnent variables exposing vulnerabilities 384 // during the effective user escalation below. 385 os.Clearenv() 386 387 if currUser.Uid != u.Uid { 388 runtime.LockOSThread() 389 // Escalate privileges of the effective user to the mounting 390 // user briefly, just for the `Mount` call. Keep the real 391 // user the same throughout. 392 err := syscall.Seteuid(int(mountAsUID)) 393 if err != nil { 394 fmt.Fprintf(os.Stderr, "Can't seteuid: %+v\n", err) 395 os.Exit(1) 396 } 397 } 398 399 c, err := fuse.Mount(os.Args[1], options...) 400 if err != nil { 401 fmt.Printf("Mount error, exiting cleanly: %+v\n", err) 402 os.Exit(0) 403 } 404 405 if currUser.Uid != u.Uid { 406 runtime.LockOSThread() 407 err := syscall.Seteuid(int(currUID)) 408 if err != nil { 409 fmt.Fprintf(os.Stderr, "Can't seteuid: %+v\n", err) 410 os.Exit(1) 411 } 412 } 413 414 interruptChan := make(chan os.Signal, 1) 415 signal.Notify(interruptChan, os.Interrupt) 416 signal.Notify(interruptChan, syscall.SIGTERM) 417 go func() { 418 select { 419 case <-interruptChan: 420 case <-r.shutdownCh: 421 return 422 } 423 424 // This might be a different system thread than the main code, so 425 // we might need to setuid again. 426 runtime.LockOSThread() 427 unmount(currUID, mountAsUID, os.Args[1]) 428 }() 429 430 restartChan := make(chan os.Signal, 1) 431 signal.Notify(restartChan, syscall.SIGUSR1) 432 go func() { 433 <-restartChan 434 435 fmt.Printf("Relaunching after an upgrade\n") 436 437 // Make this mount look empty, so if we race with the new 438 // process, it will be able to mount over us. (Note that we 439 // can't unmount first, because that causes this process to 440 // exit immediately, before launching the new process.) 441 close(r.shutdownCh) 442 443 ex, err := utils.BinPath() 444 if err != nil { 445 fmt.Fprintf(os.Stderr, 446 "Couldn't get the current executable: %v", err) 447 os.Exit(1) 448 } 449 cmd := exec.Command(ex, os.Args[1]) 450 cmd.Stdout = os.Stdout 451 cmd.Stderr = os.Stderr 452 err = cmd.Start() 453 if err != nil { 454 fmt.Fprintf(os.Stderr, "Can't start upgraded copy: %+v\n", err) 455 os.Exit(1) 456 } 457 458 // This might be a different system thread than the main code, so 459 // we might need to setuid again. 460 runtime.LockOSThread() 461 unmount(currUID, mountAsUID, os.Args[1]) 462 os.Exit(0) 463 }() 464 465 srv := fs.New(c, &fs.Config{ 466 WithContext: func(ctx context.Context, _ fuse.Request) context.Context { 467 return context.Background() 468 }, 469 }) 470 _ = srv.Serve(r) 471 }