]> ruderich.org/simon Gitweb - nsscash/nsscash.git/commitdiff
Add support for group files
authorSimon Ruderich <simon@ruderich.org>
Sat, 8 Jun 2019 13:49:33 +0000 (15:49 +0200)
committerSimon Ruderich <simon@ruderich.org>
Sat, 8 Jun 2019 13:49:33 +0000 (15:49 +0200)
13 files changed:
.gitignore
config.go
file.go
group.go [new file with mode: 0644]
group_test.go [new file with mode: 0644]
main.go
nss/Makefile
nss/cash.h
nss/cash_nss.h
nss/gr.c [new file with mode: 0644]
nss/pw.c
nss/tests/gr.c [new file with mode: 0644]
nss/tests/group [new file with mode: 0644]

index 3f82d1b9f6c425538f8b1c83b2afe3536f792862..263abf9bad3681d5552bce755814a0b4f5daf01a 100644 (file)
@@ -1,6 +1,8 @@
 /filetype_string.go
 /nss/libcash_test.so
 /nss/libnss_cash.so.2
+/nss/tests/gr
+/nss/tests/group.nsscash
 /nss/tests/passwd.nsscash
 /nss/tests/pw
 /nsscash
index 8e9a6308124a4d74adb7f8e2dde1ad82f16b84bf..99b27cff35e423e607d541bbbea2fe1c9f1a0db2 100644 (file)
--- a/config.go
+++ b/config.go
@@ -42,6 +42,7 @@ type FileType int
 const (
        FileTypePlain FileType = iota
        FileTypePasswd
+       FileTypeGroup
 )
 
 func (t *FileType) UnmarshalText(text []byte) error {
@@ -50,6 +51,8 @@ func (t *FileType) UnmarshalText(text []byte) error {
                *t = FileTypePlain
        case "passwd":
                *t = FileTypePasswd
+       case "group":
+               *t = FileTypeGroup
        default:
                return fmt.Errorf("invalid file type %q", text)
        }
diff --git a/file.go b/file.go
index b48d0751ae3bad521ea36dd39ec2327a6ec5168c..33d71c14c152c144dc4e0e3cf04e758c0cee4858 100644 (file)
--- a/file.go
+++ b/file.go
@@ -92,6 +92,22 @@ func fetchFile(file *File, state *State) error {
                }
                file.body = x.Bytes()
 
+       } else if file.Type == FileTypeGroup {
+               grs, err := ParseGroups(bytes.NewReader(body))
+               if err != nil {
+                       return err
+               }
+               if len(grs) == 0 {
+                       return fmt.Errorf("refusing to use empty group file")
+               }
+
+               var x bytes.Buffer
+               err = SerializeGroups(&x, grs)
+               if err != nil {
+                       return err
+               }
+               file.body = x.Bytes()
+
        } else {
                return fmt.Errorf("unsupported file type %v", file.Type)
        }
