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) }