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.
//
// 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")
}
version := binary.LittleEndian.Uint32(acl)
if version != 2 {
return "", errors.New("unsupported ACL format version")
}
acl = acl[4:]
type aclElem struct { text := make([]byte, 0, 2*len(acl))
Tag uint16
Perm uint16 for ; len(acl) >= 8; acl = acl[8:] {
ID uint32 tag := binary.LittleEndian.Uint16(acl)
perm := binary.LittleEndian.Uint16(acl[2:])
id := binary.LittleEndian.Uint32(acl[4:])
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 string(text), nil
} }
type acl struct { func aclPermText(p uint16) []byte {
Version uint32 s := []byte("---")
List []aclElement if p&aclPermRead != 0 {
} s[0] = 'r'
}
type aclElement struct { if p&aclPermWrite != 0 {
aclSID s[1] = 'w'
Perm uint16 }
} if p&aclPermExecute != 0 {
s[2] = 'x'
func (a aclSID) getType() int { }
return int(a >> 32) return s
}
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 "?:"
}
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)
}
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)
}
}
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)
}
}
return buf.Bytes()
}
func (a *acl) String() string {
var finalacl string
for _, acl := range a.List {
finalacl += acl.String() + "\n"
}
return finalacl
} }

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",
},
{
name: "decode empty fail",
args: args{
xattr: []byte(""),
},
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())
} }
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)
}
})
} }
} }

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.
//
if strings.HasPrefix(attr.Name, "system.posix_acl_") { // TODO support ACLs from other operating systems.
na := acl{} // FreeBSD ACLs have names "posix1e.acl_(access|default)",
na.decode(attr.Value) // but their binary format may not match the Linux format.
aclKey := ""
if na.String() != "" { switch attr.Name {
if strings.Contains(attr.Name, "system.posix_acl_access") { case "system.posix_acl_access":
tmpMap["SCHILY.acl.access"] = na.String() aclKey = "SCHILY.acl.access"
} else if strings.Contains(attr.Name, "system.posix_acl_default") { case "system.posix_acl_default":
tmpMap["SCHILY.acl.default"] = na.String() aclKey = "SCHILY.acl.default"
} }
if aclKey != "" {
text, err := formatLinuxACL(attr.Value)
if err != nil {
debug.Log("parsing Linux ACL: %v, skipping", err)
continue
} }
tmpMap[aclKey] = text
} else { } else {
tmpMap["SCHILY.xattr."+attr.Name] = attrString tmpMap["SCHILY.xattr."+attr.Name] = string(attr.Value)
} }
} }