A module's containers ran as whatever their image chose and shared folders were made writable by putting accounts in the folder's group (ADR 0312), which ADR 0325 supersedes. The engine now makes each declared module identity as a system account with the controller's numbers, runs a container declaring `identity` as it, and keeps each placed shared folder usable through one POSIX ACL entry per user, set through the kernel's xattrs on descriptors it opened itself. A folder holding data is kept only after the operator's warrant (the take act); a missing folder is made where its data lives and waits where its disk is not; root's data is never kept. A watch and a bounded sweep put back what an app narrows. The group grant is removed, and what it put an account in is taken back. Reuses the descriptor walks, the create act, the missing-path wait and the warrant- backed act message from the paused issue 567 branch.
228 lines
7.1 KiB
Go
228 lines
7.1 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; 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 {
|
|
out[i].Perm |= widen
|
|
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)
|
|
}
|