diff --git a/group.go b/group.go
new file mode 100644 (file)
index 0000000..02502f6
--- /dev/null
+++ b/group.go
@@ -0,0 +1,262 @@
+// Parse /etc/group files and serialize them
+
+// Copyright (C) 2019  Simon Ruderich
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package main
+
+import (
+       "bufio"
+       "bytes"
+       "encoding/binary"
+       "fmt"
+       "io"
+       "sort"
+       "strconv"
+       "strings"
+
+       "github.com/pkg/errors"
+)
+
+// Version written in SerializeGroups()
+const GroupVersion = 1
+
+type Group struct {
+       Name    string
+       Passwd  string
+       Gid     uint64
+       Members []string
+}
+type GroupKey struct {
+       Name    string
+       Passwd  string
+       Gid     uint64
+       Members string
+}
+
+func toKey(g Group) GroupKey {
+       return GroupKey{
+               Name:    g.Name,
+               Passwd:  g.Passwd,
+               Gid:     g.Gid,
+               Members: strings.Join(g.Members, ","),
+       }
+}
+
+// ParseGroups parses a file in the format of /etc/group and returns all
+// entries as Group structs.
+func ParseGroups(r io.Reader) ([]Group, error) {
+       var res []Group
+
+       s := bufio.NewScanner(r)
+       for s.Scan() {
+               t := s.Text()
+
+               x := strings.Split(t, ":")
+               if len(x) != 4 {
+                       return nil, fmt.Errorf("invalid line %q", t)
+               }
+
+               gid, err := strconv.ParseUint(x[2], 10, 64)
+               if err != nil {
+                       return nil, errors.Wrapf(err, "invalid gid in line %q", t)
+               }
+
+               var members []string
+               // No members must result in empty slice, not slice with the
+               // empty string
+               if x[3] != "" {
+                       members = strings.Split(x[3], ",")
+               }
+               res = append(res, Group{
+                       Name:    x[0],
+                       Passwd:  x[1],
+                       Gid:     gid,
+                       Members: members,
+               })
+       }
+       err := s.Err()
+       if err != nil {
+               return nil, err
+       }
+
+       return res, nil
+}
+
+func SerializeGroup(g Group) []byte {
+       le := binary.LittleEndian
+
+       // Concatenate all (NUL-terminated) strings and store the offsets
+       var mems bytes.Buffer
+       var mems_off []uint16
+       for _, m := range g.Members {
+               mems_off = append(mems_off, uint16(mems.Len()))
+               mems.Write([]byte(m))
+               mems.WriteByte(0)
+       }
+       var data bytes.Buffer
+       data.Write([]byte(g.Name))
+       data.WriteByte(0)
+       offPasswd := uint16(data.Len())
+       data.Write([]byte(g.Passwd))
+       data.WriteByte(0)
+       // Padding to align the following uint16
+       if data.Len()%2 != 0 {
+               data.WriteByte(0)
+       }
+       offMemOff := uint16(data.Len())
+       // Offsets for group members
+       offMem := offMemOff + 2*uint16(len(mems_off))
+       for _, o := range mems_off {
+               tmp := make([]byte, 2)
+               le.PutUint16(tmp, offMem+o)
+               data.Write(tmp)
+       }
+       // And the group members concatenated as above
+       data.Write(mems.Bytes())
+       size := uint16(data.Len())
+
+       var res bytes.Buffer // serialized result
+
+       id := make([]byte, 8)
+       // gid
+       le.PutUint64(id, g.Gid)
+       res.Write(id)
+
+       off := make([]byte, 2)
+       // off_passwd
+       le.PutUint16(off, offPasswd)
+       res.Write(off)
+       // off_mem_off
+       le.PutUint16(off, offMemOff)
+       res.Write(off)
+       // mem_count
+       le.PutUint16(off, uint16(len(g.Members)))
+       res.Write(off)
+       // data_size
+       le.PutUint16(off, size)
+       res.Write(off)
+
+       res.Write(data.Bytes())
+       // We must pad each entry so that all uint64 at the beginning of the
+       // struct are 8 byte aligned
+       l := res.Len()
+       if l%8 != 0 {
+               for i := 0; i < 8-l%8; i++ {
+                       res.WriteByte(0)
+               }
+       }
+
+       return res.Bytes()
+}
+
+func SerializeGroups(w io.Writer, grs []Group) error {
+       // Serialize groups and store offsets
+       var data bytes.Buffer
+       offsets := make(map[GroupKey]uint64)
+       for _, g := range grs {
+               // TODO: warn about duplicate entries
+               offsets[toKey(g)] = uint64(data.Len())
+               data.Write(SerializeGroup(g))
+       }
+
+       // Copy to prevent sorting from modifying the argument
+       sorted := make([]Group, len(grs))
+       copy(sorted, grs)
+
+       le := binary.LittleEndian
+       tmp := make([]byte, 8)
+
+       // Create index "sorted" in input order, used when iterating over all
+       // passwd entries (getgrent_r); keeping the original order makes
+       // debugging easier
+       var indexOrig bytes.Buffer
+       for _, g := range grs {
+               le.PutUint64(tmp, offsets[toKey(g)])
+               indexOrig.Write(tmp)
+       }
+
+       // Create index sorted after id
+       var indexId bytes.Buffer
+       sort.Slice(sorted, func(i, j int) bool {
+               return sorted[i].Gid < sorted[j].Gid
+       })
+       for _, g := range sorted {
+               le.PutUint64(tmp, offsets[toKey(g)])
+               indexId.Write(tmp)
+       }
+
+       // Create index sorted after name
+       var indexName bytes.Buffer
+       sort.Slice(sorted, func(i, j int) bool {
+               return sorted[i].Name < sorted[j].Name
+       })
+       for _, g := range sorted {
+               le.PutUint64(tmp, offsets[toKey(g)])
+               indexName.Write(tmp)
+       }
+
+       // Sanity check
+       if indexOrig.Len() != indexId.Len() ||
+               indexId.Len() != indexName.Len() {
+               return fmt.Errorf("indexes have inconsistent length")
+       }
+
+       // Write result
+
+       // magic
+       w.Write([]byte("NSS-CASH"))
+       // version
+       le.PutUint64(tmp, GroupVersion)
+       w.Write(tmp)
+       // count
+       le.PutUint64(tmp, uint64(len(grs)))
+       w.Write(tmp)
+       // off_orig_index
+       offset := uint64(0)
+       le.PutUint64(tmp, offset)
+       w.Write(tmp)
+       // off_id_index
+       offset += uint64(indexOrig.Len())
+       le.PutUint64(tmp, offset)
+       w.Write(tmp)
+       // off_name_index
+       offset += uint64(indexId.Len())
+       le.PutUint64(tmp, offset)
+       w.Write(tmp)
+       // off_data
+       offset += uint64(indexName.Len())
+       le.PutUint64(tmp, offset)
+       w.Write(tmp)
+
+       _, err := indexOrig.WriteTo(w)
+       if err != nil {
+               return err
+       }
+       _, err = indexId.WriteTo(w)
+       if err != nil {
+               return err
+       }
+       _, err = indexName.WriteTo(w)
+       if err != nil {
+               return err
+       }
+       _, err = data.WriteTo(w)
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
diff --git a/group_test.go b/group_test.go
new file mode 100644 (file)
index 0000000..bc91eb3
--- /dev/null
@@ -0,0 +1,84 @@
+// Copyright (C) 2019  Simon Ruderich
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package main
+
+import (
+       "reflect"
+       "strings"
+       "testing"
+)
+
+func TestParseGroups(t *testing.T) {
+       tests := []struct {
+               data string
+               exp  []Group
+       }{
+               {
+                       "",
+                       nil,
+               },
+               {
+                       "root:x:0:\n",
+                       []Group{
+                               Group{
+                                       Name:    "root",
+                                       Passwd:  "x",
+                                       Gid:     0,
+                                       Members: nil,
+                               },
+                       },
+               },
+               {
+                       "root:x:0:foo\n",
+                       []Group{
+                               Group{
+                                       Name:   "root",
+                                       Passwd: "x",
+                                       Gid:    0,
+                                       Members: []string{
+                                               "foo",
+                                       },
+                               },
+                       },
+               },
+               {
+                       "root:x:0:foo,bar\n",
+                       []Group{
+                               Group{
+                                       Name:   "root",
+                                       Passwd: "x",
+                                       Gid:    0,
+                                       Members: []string{
+                                               "foo",
+                                               "bar",
+                                       },
+                               },
+                       },
+               },
+       }
+
+       for n, tc := range tests {
+               res, err := ParseGroups(strings.NewReader(tc.data))
+               if err != nil {
+                       t.Errorf("%d: err = %v, want %v",
+                               n, res, nil)
+               }
+               if !reflect.DeepEqual(res, tc.exp) {
+                       t.Errorf("%d: res = %v, want %v",
+                               n, res, tc.exp)
+               }
+       }
+}
diff --git a/main.go b/main.go
index 7a8755cac1df9d5cd4ffd2ff259b2e15c4e95b07..b10f57e55b92776a18ff5dfa76fb7ed25cc5b39c 100644 (file)
--- a/main.go
+++ b/main.go
@@ -94,6 +94,15 @@ func main() {
                        if err != nil {
                                log.Fatal(err)
                        }
+               } else if t == FileTypeGroup {
+                       grs, err := ParseGroups(bytes.NewReader(src))
+                       if err != nil {
+                               log.Fatal(err)
+                       }
+                       err = SerializeGroups(&x, grs)
+                       if err != nil {
+                               log.Fatal(err)
+                       }
                } else {
                        log.Fatalf("unsupported file type %v", t)
                }
index 0ce909345aec31410edd330806f82cea50d8c325..f103dafc773d4f4e8b075be4f4141995402f1c81 100644 (file)
@@ -13,30 +13,35 @@ all: libnss_cash.so.2
 
 clean:
        rm -f libnss_cash.so.2 \
-           libcash_test.so tests/pw tests/passwd.nsscash
+           libcash_test.so tests/gr tests/pw \
+           tests/group.nsscash tests/passwd.nsscash
 
 libnss_cash.so.2 libcash_test.so: $(wildcard *.c) $(wildcard *.h)
        $(CC) -o $@ -shared -fPIC -Wl,-soname,$@ \
                $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) \
-               file.c pw.c search.c \
+               file.c gr.c pw.c search.c \
                $(LDLIBS)
 
 
 # Tests
 
-test: tests/pw tests/passwd.nsscash
+test: tests/gr tests/pw tests/group.nsscash tests/passwd.nsscash
+       LD_LIBRARY_PATH=. LD_PRELOD= ./tests/gr
        LD_LIBRARY_PATH=. LD_PRELOD= ./tests/pw
 
-tests/pw: tests/pw.c libcash_test.so
+tests/%: tests/%.c libcash_test.so
        $(CC) -o $@ $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) \
                $(TEST_CFLAGS) $(TEST_LDFLAGS) -L. \
                $< $(LDLIBS) -lcash_test -lasan
 
 tests/passwd.nsscash: tests/passwd
        ../nsscash convert passwd $< $@
+tests/group.nsscash: tests/group
+       ../nsscash convert group $< $@
 
 libcash_test.so: CFLAGS += $(TEST_CFLAGS)
-libcash_test.so: CPPFLAGS += -DNSSCASH_PASSWD_FILE='"./tests/passwd.nsscash"'
+libcash_test.so: CPPFLAGS += -DNSSCASH_GROUP_FILE='"./tests/group.nsscash"' \
+                             -DNSSCASH_PASSWD_FILE='"./tests/passwd.nsscash"'
 libcash_test.so: LDFLAGS += $(TEST_LDFLAGS)
 
 .PHONY: all clean test
