A container was still created over an unmounted mount point, so its writes landed on the filesystem below; a folder moved to another disk was kept on the old warrant; and entries left by a drop or a move that could not run lived only in a log line. Every container mounting an access whose disk is away now waits, the warrant holds only for the filesystem it was carried out on (the wait names the new one, path-free), entries left on a filesystem are recorded per mount and taken out when it is back, and every leftover is said as unhealthy until it is. A hung stat is shared per path, and a widened mask takes from other entries only the bits it adds.
243 lines
8.0 KiB
Go
243 lines
8.0 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: one holding a bit the widening adds loses that bit alone; 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 added := out[i].Perm &^ old; added != 0 {
|
|
// **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--. One mask serves them all, so an entry holding a bit the mask
|
|
// now adds cannot keep its effective right without losing that bit: exactly those bits are taken off
|
|
// it, and nothing else of it changes. An entry holding none of them is not touched at all.
|
|
for j := range out {
|
|
e := &out[j]
|
|
_, ours := need[e.ID]
|
|
if (e.Tag == aclGroupObj || e.Tag == aclGroup || (e.Tag == aclUser && !ours)) && e.Perm&added != 0 {
|
|
e.Perm &^= added
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
}
|