From: Simon Ruderich Date: Sat, 8 Jun 2019 13:49:33 +0000 (+0200) Subject: Add support for group files X-Git-Tag: 0.1~87 X-Git-Url: https://ruderich.org/simon/gitweb/?a=commitdiff_plain;h=839f07d7b3130efc613d7d3fa8ed71a7d8d5fd7f;p=nsscash%2Fnsscash.git Add support for group files --- diff --git a/.gitignore b/.gitignore index 3f82d1b..263abf9 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/config.go b/config.go index 8e9a630..99b27cf 100644 --- 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 b48d075..33d71c1 100644 --- 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 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 . + +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 index 0000000..bc91eb3 --- /dev/null +++ b/group_test.go @@ -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 . + +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 7a8755c..b10f57e 100644 --- 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) } diff --git a/nss/Makefile b/nss/Makefile index 0ce9093..f103daf 100644 --- a/nss/Makefile +++ b/nss/Makefile @@ -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 diff --git a/nss/cash.h b/nss/cash.h index b6d92a6..d227655 100644 --- a/nss/cash.h +++ b/nss/cash.h @@ -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 diff --git a/nss/cash_nss.h b/nss/cash_nss.h index c21142c..741d411 100644 --- a/nss/cash_nss.h +++ b/nss/cash_nss.h @@ -20,14 +20,23 @@ #ifndef CASH_NSS_H #define CASH_NSS_H +#include #include #include +// 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 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 . + */ + +#include +#include +#include +#include +#include + +#include + +#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); +} diff --git a/nss/pw.c b/nss/pw.c index d2c1bfa..1dd9d3b 100644 --- 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 index 0000000..e4aee5b --- /dev/null +++ b/nss/tests/gr.c @@ -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 . + */ + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..41b28b5 --- /dev/null +++ b/nss/tests/group @@ -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: