dump: Rewrite Linux ACL handling

The old version was taken from an MPL-licensed library. This is a
cleanroom implementation. The code is shorter and it's now explicit that
only Linux ACLs are supported.
This commit is contained in:
greatroar 2023-05-28 11:35:55 +02:00
parent f96896a9c0
commit aaf5254e26
3 changed files with 120 additions and 224 deletions

View File

@ -1,131 +1,88 @@
package dump package dump
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"fmt" "errors"
"strconv"
) )
const ( const (
aclUserOwner = 0x0001 // Permissions
aclUser = 0x0002 aclPermRead = 0x4
aclGroupOwner = 0x0004 aclPermWrite = 0x2
aclGroup = 0x0008 aclPermExecute = 0x1
aclMask = 0x0010
aclOthers = 0x0020 // Tags
aclTagUserObj = 0x01 // Owner.
aclTagUser = 0x02
aclTagGroupObj = 0x04 // Owning group.
aclTagGroup = 0x08
aclTagMask = 0x10
aclTagOther = 0x20
) )
type aclSID uint64 // formatLinuxACL converts a Linux ACL from its binary format to the POSIX.1e
// long text format.
type aclElem struct { //
Tag uint16 // User and group IDs are printed in decimal, because we may be dumping
Perm uint16 // a snapshot from a different machine.
ID uint32 //
} // https://man7.org/linux/man-pages/man5/acl.5.html
// https://savannah.nongnu.org/projects/acl
type acl struct { // https://simson.net/ref/1997/posix_1003.1e-990310.pdf
Version uint32 func formatLinuxACL(acl []byte) (string, error) {
List []aclElement if len(acl)-4 < 0 || (len(acl)-4)%8 != 0 {
} return "", errors.New("wrong length")
type aclElement struct {
aclSID
Perm uint16
}
func (a aclSID) getType() int {
return int(a >> 32)
}
func (a aclSID) getID() uint32 {
return uint32(a & 0xffffffff)
}
func (a aclSID) String() string {
switch a >> 32 {
case aclUserOwner:
return "user::"
case aclUser:
return fmt.Sprintf("user:%v:", a.getID())
case aclGroupOwner:
return "group::"
case aclGroup:
return fmt.Sprintf("group:%v:", a.getID())
case aclMask:
return "mask::"
case aclOthers:
return "other::"
} }
return "?:" version := binary.LittleEndian.Uint32(acl)
} if version != 2 {
return "", errors.New("unsupported ACL format version")
}
acl = acl[4:]
func (a aclElement) String() string { text := make([]byte, 0, 2*len(acl))
str := ""
if (a.Perm & 4) != 0 {
str += "r"
} else {
str += "-"
}
if (a.Perm & 2) != 0 {
str += "w"
} else {
str += "-"
}
if (a.Perm & 1) != 0 {
str += "x"
} else {
str += "-"
}
return fmt.Sprintf("%v%v", a.aclSID, str)
}
func (a *acl) decode(xattr []byte) { for ; len(acl) >= 8; acl = acl[8:] {
var elem aclElement tag := binary.LittleEndian.Uint16(acl)
ae := new(aclElem) perm := binary.LittleEndian.Uint16(acl[2:])
nr := bytes.NewReader(xattr) id := binary.LittleEndian.Uint32(acl[4:])
e := binary.Read(nr, binary.LittleEndian, &a.Version)
if e != nil {
a.Version = 0
return
}
if len(a.List) > 0 {
a.List = a.List[:0]
}
for binary.Read(nr, binary.LittleEndian, ae) == nil {
elem.aclSID = (aclSID(ae.Tag) << 32) | aclSID(ae.ID)
elem.Perm = ae.Perm
a.List = append(a.List, elem)
}
}
func (a *acl) encode() []byte { switch tag {
buf := new(bytes.Buffer) case aclTagUserObj:
ae := new(aclElem) text = append(text, "user:"...)
case aclTagUser:
err := binary.Write(buf, binary.LittleEndian, &a.Version) text = append(text, "user:"...)
// write to a bytes.Buffer always returns a nil error text = strconv.AppendUint(text, uint64(id), 10)
if err != nil { case aclTagGroupObj:
panic(err) text = append(text, "group:"...)
} case aclTagGroup:
text = append(text, "group:"...)
for _, elem := range a.List { text = strconv.AppendUint(text, uint64(id), 10)
ae.Tag = uint16(elem.getType()) case aclTagMask:
ae.Perm = elem.Perm text = append(text, "mask:"...)
ae.ID = elem.getID() case aclTagOther:
text = append(text, "other:"...)
err := binary.Write(buf, binary.LittleEndian, ae) default:
// write to a bytes.Buffer always returns a nil error return "", errors.New("unknown tag")
if err != nil {
panic(err)
} }
text = append(text, ':')
text = append(text, aclPermText(perm)...)
text = append(text, '\n')
} }
return buf.Bytes()
return string(text), nil
} }
func (a *acl) String() string { func aclPermText(p uint16) []byte {
var finalacl string s := []byte("---")
for _, acl := range a.List { if p&aclPermRead != 0 {
finalacl += acl.String() + "\n" s[0] = 'r'
} }
return finalacl if p&aclPermWrite != 0 {
s[1] = 'w'
}
if p&aclPermExecute != 0 {
s[2] = 'x'
}
return s
} }

