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
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
import (
"bytes"
"encoding/binary"
"fmt"
"errors"
"strconv"
)
const (
aclUserOwner = 0x0001
aclUser = 0x0002
aclGroupOwner = 0x0004
aclGroup = 0x0008
aclMask = 0x0010
aclOthers = 0x0020
// Permissions
aclPermRead = 0x4
aclPermWrite = 0x2
aclPermExecute = 0x1
// Tags
aclTagUserObj = 0x01 // Owner.
aclTagUser = 0x02
aclTagGroupObj = 0x04 // Owning group.
aclTagGroup = 0x08
aclTagMask = 0x10
aclTagOther = 0x20
)
type aclSID uint64
type aclElem struct {
Tag uint16
Perm uint16
ID uint32
}
type acl struct {
Version uint32
List []aclElement
}
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::"
// formatLinuxACL converts a Linux ACL from its binary format to the POSIX.1e
// long text format.
//
// User and group IDs are printed in decimal, because we may be dumping
// a snapshot from a different machine.
//
// https://man7.org/linux/man-pages/man5/acl.5.html
// https://savannah.nongnu.org/projects/acl
// https://simson.net/ref/1997/posix_1003.1e-990310.pdf
func formatLinuxACL(acl []byte) (string, error) {
if len(acl)-4 < 0 || (len(acl)-4)%8 != 0 {
return "", errors.New("wrong length")
}
return "?:"
}
version := binary.LittleEndian.Uint32(acl)
if version != 2 {
return "", errors.New("unsupported ACL format version")
}
acl = acl[4:]
func (a aclElement) String() string {
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)
}
text := make([]byte, 0, 2*len(acl))
func (a *acl) decode(xattr []byte) {
var elem aclElement
ae := new(aclElem)
nr := bytes.NewReader(xattr)
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)
}
}
for ; len(acl) >= 8; acl = acl[8:] {
tag := binary.LittleEndian.Uint16(acl)
perm := binary.LittleEndian.Uint16(acl[2:])
id := binary.LittleEndian.Uint32(acl[4:])
func (a *acl) encode() []byte {
buf := new(bytes.Buffer)
ae := new(aclElem)
err := binary.Write(buf, binary.LittleEndian, &a.Version)
// write to a bytes.Buffer always returns a nil error
if err != nil {
panic(err)
}
for _, elem := range a.List {
ae.Tag = uint16(elem.getType())
ae.Perm = elem.Perm
ae.ID = elem.getID()
err := binary.Write(buf, binary.LittleEndian, ae)
// write to a bytes.Buffer always returns a nil error
if err != nil {
panic(err)
switch tag {
case aclTagUserObj:
text = append(text, "user:"...)
case aclTagUser:
text = append(text, "user:"...)
text = strconv.AppendUint(text, uint64(id), 10)
case aclTagGroupObj:
text = append(text, "group:"...)
case aclTagGroup:
text = append(text, "group:"...)
text = strconv.AppendUint(text, uint64(id), 10)
case aclTagMask:
text = append(text, "mask:"...)
case aclTagOther:
text = append(text, "other:"...)
default:
return "", errors.New("unknown tag")
}
text = append(text, ':')
text = append(text, aclPermText(perm)...)
text = append(text, '\n')
}
return buf.Bytes()
return string(text), nil
}
func (a *acl) String() string {
var finalacl string
for _, acl := range a.List {
finalacl += acl.String() + "\n"
func aclPermText(p uint16) []byte {
s := []byte("---")
if p&aclPermRead != 0 {
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
import (
"reflect"
"testing"
rtest "github.com/restic/restic/internal/test"
)
func Test_acl_decode(t *testing.T) {
type args struct {
xattr []byte
}
tests := []struct {
name string
args args
want string
func TestFormatLinuxACL(t *testing.T) {
for _, c := range []struct {
in, out, err string
}{
{
name: "decode string",
args: args{
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},
},
want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n",
in: "\x02\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
"\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",
out: "user::rw-\nuser:3:r--\nuser:1001:r--\ngroup::-w-\n" +
"group:39:--x\nmask::rwx\nother::r--\n",
},
{
name: "decode group",
args: args{
xattr: []byte{2, 0, 0, 0, 8, 0, 1, 0, 254, 255, 0, 0},
},
want: "group:65534:--x\n",
in: "\x02\x00\x00\x00\x00\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
"\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",
err: "unknown tag",
},
{
name: "decode fail",
args: args{
xattr: []byte("abctest"),
},
want: "",
in: "\x01\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
"\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",
err: "unsupported ACL format version",
},
{
name: "decode empty fail",
args: args{
xattr: []byte(""),
},
want: "",
},
}
for _, tt := range tests {
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)
}
})
{in: "\x02\x00", err: "wrong length"},
{in: "", err: "wrong length"},
} {
out, err := formatLinuxACL([]byte(c.in))
if c.err == "" {
rtest.Equals(t, c.out, out)
} else {
rtest.Assert(t, err != nil, "wanted %q but got nil", c.err)
rtest.Equals(t, c.err, err.Error())
}
}
}

View File

@ -6,8 +6,8 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
)
@ -104,21 +104,28 @@ func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string {
tmpMap := make(map[string]string)
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_") {
na := acl{}
na.decode(attr.Value)
if na.String() != "" {
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()
}
if aclKey != "" {
text, err := formatLinuxACL(attr.Value)
if err != nil {
debug.Log("parsing Linux ACL: %v, skipping", err)
continue
}
tmpMap[aclKey] = text
} else {
tmpMap["SCHILY.xattr."+attr.Name] = attrString
tmpMap["SCHILY.xattr."+attr.Name] = string(attr.Value)
}
}