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  }