Skip to content

Commit e221e94

Browse files
PsychoPunkSagelidelgammazero
authored
fix(mfs): basic UnixFS sanity checks in files cp (#10701)
Signed-off-by: Abhinav Prakash <[email protected]> Co-authored-by: Marcin Rataj <[email protected]> Co-authored-by: Andrew Gillis <[email protected]>
1 parent 86aee74 commit e221e94

File tree

4 files changed

+190
-2
lines changed

4 files changed

+190
-2
lines changed

core/commands/files.go

+20
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ func walkBlock(ctx context.Context, dagserv ipld.DAGService, nd ipld.Node) (bool
402402
return local, sizeLocal, nil
403403
}
404404

405+
var errFilesCpInvalidUnixFS = errors.New("cp: source must be a valid UnixFS (dag-pb or raw codec)")
405406
var filesCpCmd = &cmds.Command{
406407
Helptext: cmds.HelpText{
407408
Tagline: "Add references to IPFS files and directories in MFS (or copy within MFS).",
@@ -480,6 +481,25 @@ being GC'ed.
480481
return fmt.Errorf("cp: cannot get node from path %s: %s", src, err)
481482
}
482483

484+
// Sanity-check: ensure root CID is a valid UnixFS (dag-pb or raw block)
485+
// Context: https://github.com/ipfs/kubo/issues/10331
486+
srcCidType := node.Cid().Type()
487+
switch srcCidType {
488+
case cid.Raw:
489+
if _, ok := node.(*dag.RawNode); !ok {
490+
return errFilesCpInvalidUnixFS
491+
}
492+
case cid.DagProtobuf:
493+
if _, ok := node.(*dag.ProtoNode); !ok {
494+
return errFilesCpInvalidUnixFS
495+
}
496+
if _, err = ft.FSNodeFromBytes(node.(*dag.ProtoNode).Data()); err != nil {
497+
return fmt.Errorf("%w: %v", errFilesCpInvalidUnixFS, err)
498+
}
499+
default:
500+
return errFilesCpInvalidUnixFS
501+
}
502+
483503
if mkParents {
484504
err := ensureContainingDirectoryExists(nd.FilesRoot, dst, prefix)
485505
if err != nil {

core/commands/files_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"io"
6+
"testing"
7+
8+
dag "github.com/ipfs/boxo/ipld/merkledag"
9+
cmds "github.com/ipfs/go-ipfs-cmds"
10+
coremock "github.com/ipfs/kubo/core/mock"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestFilesCp_DagCborNodeFails(t *testing.T) {
15+
ctx, cancel := context.WithCancel(context.Background())
16+
defer cancel()
17+
18+
cmdCtx, err := coremock.MockCmdsCtx()
19+
require.NoError(t, err)
20+
21+
node, err := cmdCtx.ConstructNode()
22+
require.NoError(t, err)
23+
24+
invalidData := []byte{0x00}
25+
protoNode := dag.NodeWithData(invalidData)
26+
err = node.DAG.Add(ctx, protoNode)
27+
require.NoError(t, err)
28+
29+
req := &cmds.Request{
30+
Context: ctx,
31+
Arguments: []string{
32+
"/ipfs/" + protoNode.Cid().String(),
33+
"/test-destination",
34+
},
35+
Options: map[string]interface{}{
36+
"force": false,
37+
},
38+
}
39+
40+
_, pw := io.Pipe()
41+
res, err := cmds.NewWriterResponseEmitter(pw, req)
42+
require.NoError(t, err)
43+
44+
err = filesCpCmd.Run(req, res, &cmdCtx)
45+
require.Error(t, err)
46+
require.ErrorContains(t, err, "cp: source must be a valid UnixFS (dag-pb or raw codec)")
47+
}

docs/changelogs/v0.34.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
- [`IPFS_LOG_LEVEL` deprecated](#ipfs_log_level-deprecated)
1313
- [Pebble datastore format update](#pebble-datastore-format-update)
1414
- [Badger datastore update](#badger-datastore-update)
15-
- [Datastore Implementation updates](#datastore-implementation-updates)
16-
- [One multi-error package](#one-multi-error-package)
15+
- [Datastore Implementation Updates](#datastore-implementation-updates)
16+
- [One Multi-error Package](#one-multi-error-package)
1717
- [📦️ Important dependency updates](#-important-dependency-updates)
1818
- [👨‍👩‍👧‍👦 Contributors](#-contributors)
1919

@@ -48,6 +48,7 @@ For more details, check out the [`AutoTLS` configuration documentation](https://
4848
- `ipfs config` is now validating json fields ([#10679](https://github.com/ipfs/kubo/pull/10679)).
4949
- Deprecated the `bitswap reprovide` command. Make sure to switch to modern `routing reprovide`. ([#10677](https://github.com/ipfs/kubo/pull/10677))
5050
- The `stats reprovide` command now shows additional stats for [`Routing.AcceleratedDHTClient`](https://github.com/ipfs/kubo/blob/master/docs/config.md#routingaccelerateddhtclient), indicating the last and next `reprovide` times. ([#10677](https://github.com/ipfs/kubo/pull/10677))
51+
- `ipfs files cp` now performs basic codec check and will error when source is not a valid UnixFS (only `dag-pb` and `raw` codecs are allowed in MFS)
5152

5253
#### Bitswap improvements from Boxo
5354

test/cli/files_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/ipfs/kubo/test/cli/harness"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestFilesCp(t *testing.T) {
15+
t.Parallel()
16+
17+
t.Run("files cp with valid UnixFS succeeds", func(t *testing.T) {
18+
t.Parallel()
19+
20+
node := harness.NewT(t).NewNode().Init().StartDaemon()
21+
22+
// Create simple text file
23+
data := "testing files cp command"
24+
cid := node.IPFSAddStr(data)
25+
26+
// Copy form IPFS => MFS
27+
res := node.IPFS("files", "cp", fmt.Sprintf("/ipfs/%s", cid), "/valid-file")
28+
assert.NoError(t, res.Err)
29+
30+
// verification
31+
catRes := node.IPFS("files", "read", "/valid-file")
32+
assert.Equal(t, data, catRes.Stdout.Trimmed())
33+
})
34+
35+
t.Run("files cp with unsupported DAG node type fails", func(t *testing.T) {
36+
t.Parallel()
37+
node := harness.NewT(t).NewNode().Init().StartDaemon()
38+
39+
// MFS UnixFS is limited to dag-pb or raw, so we create a dag-cbor node to test this
40+
jsonData := `{"data": "not a UnixFS node"}`
41+
tempFile := filepath.Join(node.Dir, "test.json")
42+
err := os.WriteFile(tempFile, []byte(jsonData), 0644)
43+
require.NoError(t, err)
44+
cid := node.IPFS("dag", "put", "--input-codec=json", "--store-codec=dag-cbor", tempFile).Stdout.Trimmed()
45+
46+
// copy without --force
47+
res := node.RunIPFS("files", "cp", fmt.Sprintf("/ipfs/%s", cid), "/invalid-file")
48+
assert.NotEqual(t, 0, res.ExitErr.ExitCode())
49+
assert.Contains(t, res.Stderr.String(), "Error: cp: source must be a valid UnixFS (dag-pb or raw codec)")
50+
})
51+
52+
t.Run("files cp with invalid UnixFS data structure fails", func(t *testing.T) {
53+
t.Parallel()
54+
node := harness.NewT(t).NewNode().Init().StartDaemon()
55+
56+
// Create an invalid proto file
57+
data := []byte{0xDE, 0xAD, 0xBE, 0xEF} // Invalid protobuf data
58+
tempFile := filepath.Join(node.Dir, "invalid-proto.bin")
59+
err := os.WriteFile(tempFile, data, 0644)
60+
require.NoError(t, err)
61+
62+
res := node.IPFS("block", "put", "--format=raw", tempFile)
63+
require.NoError(t, res.Err)
64+
65+
// we manually changed codec from raw to dag-pb to test "bad dag-pb" scenario
66+
cid := "bafybeic7pdbte5heh6u54vszezob3el6exadoiw4wc4ne7ny2x7kvajzkm"
67+
68+
// should fail because node cant be read as a valid dag-pb
69+
cpResNoForce := node.RunIPFS("files", "cp", fmt.Sprintf("/ipfs/%s", cid), "/invalid-proto")
70+
assert.NotEqual(t, 0, cpResNoForce.ExitErr.ExitCode())
71+
assert.Contains(t, cpResNoForce.Stderr.String(), "Error")
72+
})
73+
74+
t.Run("files cp with raw node succeeds", func(t *testing.T) {
75+
t.Parallel()
76+
node := harness.NewT(t).NewNode().Init().StartDaemon()
77+
78+
// Create a raw node
79+
data := "raw data"
80+
tempFile := filepath.Join(node.Dir, "raw.bin")
81+
err := os.WriteFile(tempFile, []byte(data), 0644)
82+
require.NoError(t, err)
83+
84+
res := node.IPFS("block", "put", "--format=raw", tempFile)
85+
require.NoError(t, res.Err)
86+
cid := res.Stdout.Trimmed()
87+
88+
// Copy from IPFS to MFS (raw nodes should work without --force)
89+
cpRes := node.IPFS("files", "cp", fmt.Sprintf("/ipfs/%s", cid), "/raw-file")
90+
assert.NoError(t, cpRes.Err)
91+
92+
// Verify the file was copied correctly
93+
catRes := node.IPFS("files", "read", "/raw-file")
94+
assert.Equal(t, data, catRes.Stdout.Trimmed())
95+
})
96+
97+
t.Run("files cp creates intermediate directories with -p", func(t *testing.T) {
98+
t.Parallel()
99+
node := harness.NewT(t).NewNode().Init().StartDaemon()
100+
101+
// Create a simple text file and add it to IPFS
102+
data := "hello parent directories"
103+
tempFile := filepath.Join(node.Dir, "parent-test.txt")
104+
err := os.WriteFile(tempFile, []byte(data), 0644)
105+
require.NoError(t, err)
106+
107+
cid := node.IPFS("add", "-Q", tempFile).Stdout.Trimmed()
108+
109+
// Copy from IPFS to MFS with parent flag
110+
res := node.IPFS("files", "cp", "-p", fmt.Sprintf("/ipfs/%s", cid), "/parent/dir/file")
111+
assert.NoError(t, res.Err)
112+
113+
// Verify the file and directories were created
114+
lsRes := node.IPFS("files", "ls", "/parent/dir")
115+
assert.Contains(t, lsRes.Stdout.String(), "file")
116+
117+
catRes := node.IPFS("files", "read", "/parent/dir/file")
118+
assert.Equal(t, data, catRes.Stdout.Trimmed())
119+
})
120+
}

0 commit comments

Comments
 (0)