View File

@ -1,114 +1,46 @@
package dump package dump
import ( import (
"reflect"
"testing" "testing"
rtest "github.com/restic/restic/internal/test"
) )
func Test_acl_decode(t *testing.T) { func TestFormatLinuxACL(t *testing.T) {
type args struct { for _, c := range []struct {
xattr []byte in, out, err string
}
tests := []struct {
name string
args args
want string
}{ }{
{ {
name: "decode string", in: "\x02\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
args: args{ "\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
xattr: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255}, "\x04\x00\x02\x00\xff\xff\xff\xff\b\x00\x01\x00'\x00\x00\x00" +
}, "\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff",
want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n", out: "user::rw-\nuser:3:r--\nuser:1001:r--\ngroup::-w-\n" +
"group:39:--x\nmask::rwx\nother::r--\n",
}, },
{ {
name: "decode group", in: "\x02\x00\x00\x00\x00\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
args: args{ "\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
xattr: []byte{2, 0, 0, 0, 8, 0, 1, 0, 254, 255, 0, 0}, "\x04\x00\x06\x00\xff\xff\xff\xff\b\x00\x05\x00'\x00\x00\x00" +
}, "\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff",
want: "group:65534:--x\n", err: "unknown tag",
}, },
{ {
name: "decode fail", in: "\x01\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
args: args{ "\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
xattr: []byte("abctest"), "\x04\x00\x06\x00\xff\xff\xff\xff\b\x00\x05\x00'\x00\x00\x00" +
}, "\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff",
want: "", err: "unsupported ACL format version",
}, },
{ {in: "\x02\x00", err: "wrong length"},
name: "decode empty fail", {in: "", err: "wrong length"},
args: args{ } {
xattr: []byte(""), out, err := formatLinuxACL([]byte(c.in))
}, if c.err == "" {
want: "", rtest.Equals(t, c.out, out)
}, } else {
} rtest.Assert(t, err != nil, "wanted %q but got nil", c.err)
for _, tt := range tests { rtest.Equals(t, c.err, err.Error())
t.Run(tt.name, func(t *testing.T) { }
a := &acl{}
a.decode(tt.args.xattr)
if tt.want != a.String() {
t.Errorf("acl.decode() = %v, want: %v", a.String(), tt.want)
}
a.decode(tt.args.xattr)
if tt.want != a.String() {
t.Errorf("second acl.decode() = %v, want: %v", a.String(), tt.want)
}
})
}
}
func Test_acl_encode(t *testing.T) {
tests := []struct {
name string
want []byte
args []aclElement
}{
{
name: "encode values",
want: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
args: []aclElement{
{
aclSID: 8589934591,
Perm: 6,
},
{
aclSID: 8589934592,
Perm: 7,
},
{
aclSID: 8590000126,
Perm: 7,
},
{
aclSID: 21474836479,
Perm: 7,
},
{
aclSID: 73014444031,
Perm: 7,
},
{
aclSID: 141733920767,
Perm: 4,
},
},
},
{
name: "encode fail",
want: []byte{2, 0, 0, 0},
args: []aclElement{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &acl{
Version: 2,
List: tt.args,
}
if got := a.encode(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("acl.encode() = %v, want %v", got, tt.want)
}
})
} }
} }

View File

@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
) )
@ -104,21 +104,28 @@ func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string {
tmpMap := make(map[string]string) tmpMap := make(map[string]string)
for _, attr := range xattrs { for _, attr := range xattrs {
attrString := string(attr.Value) // Check for Linux POSIX.1e ACLs.
//
// TODO support ACLs from other operating systems.
// FreeBSD ACLs have names "posix1e.acl_(access|default)",
// but their binary format may not match the Linux format.
aclKey := ""
switch attr.Name {
case "system.posix_acl_access":
aclKey = "SCHILY.acl.access"
case "system.posix_acl_default":
aclKey = "SCHILY.acl.default"
}
if strings.HasPrefix(attr.Name, "system.posix_acl_") { if aclKey != "" {
na := acl{} text, err := formatLinuxACL(attr.Value)
na.decode(attr.Value) if err != nil {
debug.Log("parsing Linux ACL: %v, skipping", err)
if na.String() != "" { continue
if strings.Contains(attr.Name, "system.posix_acl_access") {
tmpMap["SCHILY.acl.access"] = na.String()
} else if strings.Contains(attr.Name, "system.posix_acl_default") {
tmpMap["SCHILY.acl.default"] = na.String()
}
} }
tmpMap[aclKey] = text
} else { } else {
tmpMap["SCHILY.xattr."+attr.Name] = attrString tmpMap["SCHILY.xattr."+attr.Name] = string(attr.Value)
} }
} }