protondrive: stop at a folder the server reports as its own ancestor
Fetching a file or folder fetches its folders up to one already known, and a server reporting folders as each other's parents made that recurse until rclone crashed. A cycle, or folders nested more than 2048 deep, is now an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52a44e41ce
commit
8666b66038
4 files changed
+97
-5
No files matched your search
@@ -35,6 +35,12 @@ const (
|
||||
// metadataBatchSize is the most node IDs one metadata request names.
|
||||
metadataBatchSize = 100
|
||||
|
||||
// maxFolderDepth is the most ancestors fetched for a node: 2048
|
||||
// levels of one-letter names fill Linux's 4096-byte PATH_MAX, so no
|
||||
// usable path is deeper, and Proton refuses deep nesting itself
|
||||
// (code 200301).
|
||||
maxFolderDepth = 2048
|
||||
|
||||
// metadataConcurrency is the most metadata requests in flight for
|
||||
// one listing.
|
||||
metadataConcurrency = 15
|
||||
@@ -91,7 +97,8 @@ type client struct {
|
||||
// before it is sent; for tests.
|
||||
verifierHook func(token []byte)
|
||||
|
||||
maxFileBlocks int // the most blocks a file may list: maxFileBlocks, unless a test lowers it
|
||||
maxFileBlocks int // the most blocks a file may list: maxFileBlocks, unless a test lowers it
|
||||
maxFolderDepth int // the most ancestors fetched for a node: maxFolderDepth, unless a test lowers it
|
||||
|
||||
mu sync.Mutex
|
||||
folderKeys map[string]*nodeKeys // by folder ID; kept whatever the cache, as node keys never change
|
||||
@@ -183,6 +190,7 @@ func newClient(ctx context.Context, s *session, opt clientOptions) (*client, err
|
||||
folderKeys: make(map[string]*nodeKeys),
|
||||
reported: make(map[string]bool),
|
||||
maxFileBlocks: maxFileBlocks,
|
||||
maxFolderDepth: maxFolderDepth,
|
||||
}
|
||||
if err := c.bootstrap(ctx); err != nil {
|
||||
c.close()
|
||||
@@ -610,13 +618,19 @@ func (c *client) rememberKeys(n *node) {
|
||||
|
||||
// folderKeysOf returns the node keys of folder id.
|
||||
func (c *client) folderKeysOf(ctx context.Context, id string) (*nodeKeys, error) {
|
||||
return c.folderKeysIn(ctx, id, nil)
|
||||
}
|
||||
|
||||
// folderKeysIn is folderKeysOf for the parent of the nodes below,
|
||||
// nearest first, which are being fetched.
|
||||
func (c *client) folderKeysIn(ctx context.Context, id string, below []string) (*nodeKeys, error) {
|
||||
c.mu.Lock()
|
||||
keys := c.folderKeys[id]
|
||||
c.mu.Unlock()
|
||||
if keys != nil {
|
||||
return keys, nil
|
||||
}
|
||||
n, err := c.getNode(ctx, id)
|
||||
n, err := c.getNodeIn(ctx, id, below)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -640,18 +654,38 @@ func (c *client) fetchLinks(ctx context.Context, ids []string) ([]api.LinkDetail
|
||||
// getNode returns the node id, of any state, wrapping errNotFound if
|
||||
// it doesn't exist.
|
||||
func (c *client) getNode(ctx context.Context, id string) (*node, error) {
|
||||
return c.getNodeIn(ctx, id, nil)
|
||||
}
|
||||
|
||||
// getNodeIn is getNode for the parent of the nodes below, nearest
|
||||
// first, which are being fetched.
|
||||
func (c *client) getNodeIn(ctx context.Context, id string, below []string) (*node, error) {
|
||||
if id == c.root.ID {
|
||||
return c.root, nil
|
||||
}
|
||||
if n := c.cache.get(id); n != nil {
|
||||
return n, nil
|
||||
}
|
||||
return c.fetchNode(ctx, id)
|
||||
return c.fetchNodeIn(ctx, id, below)
|
||||
}
|
||||
|
||||
// fetchNode reads node id, of any state, from the server whatever the
|
||||
// cache holds, and caches it. It wraps errNotFound if it doesn't exist.
|
||||
func (c *client) fetchNode(ctx context.Context, id string) (*node, error) {
|
||||
return c.fetchNodeIn(ctx, id, nil)
|
||||
}
|
||||
|
||||
// fetchNodeIn is fetchNode for the parent of the nodes below, nearest
|
||||
// first, which are being fetched, as the parents of a node not yet in
|
||||
// the cache are fetched up to one that is. A server reporting a cycle
|
||||
// or a chain deeper than maxFolderDepth gets an error.
|
||||
func (c *client) fetchNodeIn(ctx context.Context, id string, below []string) (*node, error) {
|
||||
switch {
|
||||
case slices.Contains(below, id):
|
||||
return nil, fmt.Errorf("get node %s: %w: the server reports it as its own ancestor", id, errIntegrity)
|
||||
case len(below) >= c.maxFolderDepth:
|
||||
return nil, fmt.Errorf("get node %s: %w: folders nested more than %d deep", id, errIntegrity, c.maxFolderDepth)
|
||||
}
|
||||
links, err := c.fetchLinks(ctx, []string{id})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get node %s: %w", id, err)
|
||||
@@ -664,7 +698,7 @@ func (c *client) fetchNode(ctx context.Context, id string) (*node, error) {
|
||||
if d.Link.ParentLinkID == "" {
|
||||
return nil, fmt.Errorf("get node %s: not in the drive", id)
|
||||
}
|
||||
parent, err := c.folderKeysOf(ctx, d.Link.ParentLinkID)
|
||||
parent, err := c.folderKeysIn(ctx, d.Link.ParentLinkID, append(below[:len(below):len(below)], id))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get node %s: %w", id, err)
|
||||
}
|
||||
|
||||
@@ -1240,3 +1240,46 @@ func TestListingRacingChange(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParentCycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := protontest.New(t)
|
||||
u := srv.AddUser("user", "pass")
|
||||
a := u.SeedFolder(u.RootID(), "a")
|
||||
b := u.SeedFolder(a, "b")
|
||||
self := u.SeedFolder(u.RootID(), "self")
|
||||
u.SeedFile(b, "file", []byte("x"), time.Now())
|
||||
u.SetParent(a, b)
|
||||
u.SetParent(self, self)
|
||||
c := newTestClientFor(t, srv, u, clientOptions{})
|
||||
|
||||
for name, id := range map[string]string{"two folders": b, "self": self} {
|
||||
srv.ResetRequests()
|
||||
_, err := c.getNode(context.Background(), id)
|
||||
require.ErrorIs(t, err, errIntegrity, name)
|
||||
assert.Contains(t, err.Error(), "its own ancestor", name)
|
||||
assert.LessOrEqual(t, srv.Requests()["POST /drive/v2/volumes/{volumeID}/links"], 3, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderDepthLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := protontest.New(t)
|
||||
u := srv.AddUser("user", "pass")
|
||||
id := u.RootID()
|
||||
var ids []string
|
||||
for i := range 6 {
|
||||
id = u.SeedFolder(id, fmt.Sprintf("d%d", i))
|
||||
ids = append(ids, id)
|
||||
}
|
||||
c := newTestClientFor(t, srv, u, clientOptions{})
|
||||
c.maxFolderDepth = 4
|
||||
_, err := c.getNode(context.Background(), ids[5])
|
||||
require.ErrorIs(t, err, errIntegrity)
|
||||
assert.Contains(t, err.Error(), "more than 4 deep")
|
||||
// Within the limit is fine.
|
||||
c = newTestClientFor(t, srv, u, clientOptions{})
|
||||
c.maxFolderDepth = 6
|
||||
_, err = c.getNode(context.Background(), ids[5])
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -821,6 +821,19 @@ func (u *User) ForgeRootSigner(signer *User, email string) {
|
||||
l.name = encName
|
||||
}
|
||||
|
||||
// SetParent reports link id as a child of parentID from now on,
|
||||
// without encrypting anything anew, as a hostile server could, even if
|
||||
// that makes a cycle.
|
||||
func (u *User) SetParent(id, parentID string) {
|
||||
u.srv.mu.Lock()
|
||||
defer u.srv.mu.Unlock()
|
||||
l := u.mustDriveLocked().links[id]
|
||||
if l == nil {
|
||||
panic("protontest: no link " + id)
|
||||
}
|
||||
l.parentID = parentID
|
||||
}
|
||||
|
||||
// insertLink adds an active link l under parentID and returns its new
|
||||
// ID, created now unless l has a creation time. It panics if parentID
|
||||
// has an active or draft child with the same name hash, which a seeding
|
||||
|
||||
@@ -596,7 +596,9 @@ library what must stay readable.
|
||||
`func (s *Server) ServeKeysAs(email string, owner *User)`,
|
||||
`func (u *User) SeedFolderSignedAs(signer *User, email, parentID, name string) (id string)` and
|
||||
`func (u *User) SeedFileSignedAs(signer *User, email, parentID, name string, content []byte) (id string)`
|
||||
(Task 16 fix round 3).
|
||||
(Task 16 fix round 3), and
|
||||
`func (u *User) SetParent(id, parentID string)` (a parent the server
|
||||
reports, cycles included).
|
||||
- `func (u *User) Dump() []DumpedNode` - decrypted tree for assertions
|
||||
(`ID`, `Path`, `IsFolder`, `Content`, `ModTime`, `SHA1`, `State`, and
|
||||
`Err` joining every decryption, signature, block-hash, manifest and
|
||||
|
||||
Reference in new issue
Block a user