index b6d92a6a6c89f12d2550cc99ec33ed1dd183ba78..d227655b6ef2ce4b7612a3ca2d822a8b3e9f5617 100644 (file)
@@ -31,6 +31,9 @@
 #ifndef NSSCASH_PASSWD_FILE
 # define NSSCASH_PASSWD_FILE "/etc/passwd.nsscash"
 #endif
+#ifndef NSSCASH_GROUP_FILE
+# define NSSCASH_GROUP_FILE "/etc/group.nsscash"
+#endif
 
 
 // Global structs
index c21142c3f9fe6a6b519272609517d602ca33e063..741d411d36f9776adcde74494ffc43d786d16096 100644 (file)
 #ifndef CASH_NSS_H
 #define CASH_NSS_H
 
+#include <grp.h>
 #include <nss.h>
 #include <pwd.h>
 
 
+// struct passwd
 enum nss_status _nss_cash_setpwent(int);
 enum nss_status _nss_cash_endpwent(void);
 enum nss_status _nss_cash_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop);
 enum nss_status _nss_cash_getpwuid_r(uid_t uid, struct passwd *result, char *buffer, size_t buflen, int *errnop);
 enum nss_status _nss_cash_getpwnam_r(const char *name, struct passwd *result, char *buffer, size_t buflen, int *errnop);
 
+// struct group
+enum nss_status _nss_cash_setgrent(int);
+enum nss_status _nss_cash_endgrent(void);
+enum nss_status _nss_cash_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop);
+enum nss_status _nss_cash_getgrgid_r(gid_t gid, struct group *result, char *buffer, size_t buflen, int *errnop);
+enum nss_status _nss_cash_getgrnam_r(const char *name, struct group *result, char *buffer, size_t buflen, int *errnop);
+
 #endif
diff --git a/nss/gr.c b/nss/gr.c
new file mode 100644 (file)
index 0000000..c49ef47
--- /dev/null
+++ b/nss/gr.c
@@ -0,0 +1,196 @@
+/*
+ * Handle group entries via struct group
+ *
+ * Copyright (C) 2019  Simon Ruderich
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <pthread.h>
+
+#include "cash.h"
+#include "cash_nss.h"
+#include "file.h"
+#include "search.h"
+
+
+// NOTE: This file is very similar to pw.c, keep in sync!
+
+struct group_entry {
+    uint64_t gid;
+
+    //       off_name = 0
+    uint16_t off_passwd;
+    uint16_t off_mem_off;
+
+    uint16_t mem_count; // group member count
+
+    /*
+     * Data contains all strings (name, passwd) concatenated, with their
+     * trailing NUL. The off_* variables point to beginning of each string.
+     *
+     * After that the offsets of the members of the group are stored as
+     * mem_count uint16_t values, followed by the member names concatenated as
+     * with the strings above.
+     *
+     * All offsets are relative to the beginning of data.
+     */
+    uint16_t data_size;
+    char data[];
+} __attribute__((packed));
+
+static bool entry_to_group(const struct group_entry *e, struct group *g, char *tmp, size_t space) {
+    const size_t mem_size = (size_t)(e->mem_count + 1) * sizeof(char *);
+
+    if (space < e->data_size + mem_size) {
+        return false;
+    }
+
+    char **groups = (char **)tmp;
+
+    const uint16_t *offs_mem = (const uint16_t *)(e->data + e->off_mem_off);
+    for (uint16_t i = 0; i < e->mem_count; i++) {
+        groups[i] = tmp + mem_size + offs_mem[i];
+    }
+    groups[e->mem_count] = NULL;
+
+    // This unnecessarily copies offs_mem[] as well but keeps the code simpler
+    // and the meaning of variables consistent with pw.c
+    memcpy(tmp + mem_size, e->data, e->data_size);
+
+    g->gr_gid = (gid_t)e->gid;
+    g->gr_name = tmp + mem_size + 0;
+    g->gr_passwd = tmp + mem_size + e->off_passwd;
+    g->gr_mem = groups;
+
+    return true;
+}
+
+
+static struct file static_file = {
+    .fd = -1,
+};
+static pthread_mutex_t static_file_lock = PTHREAD_MUTEX_INITIALIZER;
+
+enum nss_status _nss_cash_setgrent(int x) {
+    (void)x;
+
+    pthread_mutex_lock(&static_file_lock);
+    // Unmap is necessary to detect changes when the file was replaced on
+    // disk
+    unmap_file(&static_file);
+    // getgrent_r will open the file if necessary when called
+    pthread_mutex_unlock(&static_file_lock);
+
+    return NSS_STATUS_SUCCESS;
+}
+
+enum nss_status _nss_cash_endgrent(void) {
+    pthread_mutex_lock(&static_file_lock);
+    unmap_file(&static_file);
+    pthread_mutex_unlock(&static_file_lock);
+
+    return NSS_STATUS_SUCCESS;
+}
+
+static enum nss_status internal_getgrent_r(struct group *result, char *buffer, size_t buflen) {
+    // First call to getgrent_r, load file from disk
+    if (static_file.header == NULL) {
+        if (!map_file(NSSCASH_GROUP_FILE, &static_file)) {
+            return NSS_STATUS_UNAVAIL;
+        }
+    }
+
+    const struct header *h = static_file.header;
+    // End of "file", stop
+    if (static_file.next_index >= h->count) {
+        errno = ENOENT;
+        return NSS_STATUS_NOTFOUND;
+    }
+
+    uint64_t *off_orig = (uint64_t *)(h->data + h->off_orig_index);
+    const char *e = h->data + h->off_data + off_orig[static_file.next_index];
+    if (!entry_to_group((struct group_entry *)e, result, buffer, buflen)) {
+        errno = ERANGE;
+        return NSS_STATUS_TRYAGAIN;
+    }
+    static_file.next_index++;
+
+    return NSS_STATUS_SUCCESS;
+}
+enum nss_status _nss_cash_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) {
+    pthread_mutex_lock(&static_file_lock);
+    enum nss_status s = internal_getgrent_r(result, buffer, buflen);
+    pthread_mutex_unlock(&static_file_lock);
+    if (s != NSS_STATUS_SUCCESS) {
+        *errnop = errno;
+    }
+    return s;
+}
+
+
+static enum nss_status internal_getgr(struct search_key *key, struct group *result, char *buffer, size_t buflen, int *errnop) {
+    struct file f;
+    if (!map_file(NSSCASH_GROUP_FILE, &f)) {
+        *errnop = errno;
+        return NSS_STATUS_UNAVAIL;
+    }
+    const struct header *h = f.header;
+
+    key->data = h->data + h->off_data;
+    uint64_t off_index = (key->id != NULL)
+                       ? h->off_id_index
+                       : h->off_name_index;
+    uint64_t *off = search(key, h->data + off_index, h->count);
+    if (off == NULL) {
+        unmap_file(&f);
+        errno = ENOENT;
+        *errnop = errno;
+        return NSS_STATUS_NOTFOUND;
+    }
+
+    const char *e = h->data + h->off_data + *off;
+    if (!entry_to_group((struct group_entry *)e, result, buffer, buflen)) {
+        unmap_file(&f);
+        errno = ERANGE;
+        *errnop = errno;
+        return NSS_STATUS_TRYAGAIN;
+    }
+
+    unmap_file(&f);
+    return NSS_STATUS_SUCCESS;
+}
+
+enum nss_status _nss_cash_getgrgid_r(gid_t gid, struct group *result, char *buffer, size_t buflen, int *errnop) {
+    uint64_t id = (uint64_t)gid;
+    struct search_key key = {
+        .id = &id,
+        .offset = offsetof(struct group_entry, gid),
+    };
+    return internal_getgr(&key, result, buffer, buflen, errnop);
+}
+
+enum nss_status _nss_cash_getgrnam_r(const char *name, struct group *result, char *buffer, size_t buflen, int *errnop) {
+    struct search_key key = {
+        .name = name,
+        .offset = sizeof(struct group_entry), // name is first value in data[]
+    };
+    return internal_getgr(&key, result, buffer, buflen, errnop);
+}
index d2c1bfa4acd793b12e5cbdf084de22ddefe6ed33..1dd9d3b5c05279195af7ccbc2497035305735565 100644 (file)
--- a/nss/pw.c
+++ b/nss/pw.c
@@ -31,6 +31,8 @@
 #include "search.h"
 
 
