A walk run while a dataset was unmounted wrote onto root's empty mount point, or "dropped" entries from it and forgot the ones on the data; a folder kept while empty below an unnamed mount was then swept whole once the disk mounted, with no warrant. Every walk is now judged as an apply is, a changed mount judges the folder afresh, and systemd .mount units and mounts seen are read for the first-sight rule. Widening a mask caps the entries it limited, so the owning group gains nothing. The watch lets go of removed folders, re-arms on lost notice, reads users under its lock and ends with its context. A drop that cannot read an ACL is counted, a broken folder's words carry no path, a grant's group that could not be left is tried again, and a folder's stat is bounded.
242 lines
7.8 KiB
Go
242 lines
7.8 KiB
Go
package apply
|
|
|
|
// The POSIX ACL a kept folder carries, as the kernel stores it (novox/hq ADR 0325 §5, issue 567).
|
|
//
|
|
// **Read and written as the kernel's own extended attribute, never through setfacl.** Linux keeps a file's access
|
|
// ACL in the attribute system.posix_acl_access and a folder's default ACL in system.posix_acl_default, each a
|
|
// little-endian header (version 2) followed by entries of a tag, a permission and an id. The engine reads and
|
|
// writes them on a descriptor it opened itself (fgetxattr, fsetxattr), so a name swapped for a link between the
|
|
// look and the change changes nothing, and no program is run per file of a library of millions.
|
|
//
|
|
// **What the mesh changes is narrow.** It adds or sets one named-user entry per uid it keeps, widens the mask to
|
|
// what the entries need, and on a file with no ACL yet writes the owning group's entry as exactly the group bits
|
|
// the mode had: owner, owning group, the owner's bits, others' bits and every other entry stay as found.
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// The attributes and the tags of the kernel's ACL format (linux/posix_acl_xattr.h).
|
|
const (
|
|
aclAccessXattr = "system.posix_acl_access"
|
|
aclDefaultXattr = "system.posix_acl_default"
|
|
aclVersion = 2
|
|
|
|
aclUserObj = 0x01
|
|
aclUser = 0x02
|
|
aclGroupObj = 0x04
|
|
aclGroup = 0x08
|
|
aclMask = 0x10
|
|
aclOther = 0x20
|
|
|
|
// aclUndefinedID is the id of an entry that names nobody: the owner, the owning group, the mask, others.
|
|
aclUndefinedID = 0xFFFFFFFF
|
|
)
|
|
|
|
// The permission bits of one entry.
|
|
const (
|
|
permRead = 4
|
|
permWrite = 2
|
|
permExec = 1
|
|
)
|
|
|
|
// aclEntry is one entry: its tag, its permission bits (rwx as 4, 2, 1) and, for a named user or group, its id.
|
|
type aclEntry struct {
|
|
Tag uint16
|
|
Perm uint16
|
|
ID uint32
|
|
}
|
|
|
|
// acl is a whole ACL as the kernel holds it.
|
|
type acl []aclEntry
|
|
|
|
// decodeACL reads an attribute's bytes. An attribute that is not version 2, or not whole entries, is refused.
|
|
func decodeACL(raw []byte) (acl, error) {
|
|
if len(raw) < 4 || (len(raw)-4)%8 != 0 {
|
|
return nil, fmt.Errorf("an ACL attribute of %d bytes is not a header and whole entries", len(raw))
|
|
}
|
|
if v := binary.LittleEndian.Uint32(raw); v != aclVersion {
|
|
return nil, fmt.Errorf("an ACL attribute of version %d, and the engine reads version %d", v, aclVersion)
|
|
}
|
|
out := make(acl, 0, (len(raw)-4)/8)
|
|
for at := 4; at < len(raw); at += 8 {
|
|
out = append(out, aclEntry{
|
|
Tag: binary.LittleEndian.Uint16(raw[at:]),
|
|
Perm: binary.LittleEndian.Uint16(raw[at+2:]),
|
|
ID: binary.LittleEndian.Uint32(raw[at+4:]),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// encode is the attribute's bytes, the entries in the order the kernel requires: by tag, then by id.
|
|
func (a acl) encode() []byte {
|
|
sorted := append(acl(nil), a...)
|
|
sort.SliceStable(sorted, func(i, j int) bool {
|
|
if sorted[i].Tag != sorted[j].Tag {
|
|
return sorted[i].Tag < sorted[j].Tag
|
|
}
|
|
return sorted[i].ID < sorted[j].ID
|
|
})
|
|
out := make([]byte, 4, 4+8*len(sorted))
|
|
binary.LittleEndian.PutUint32(out, aclVersion)
|
|
for _, e := range sorted {
|
|
var b [8]byte
|
|
binary.LittleEndian.PutUint16(b[0:], e.Tag)
|
|
binary.LittleEndian.PutUint16(b[2:], e.Perm)
|
|
binary.LittleEndian.PutUint32(b[4:], e.ID)
|
|
out = append(out, b[:]...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// minimalACL is the ACL a mode alone stands for: the owner's, the owning group's and others' bits.
|
|
func minimalACL(mode uint32) acl {
|
|
return acl{
|
|
{Tag: aclUserObj, Perm: uint16(mode>>6) & 7, ID: aclUndefinedID},
|
|
{Tag: aclGroupObj, Perm: uint16(mode>>3) & 7, ID: aclUndefinedID},
|
|
{Tag: aclOther, Perm: uint16(mode) & 7, ID: aclUndefinedID},
|
|
}
|
|
}
|
|
|
|
// find is the index of the entry with tag and id, or -1.
|
|
func (a acl) find(tag uint16, id uint32) int {
|
|
for i, e := range a {
|
|
if e.Tag == tag && (e.ID == id || (tag != aclUser && tag != aclGroup)) {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// perm is the bits of the entry with tag and id, and whether there is one.
|
|
func (a acl) perm(tag uint16, id uint32) (uint16, bool) {
|
|
if i := a.find(tag, id); i >= 0 {
|
|
return a[i].Perm, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// check refuses an ACL the kernel would: one owner's, owning group's and others' entry each, and a mask whenever
|
|
// a named entry is there.
|
|
func (a acl) check() error {
|
|
count := map[uint16]int{}
|
|
named := false
|
|
for _, e := range a {
|
|
count[e.Tag]++
|
|
if e.Tag == aclUser || e.Tag == aclGroup {
|
|
named = true
|
|
}
|
|
}
|
|
if count[aclUserObj] != 1 || count[aclGroupObj] != 1 || count[aclOther] != 1 || count[aclMask] > 1 {
|
|
return errors.New("an ACL without exactly one owner's, owning group's and others' entry")
|
|
}
|
|
if named && count[aclMask] != 1 {
|
|
return errors.New("an ACL with named entries and no mask")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keptACL is a with the mesh's entries made: for each uid of need its entry set to exactly its bits, and the mask
|
|
// widened by what those entries need — never narrowed, so an app's wider mask stays. A mask already there keeps
|
|
// what it gave every other entry, each capped at the old mask when it widens; with none yet (a file the mode alone described) the mask starts at the owning
|
|
// group's bits, which minimalACL gave its entry, so the owning group keeps exactly the right its group bits gave.
|
|
// Nothing else is changed.
|
|
func keptACL(a acl, need map[uint32]uint16) acl {
|
|
out := append(acl(nil), a...)
|
|
var widen uint16
|
|
for uid, bits := range need {
|
|
widen |= bits
|
|
if i := out.find(aclUser, uid); i >= 0 {
|
|
out[i].Perm = bits
|
|
} else {
|
|
out = append(out, aclEntry{Tag: aclUser, Perm: bits, ID: uid})
|
|
}
|
|
}
|
|
if i := out.find(aclMask, 0); i >= 0 {
|
|
old := out[i].Perm
|
|
out[i].Perm |= widen
|
|
if out[i].Perm != old {
|
|
// **A wider mask widens nobody else.** The owning group's entry and every named entry the mesh does
|
|
// not set here were limited by the old mask: a file made at 0644 under a default ACL carries the
|
|
// owning group's rwx with a mask of r--. Each is set to what it gave under the old mask, so its
|
|
// effective right stays exactly as it was.
|
|
for j := range out {
|
|
e := &out[j]
|
|
_, ours := need[e.ID]
|
|
if e.Tag == aclGroupObj || e.Tag == aclGroup || (e.Tag == aclUser && !ours) {
|
|
e.Perm &= old
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
if len(need) == 0 {
|
|
return out
|
|
}
|
|
group, _ := out.perm(aclGroupObj, 0)
|
|
return append(out, aclEntry{Tag: aclMask, Perm: group | widen, ID: aclUndefinedID})
|
|
}
|
|
|
|
// withoutUsers is a with the named-user entry of each uid of drop taken out, and nothing else changed: the mask
|
|
// stays as it was, so no other entry loses a right.
|
|
func withoutUsers(a acl, drop map[uint32]bool) acl {
|
|
out := make(acl, 0, len(a))
|
|
for _, e := range a {
|
|
if e.Tag == aclUser && drop[e.ID] {
|
|
continue
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// effective is the right a uid has through a, given the file's owner and mode: the owner's bits for the owner,
|
|
// else its named entry limited by the mask, else nothing a named entry gives (the owning group's and others'
|
|
// rights are not the mesh's to count on). An ACL with no mask limits nothing.
|
|
func effective(a acl, uid, owner uint32, mode uint32) uint16 {
|
|
if uid == owner {
|
|
return uint16(mode>>6) & 7
|
|
}
|
|
bits, ok := a.perm(aclUser, uid)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
if mask, has := a.perm(aclMask, 0); has {
|
|
bits &= mask
|
|
}
|
|
return bits
|
|
}
|
|
|
|
// needOn is what a use needs on a folder (read and enter; with read-write, write too) or on a file (read; with
|
|
// read-write, write): entering means nothing on a file, and an execute bit given to data would be a right nobody
|
|
// asked for.
|
|
func needOn(readWrite, folder bool) uint16 {
|
|
bits := uint16(permRead)
|
|
if readWrite {
|
|
bits |= permWrite
|
|
}
|
|
if folder {
|
|
bits |= permExec
|
|
}
|
|
return bits
|
|
}
|
|
|
|
// permWords is bits as ls writes them: "rwx", "r-x".
|
|
func permWords(bits uint16) string {
|
|
out := []byte("---")
|
|
if bits&permRead != 0 {
|
|
out[0] = 'r'
|
|
}
|
|
if bits&permWrite != 0 {
|
|
out[1] = 'w'
|
|
}
|
|
if bits&permExec != 0 {
|
|
out[2] = 'x'
|
|
}
|
|
return string(out)
|
|
}
|