The archive backend passes remote .sqfs bytes straight to the go-diskfs squashfs parser, which does not validate several attacker-controlled superblock and metadata fields. A crafted image can make the parser panic. Recover panics at the go-diskfs boundary and return an "invalid or corrupt squashfs image" error instead. As well as the parse entry points (Read, ReadDir, OpenFile) this wraps the reader returned by Open, since the parser reads file data lazily and can panic long after the image opened successfully. Addresses GHSA-6jcg-q3wp-x2f4.
483 lines
13 KiB
Go
483 lines
13 KiB
Go
// Package squashfs implements a squashfs archiver for the archive backend
|
|
package squashfs
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/diskfs/go-diskfs/filesystem/squashfs"
|
|
"github.com/rclone/rclone/backend/archive/archiver"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
"github.com/rclone/rclone/fs/log"
|
|
"github.com/rclone/rclone/lib/readers"
|
|
"github.com/rclone/rclone/vfs"
|
|
"github.com/rclone/rclone/vfs/vfscommon"
|
|
)
|
|
|
|
func init() {
|
|
archiver.Register(archiver.Archiver{
|
|
New: New,
|
|
Extension: ".sqfs",
|
|
})
|
|
}
|
|
|
|
// Fs represents a wrapped fs.Fs
|
|
type Fs struct {
|
|
f fs.Fs
|
|
wrapper fs.Fs
|
|
name string
|
|
features *fs.Features // optional features
|
|
vfs *vfs.VFS
|
|
sqfs *squashfs.FileSystem // interface to the squashfs
|
|
c *cache
|
|
node vfs.Node // squashfs file object - set if reading
|
|
remote string // remote of the squashfs file object
|
|
prefix string // position for objects
|
|
prefixSlash string // position for objects with a slash on
|
|
root string // position to read from within the archive
|
|
}
|
|
|
|
// recoverParsePanic converts a panic raised while parsing a squashfs image
|
|
// into an error assigned through err.
|
|
func recoverParsePanic(err *error) {
|
|
if r := recover(); r != nil {
|
|
*err = fmt.Errorf("invalid or corrupt squashfs image: %v", r)
|
|
}
|
|
}
|
|
|
|
// recoveringReadCloser wraps a reader returned by the go-diskfs parser so
|
|
// panics raised while reading become errors.
|
|
//
|
|
// The parser reads file data lazily, so an image which opens cleanly can
|
|
// still panic on the first Read.
|
|
type recoveringReadCloser struct {
|
|
io.ReadCloser
|
|
}
|
|
|
|
func (r recoveringReadCloser) Read(p []byte) (n int, err error) {
|
|
defer recoverParsePanic(&err)
|
|
return r.ReadCloser.Read(p)
|
|
}
|
|
|
|
// New constructs an Fs from the (wrappedFs, remote) with the objects
|
|
// prefix with prefix and rooted at root
|
|
func New(ctx context.Context, wrappedFs fs.Fs, remote, prefix, root string) (_ fs.Fs, err error) {
|
|
defer recoverParsePanic(&err)
|
|
// FIXME vfs cache?
|
|
// FIXME could factor out ReadFileHandle and just use that rather than the full VFS
|
|
fs.Debugf(nil, "Squashfs: New: remote=%q, prefix=%q, root=%q", remote, prefix, root)
|
|
vfsOpt := vfscommon.Opt
|
|
vfsOpt.ReadWait = 0
|
|
VFS := vfs.New(ctx, wrappedFs, &vfsOpt)
|
|
node, err := VFS.Stat(remote)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find %q archive: %w", remote, err)
|
|
}
|
|
|
|
c := newCache(node)
|
|
|
|
// FIXME blocksize
|
|
sqfs, err := squashfs.Read(c, node.Size(), 0, 1024*1024)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read squashfs: %w", err)
|
|
}
|
|
|
|
f := &Fs{
|
|
f: wrappedFs,
|
|
name: path.Join(fs.ConfigString(wrappedFs), remote),
|
|
vfs: VFS,
|
|
node: node,
|
|
sqfs: sqfs,
|
|
c: c,
|
|
remote: remote,
|
|
root: strings.Trim(root, "/"),
|
|
prefix: prefix,
|
|
prefixSlash: prefix + "/",
|
|
}
|
|
if prefix == "" {
|
|
f.prefixSlash = ""
|
|
}
|
|
|
|
singleObject := false
|
|
|
|
// Find the directory the root points to
|
|
if f.root != "" && !strings.HasSuffix(root, "/") {
|
|
native, err := f.toNative("")
|
|
if err == nil {
|
|
native = strings.TrimRight(native, "/")
|
|
_, err := f.newObjectNative(native)
|
|
if err == nil {
|
|
// If it pointed to a file, find the directory above
|
|
f.root = path.Dir(f.root)
|
|
if f.root == "." || f.root == "/" {
|
|
f.root = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME
|
|
// the features here are ones we could support, and they are
|
|
// ANDed with the ones from wrappedFs
|
|
//
|
|
// FIXME some of these need to be forced on - CanHaveEmptyDirectories
|
|
f.features = (&fs.Features{
|
|
CaseInsensitive: false,
|
|
DuplicateFiles: false,
|
|
ReadMimeType: false, // MimeTypes not supported with gsquashfs
|
|
WriteMimeType: false,
|
|
BucketBased: false,
|
|
CanHaveEmptyDirectories: true,
|
|
}).Fill(ctx, f).Mask(ctx, wrappedFs).WrapsFs(f, wrappedFs)
|
|
|
|
if singleObject {
|
|
return f, fs.ErrorIsFile
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// Name of the remote (as passed into NewFs)
|
|
func (f *Fs) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
// Root of the remote (as passed into NewFs)
|
|
func (f *Fs) Root() string {
|
|
return f.root
|
|
}
|
|
|
|
// Features returns the optional features of this Fs
|
|
func (f *Fs) Features() *fs.Features {
|
|
return f.features
|
|
}
|
|
|
|
// String returns a description of the FS
|
|
func (f *Fs) String() string {
|
|
return fmt.Sprintf("Squashfs %q", f.name)
|
|
}
|
|
|
|
// This turns a remote into a native path in the squashfs starting with a /
|
|
func (f *Fs) toNative(remote string) (string, error) {
|
|
native := strings.Trim(remote, "/")
|
|
if f.prefix == "" {
|
|
native = "/" + native
|
|
} else if native == f.prefix {
|
|
native = "/"
|
|
} else if !strings.HasPrefix(native, f.prefixSlash) {
|
|
return "", fmt.Errorf("internal error: %q doesn't start with prefix %q", native, f.prefixSlash)
|
|
} else {
|
|
native = native[len(f.prefix):]
|
|
}
|
|
if f.root != "" {
|
|
native = "/" + f.root + native
|
|
}
|
|
return native, nil
|
|
}
|
|
|
|
// This turns a native path with a leading / into a path suitable for
|
|
// the squashfs library which requires io/fs.ValidPath paths with no
|
|
// leading / and "." for the root directory.
|
|
func toIOFS(native string) string {
|
|
native = strings.Trim(native, "/")
|
|
if native == "" {
|
|
return "."
|
|
}
|
|
return native
|
|
}
|
|
|
|
// Turn a (nativeDir, leaf) into a remote
|
|
func (f *Fs) fromNative(nativeDir string, leaf string) string {
|
|
// fs.Debugf(nil, "nativeDir = %q, leaf = %q, root=%q", nativeDir, leaf, f.root)
|
|
dir := nativeDir
|
|
if f.root != "" {
|
|
dir = strings.TrimPrefix(dir, "/"+f.root)
|
|
}
|
|
remote := f.prefixSlash + strings.Trim(path.Join(dir, leaf), "/")
|
|
// fs.Debugf(nil, "dir = %q, remote=%q", dir, remote)
|
|
return remote
|
|
}
|
|
|
|
// Convert a FileInfo into an Object from native dir
|
|
func (f *Fs) objectFromFileInfo(nativeDir string, item os.FileInfo) *Object {
|
|
return &Object{
|
|
fs: f,
|
|
remote: f.fromNative(nativeDir, item.Name()),
|
|
size: item.Size(),
|
|
modTime: item.ModTime(),
|
|
}
|
|
}
|
|
|
|
// List the objects and directories in dir into entries. The
|
|
// entries can be returned in any order but should be for a
|
|
// complete directory.
|
|
//
|
|
// dir should be "" to list the root, and should not have
|
|
// trailing slashes.
|
|
//
|
|
// This should return ErrDirNotFound if the directory isn't
|
|
// found.
|
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
|
defer log.Trace(f, "dir=%q", dir)("entries=%v, err=%v", &entries, &err)
|
|
defer recoverParsePanic(&err)
|
|
|
|
nativeDir, err := f.toNative(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items, err := f.sqfs.ReadDir(toIOFS(nativeDir))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read squashfs: couldn't read directory: %w", err)
|
|
}
|
|
|
|
entries = make(fs.DirEntries, 0, len(items))
|
|
for _, item := range items {
|
|
// fs.Debugf(item.Name(), "entry = %#v", item)
|
|
var entry fs.DirEntry
|
|
if item.IsDir() {
|
|
remote := f.fromNative(nativeDir, item.Name())
|
|
info, err := item.Info()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading item %q: %w", item.Name(), err)
|
|
}
|
|
entry = fs.NewDir(remote, info.ModTime())
|
|
} else if item.Type().IsRegular() {
|
|
info, err := item.Info()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading item %q: %w", item.Name(), err)
|
|
}
|
|
entry = f.objectFromFileInfo(nativeDir, info)
|
|
} else {
|
|
fs.Debugf(item.Name(), "FIXME Not regular file - skipping")
|
|
continue
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
// fs.Debugf(f, "dir=%q, entries=%v", dir, entries)
|
|
return entries, nil
|
|
}
|
|
|
|
// newObjectNative finds the object at the native path passed in
|
|
func (f *Fs) newObjectNative(nativePath string) (o fs.Object, err error) {
|
|
defer recoverParsePanic(&err)
|
|
// get the path and filename
|
|
dir, leaf := path.Split(nativePath)
|
|
dir = strings.TrimRight(dir, "/")
|
|
leaf = strings.Trim(leaf, "/")
|
|
|
|
// FIXME need to detect directory not found
|
|
fis, err := f.sqfs.ReadDir(toIOFS(dir))
|
|
if err != nil {
|
|
|
|
return nil, fs.ErrorObjectNotFound
|
|
}
|
|
|
|
for _, fi := range fis {
|
|
if fi.Name() == leaf {
|
|
if fi.IsDir() {
|
|
return nil, fs.ErrorNotAFile
|
|
}
|
|
info, err := fi.Info()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading item %q: %w", fi.Name(), err)
|
|
}
|
|
o = f.objectFromFileInfo(dir, info)
|
|
break
|
|
}
|
|
}
|
|
if o == nil {
|
|
return nil, fs.ErrorObjectNotFound
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
// NewObject finds the Object at remote.
|
|
func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err error) {
|
|
defer log.Trace(f, "remote=%q", remote)("obj=%v, err=%v", &o, &err)
|
|
|
|
nativePath, err := f.toNative(remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f.newObjectNative(nativePath)
|
|
}
|
|
|
|
// Precision of the ModTimes in this Fs
|
|
func (f *Fs) Precision() time.Duration {
|
|
return time.Second
|
|
}
|
|
|
|
// Mkdir makes the directory (container, bucket)
|
|
//
|
|
// Shouldn't return an error if it already exists
|
|
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
|
return vfs.EROFS
|
|
}
|
|
|
|
// Rmdir removes the directory (container, bucket) if empty
|
|
//
|
|
// Return an error if it doesn't exist or isn't empty
|
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|
return vfs.EROFS
|
|
}
|
|
|
|
// Put in to the remote path with the modTime given of the given size
|
|
//
|
|
// May create the object even if it returns an error - if so
|
|
// will return the object and the error, otherwise will return
|
|
// nil and the error
|
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) {
|
|
return nil, vfs.EROFS
|
|
}
|
|
|
|
// Hashes returns the supported hash sets.
|
|
func (f *Fs) Hashes() hash.Set {
|
|
return hash.Set(hash.None)
|
|
}
|
|
|
|
// UnWrap returns the Fs that this Fs is wrapping
|
|
func (f *Fs) UnWrap() fs.Fs {
|
|
return f.f
|
|
}
|
|
|
|
// WrapFs returns the Fs that is wrapping this Fs
|
|
func (f *Fs) WrapFs() fs.Fs {
|
|
return f.wrapper
|
|
}
|
|
|
|
// SetWrapper sets the Fs that is wrapping this Fs
|
|
func (f *Fs) SetWrapper(wrapper fs.Fs) {
|
|
f.wrapper = wrapper
|
|
}
|
|
|
|
// Object describes an object to be read from the raw squashfs file
|
|
type Object struct {
|
|
fs *Fs
|
|
remote string
|
|
size int64
|
|
modTime time.Time
|
|
}
|
|
|
|
// Fs returns read only access to the Fs that this object is part of
|
|
func (o *Object) Fs() fs.Info {
|
|
return o.fs
|
|
}
|
|
|
|
// Return a string version
|
|
func (o *Object) String() string {
|
|
if o == nil {
|
|
return "<nil>"
|
|
}
|
|
return o.Remote()
|
|
}
|
|
|
|
// Turn a squashfs path into a full path for the parent Fs
|
|
// func (o *Object) path(remote string) string {
|
|
// return path.Join(o.fs.prefix, remote)
|
|
// }
|
|
|
|
// Remote returns the remote path
|
|
func (o *Object) Remote() string {
|
|
return o.remote
|
|
}
|
|
|
|
// Size returns the size of the file
|
|
func (o *Object) Size() int64 {
|
|
return o.size
|
|
}
|
|
|
|
// ModTime returns the modification time of the object
|
|
//
|
|
// It attempts to read the objects mtime and if that isn't present the
|
|
// LastModified returned in the http headers
|
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
|
return o.modTime
|
|
}
|
|
|
|
// SetModTime sets the modification time of the local fs object
|
|
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|
return vfs.EROFS
|
|
}
|
|
|
|
// Storable raturns a boolean indicating if this object is storable
|
|
func (o *Object) Storable() bool {
|
|
return true
|
|
}
|
|
|
|
// Hash returns the selected checksum of the file
|
|
// If no checksum is available it returns ""
|
|
func (o *Object) Hash(ctx context.Context, ht hash.Type) (string, error) {
|
|
return "", hash.ErrUnsupported
|
|
}
|
|
|
|
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (rc io.ReadCloser, err error) {
|
|
defer recoverParsePanic(&err)
|
|
var offset, limit int64 = 0, -1
|
|
for _, option := range options {
|
|
switch x := option.(type) {
|
|
case *fs.SeekOption:
|
|
offset = x.Offset
|
|
case *fs.RangeOption:
|
|
offset, limit = x.Decode(o.Size())
|
|
default:
|
|
if option.Mandatory() {
|
|
fs.Logf(o, "Unsupported mandatory option: %v", option)
|
|
}
|
|
}
|
|
}
|
|
|
|
remote, err := o.fs.toNative(o.remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fs.Debugf(o, "Opening %q", remote)
|
|
fh, err := o.fs.sqfs.OpenFile(toIOFS(remote), os.O_RDONLY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// discard data from start as necessary
|
|
if offset > 0 {
|
|
_, err = fh.Seek(offset, io.SeekStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
rc = recoveringReadCloser{fh}
|
|
|
|
// If limited then don't return everything
|
|
if limit >= 0 {
|
|
fs.Debugf(nil, "limit=%d, offset=%d, options=%v", limit, offset, options)
|
|
return readers.NewLimitedReadCloser(rc, limit), nil
|
|
}
|
|
|
|
return rc, nil
|
|
}
|
|
|
|
// Update in to the object with the modTime given of the given size
|
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
|
return vfs.EROFS
|
|
}
|
|
|
|
// Remove an object
|
|
func (o *Object) Remove(ctx context.Context) error {
|
|
return vfs.EROFS
|
|
}
|
|
|
|
// Check the interfaces are satisfied
|
|
var (
|
|
_ fs.Fs = (*Fs)(nil)
|
|
_ fs.UnWrapper = (*Fs)(nil)
|
|
_ fs.Wrapper = (*Fs)(nil)
|
|
_ fs.Object = (*Object)(nil)
|
|
)
|