+// NOTE: This file is very similar to gr.c, keep in sync!
+
 struct passwd_entry {
     uint64_t uid;
     uint64_t gid;
diff --git a/nss/tests/gr.c b/nss/tests/gr.c
new file mode 100644 (file)
index 0000000..e4aee5b
--- /dev/null
@@ -0,0 +1,328 @@
+/*
+ * Tests for the NSS cash module
+ *
+ * Copyright (C) 2019  Simon Ruderich
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <limits.h>
+
+#include "../cash_nss.h"
+
+
+static void test_getgrent(void) {
+    struct group g;
+    enum nss_status s;
+    char tmp[1024];
+    char tmp_small[10];
+    int errnop = 0;
+
+    // Test one setgrent/getgrent/endgrent round
+
+    s = _nss_cash_setgrent(0);
+    assert(s == NSS_STATUS_SUCCESS);
+
+    // Multiple calls with too small buffer don't advance any internal indices
+    s = _nss_cash_getgrent_r(&g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+    s = _nss_cash_getgrent_r(&g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+    s = _nss_cash_getgrent_r(&g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "root"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 0);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "daemon"));
+    assert(g.gr_gid == 1);
+    assert(g.gr_mem != NULL);
+    assert(!strcmp(g.gr_mem[0], "andariel"));
+    assert(!strcmp(g.gr_mem[1], "duriel"));
+    assert(!strcmp(g.gr_mem[2], "mephisto"));
+    assert(!strcmp(g.gr_mem[3], "diablo"));
+    assert(!strcmp(g.gr_mem[4], "baal"));
+    assert(g.gr_mem[5] == NULL);
+    for (int i = 0; i < 21; i++) {
+        s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+        assert(s == NSS_STATUS_SUCCESS);
+    }
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "www-data"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 33);
+    assert(g.gr_mem != NULL);
+    assert(!strcmp(g.gr_mem[0], "nobody"));
+    assert(g.gr_mem[1] == NULL);
+    for (int i = 0; i < 29; i++) {
+        s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+        assert(s == NSS_STATUS_SUCCESS);
+    }
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "postfix"));
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "postdrop"));
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND);
+    assert(errnop == ENOENT);
+
+    s = _nss_cash_endgrent();
+    assert(s == NSS_STATUS_SUCCESS);
+
+
+    // Test proper reset
+
+    s = _nss_cash_setgrent(0);
+    assert(s == NSS_STATUS_SUCCESS);
+
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "root"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 0);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_endgrent();
+    assert(s == NSS_STATUS_SUCCESS);
+
+
+    // Test proper reset the 2nd
+
+    s = _nss_cash_setgrent(0);
+    assert(s == NSS_STATUS_SUCCESS);
+
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "root"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 0);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_endgrent();
+    assert(s == NSS_STATUS_SUCCESS);
+
+
+    // Test many rounds to check for open file leaks
+    for (int i = 0; i < 10000; i++) {
+        s = _nss_cash_setgrent(0);
+        assert(s == NSS_STATUS_SUCCESS);
+
+        s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+        assert(s == NSS_STATUS_SUCCESS);
+        assert(!strcmp(g.gr_name, "root"));
+
+        s = _nss_cash_endgrent();
+        assert(s == NSS_STATUS_SUCCESS);
+    }
+
+
+    // Test with cash file is not present
+
+    assert(rename("tests/group.nsscash", "tests/group.nsscash.tmp") == 0);
+    s = _nss_cash_setgrent(0);
+    assert(s == NSS_STATUS_SUCCESS);
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_UNAVAIL);
+    assert(errnop == ENOENT);
+    s = _nss_cash_getgrent_r(&g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_UNAVAIL);
+    assert(errnop == ENOENT);
+    s = _nss_cash_endgrent();
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(rename("tests/group.nsscash.tmp", "tests/group.nsscash") == 0);
+}
+
+static void test_getgrgid(void) {
+    struct group g;
+    enum nss_status s;
+    char tmp[1024];
+    char tmp_small[10];
+    int errnop = 0;
+
+    s = _nss_cash_getgrgid_r(0, &g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+    s = _nss_cash_getgrgid_r(14, &g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND); // 14 does not exist
+    assert(errnop == ENOENT);
+    s = _nss_cash_getgrgid_r(65534, &g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+
+    s = _nss_cash_getgrgid_r(0, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "root"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 0);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrgid_r(1, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "daemon"));
+    assert(g.gr_gid == 1);
+    assert(g.gr_mem != NULL);
+    assert(!strcmp(g.gr_mem[0], "andariel"));
+    assert(!strcmp(g.gr_mem[1], "duriel"));
+    assert(!strcmp(g.gr_mem[2], "mephisto"));
+    assert(!strcmp(g.gr_mem[3], "diablo"));
+    assert(!strcmp(g.gr_mem[4], "baal"));
+    assert(g.gr_mem[5] == NULL);
+
+    s = _nss_cash_getgrgid_r(11, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND);
+    assert(errnop == ENOENT);
+
+    s = _nss_cash_getgrgid_r(103, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "systemd-network"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 103);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrgid_r(107, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "kvm"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 107);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrgid_r(65534, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "nogroup"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 65534);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrgid_r(INT_MAX, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND);
+    assert(errnop == ENOENT);
+
+
+    // Test with cash file is not present
+
+    assert(rename("tests/group.nsscash", "tests/group.nsscash.tmp") == 0);
+    s = _nss_cash_getgrgid_r(0, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_UNAVAIL);
+    assert(errnop == ENOENT);
+    s = _nss_cash_getgrgid_r(14, &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_UNAVAIL);
+    assert(errnop == ENOENT);
+    assert(rename("tests/group.nsscash.tmp", "tests/group.nsscash") == 0);
+}
+
+static void test_getgrnam(void) {
+    struct group g;
+    enum nss_status s;
+    char tmp[1024];
+    char tmp_small[10];
+    int errnop = 0;
+
+    s = _nss_cash_getgrnam_r("root", &g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+    s = _nss_cash_getgrnam_r("nope", &g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND); // does not exist
+    assert(errnop == ENOENT);
+    s = _nss_cash_getgrnam_r("nogroup", &g, tmp_small, sizeof(tmp_small), &errnop);
+    assert(s == NSS_STATUS_TRYAGAIN);
+    assert(errnop == ERANGE);
+
+    s = _nss_cash_getgrnam_r("root", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "root"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 0);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrnam_r("daemon", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "daemon"));
+    assert(g.gr_gid == 1);
+    assert(g.gr_mem != NULL);
+    assert(!strcmp(g.gr_mem[0], "andariel"));
+    assert(!strcmp(g.gr_mem[1], "duriel"));
+    assert(!strcmp(g.gr_mem[2], "mephisto"));
+    assert(!strcmp(g.gr_mem[3], "diablo"));
+    assert(!strcmp(g.gr_mem[4], "baal"));
+    assert(g.gr_mem[5] == NULL);
+
+    s = _nss_cash_getgrnam_r("nope2", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND);
+    assert(errnop == ENOENT);
+
+    s = _nss_cash_getgrnam_r("systemd-network", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "systemd-network"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 103);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrnam_r("postfix", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_SUCCESS);
+    assert(!strcmp(g.gr_name, "postfix"));
+    assert(!strcmp(g.gr_passwd, "x"));
+    assert(g.gr_gid == 114);
+    assert(g.gr_mem != NULL);
+    assert(g.gr_mem[0] == NULL);
+
+    s = _nss_cash_getgrnam_r("", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_NOTFOUND);
+    assert(errnop == ENOENT);
+
+
+    // Test with cash file is not present
+
+    assert(rename("tests/group.nsscash", "tests/group.nsscash.tmp") == 0);
+    s = _nss_cash_getgrnam_r("root", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_UNAVAIL);
+    assert(errnop == ENOENT);
+    s = _nss_cash_getgrnam_r("nope", &g, tmp, sizeof(tmp), &errnop);
+    assert(s == NSS_STATUS_UNAVAIL);
+    assert(errnop == ENOENT);
+    assert(rename("tests/group.nsscash.tmp", "tests/group.nsscash") == 0);
+}
+
+int main(void) {
+    test_getgrent();
+    test_getgrgid();
+    test_getgrnam();
+
+    return EXIT_SUCCESS;
+}
diff --git a/nss/tests/group b/nss/tests/group
new file mode 100644 (file)
index 0000000..41b28b5
--- /dev/null
@@ -0,0 +1,55 @@
+root:x:0:
+daemon:x:1:andariel,duriel,mephisto,diablo,baal
+bin:x:2:
+sys:x:3:
+adm:x:4:
+tty:x:5:
+disk:x:6:
+lp:x:7:
+mail:x:8:
+news:x:9:
+uucp:x:10:
+man:x:12:
+proxy:x:13:
+kmem:x:15:
+dialout:x:20:
+fax:x:21:
+voice:x:22:
+cdrom:x:24:
+floppy:x:25:
+tape:x:26:
+sudo:x:27:
+audio:x:29:
+dip:x:30:
+www-data:x:33:nobody
+backup:x:34:
+operator:x:37:
+list:x:38:
+irc:x:39:
+src:x:40:
+gnats:x:41:
+shadow:x:42:
+utmp:x:43:
+video:x:44:
+sasl:x:45:
+plugdev:x:46:
+staff:x:50:
+games:x:60:
+users:x:100:
+nogroup:x:65534:
+systemd-journal:x:101:
+systemd-timesync:x:102:
+systemd-network:x:103:
+systemd-resolve:x:104:
+messagebus:x:105:
+input:x:106:
+kvm:x:107:
+render:x:108:
+crontab:x:109:
+netdev:x:110:
+ssh:x:111:
+systemd-coredump:x:999:
+_cvsadmin:x:112:
+ssl-cert:x:113:
+postfix:x:114:
+postdrop:x:115: