/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
const (
FileTypePlain FileType = iota
FileTypePasswd
+ FileTypeGroup
)
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)
}
}
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)
}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+ }
+ }
+}
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)
}
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
#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
#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
--- /dev/null
+/*
+ * 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);
+}
#include "search.h"
+// NOTE: This file is very similar to gr.c, keep in sync!
+
struct passwd_entry {
uint64_t uid;
uint64_t gid;
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+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: