From a3031f694a0d58d23d383346ee848a6c78a1e4b9 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Tue, 11 Jun 2019 11:05:08 +0200 Subject: [PATCH] Check size limits and abort if they are violated Also document them in the README and add tests. --- README | 9 +++++ group.go | 6 +++ nss/tests/gr.c | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ nss/tests/pw.c | 80 +++++++++++++++++++++++++++++++++++++ passwd.go | 6 +++ 5 files changed, 207 insertions(+) diff --git a/README b/README index a6d40b5..1b8a6c3 100644 --- a/README +++ b/README @@ -33,6 +33,15 @@ Nsscash is very careful when deploying the changes: permitted and will not be written to disk. This is designed to prevent the accidental loss of all users/groups on a system. +The passwd/group files have the following size restrictions: +- maximum number of entries: '2^64-1' (uint64_t) +- maximum passwd entry size: 65543 bytes (including newline) +- maximum group entry size: 65535 bytes (including newline, only one member) +- maximum members per group: depends on the user name length, + with 9 bytes per user: 5460 users +- `nsscash` checks for these restrictions and aborts with an error if they are + violated + nsscash is licensed under AGPL version 3 or later. [1] https://github.com/google/nsscache diff --git a/group.go b/group.go index 126ebb0..4ca213a 100644 --- a/group.go +++ b/group.go @@ -26,6 +26,7 @@ import ( "sort" "strconv" "strings" + "math" "github.com/pkg/errors" ) @@ -130,6 +131,11 @@ func SerializeGroup(g Group) ([]byte, error) { } // And the group members concatenated as above data.Write(mems.Bytes()) + // Ensure the offsets can fit the length of this entry + if data.Len() > math.MaxUint16 { + return nil, fmt.Errorf("group too large to serialize: %v, %v", + data.Len(), g) + } size := uint16(data.Len()) var res bytes.Buffer // serialized result diff --git a/nss/tests/gr.c b/nss/tests/gr.c index 3b2f994..487e673 100644 --- a/nss/tests/gr.c +++ b/nss/tests/gr.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "../cash_nss.h" @@ -319,10 +320,115 @@ static void test_getgrnam(void) { assert(rename("tests/group.nsscash.tmp", "tests/group.nsscash") == 0); } +static void test_limits(void) { + char large_member[65525]; + memset(large_member, 'X', sizeof(large_member)); + large_member[sizeof(large_member)-1] = '\0'; + + char many_members[54603]; // 5461 members + memset(many_members, 'X', sizeof(many_members)); + for (int i = 9; i < (int)sizeof(many_members); i += 10) { + many_members[i-1] = (char)('A' + i % ('Z' - 'A')); + many_members[i] = ','; + } + many_members[sizeof(many_members)-1] = '\0'; + + int r; + FILE *fh; + + const char *nsscache_cmd = "../nsscash convert group " + "tests/limits tests/limits.nsscash 2> /dev/null"; + + // Entries which will not fit in uint16_t, nsscash must abort + + fh = fopen("tests/limits", "w"); + assert(fh != NULL); + r = fprintf(fh, "test:x:42:A%s\n", large_member); + assert(r == 65536); + r = fclose(fh); + assert(r == 0); + + r = system(nsscache_cmd); + assert(r != -1); + assert(WIFEXITED(r) && WEXITSTATUS(r) == 1); + + fh = fopen("tests/limits", "w"); + assert(fh != NULL); + r = fprintf(fh, "many:x:4711:%s%s\n", many_members, many_members); + assert(r == 109217); + r = fclose(fh); + assert(r == 0); + + r = system(nsscache_cmd); + assert(r != -1); + assert(WIFEXITED(r) && WEXITSTATUS(r) == 1); + + // Largest entries which will fit + + fh = fopen("tests/limits", "w"); + assert(fh != NULL); + r = fprintf(fh, "test:x:42:%s\n", large_member); + assert(r == 65535); + r = fprintf(fh, "many:x:4711:%s\n", many_members); + assert(r == 54615); + r = fclose(fh); + assert(r == 0); + + r = system(nsscache_cmd); + assert(r != -1); + assert(WIFEXITED(r) && WEXITSTATUS(r) == 0); + + r = rename("tests/group.nsscash", "tests/group.nsscash.tmp"); + assert(r == 0); + r = rename("tests/limits.nsscash", "tests/group.nsscash"); + assert(r == 0); + + // Check if the entry can be retrieved + + struct group g; + enum nss_status s; + char tmp[sizeof(char **) + 1*sizeof(char *) + 1*sizeof(uint16_t) + + 4+1 + 1+1 + 65525 + 1]; + char tmp2[sizeof(char **) + 5462*sizeof(char *) + 5462*sizeof(uint16_t) + + 4+1 + 1+1 + 54603 + 1]; + int errnop = 0; + + s = _nss_cash_getgrgid_r(42, &g, tmp, sizeof(tmp), &errnop); + assert(s == NSS_STATUS_SUCCESS); + assert(!strcmp(g.gr_name, "test")); + assert(g.gr_gid == 42); + assert(g.gr_mem != NULL); + assert(!strcmp(g.gr_mem[0], large_member)); + assert(g.gr_mem[1] == NULL); + + s = _nss_cash_getgrgid_r(4711, &g, tmp2, sizeof(tmp2), &errnop); + assert(s == NSS_STATUS_SUCCESS); + assert(!strcmp(g.gr_name, "many")); + assert(g.gr_gid == 4711); + assert(g.gr_mem != NULL); + for (int i = 0; i < 5461-1; i++) { + char x[9+1]; + memset(x, 'X', sizeof(x)); + x[8] = (char)('A' + (i * 10 + 9) % ('Z' - 'A')); + x[9] = '\0'; + assert(!strcmp(g.gr_mem[i], x)); + } + assert(!strcmp(g.gr_mem[5461-1], "XX")); + assert(g.gr_mem[5461] == NULL); + + r = rename("tests/group.nsscash.tmp", "tests/group.nsscash"); + assert(r == 0); + + r = unlink("tests/limits"); + assert(r == 0); +} + int main(void) { test_getgrent(); test_getgrgid(); test_getgrnam(); + test_limits(); + return EXIT_SUCCESS; } diff --git a/nss/tests/pw.c b/nss/tests/pw.c index 86e6ae4..b832b38 100644 --- a/nss/tests/pw.c +++ b/nss/tests/pw.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "../cash_nss.h" @@ -312,10 +313,89 @@ static void test_getpwnam(void) { assert(rename("tests/passwd.nsscash.tmp", "tests/passwd.nsscash") == 0); } +static void test_limits(void) { + char gecos[65508]; + memset(gecos, 'X', sizeof(gecos)); + gecos[sizeof(gecos)-1] = '\0'; + + int r; + FILE *fh; + + const char *nsscache_cmd = "../nsscash convert passwd " + "tests/limits tests/limits.nsscash 2> /dev/null"; + + // Entries which will not fit in uint16_t, nsscash must abort + + fh = fopen("tests/limits", "w"); + assert(fh != NULL); + r = fprintf(fh, "test:xx:42:4711:%s:/home/test:/bin/zsh\n", gecos); + assert(r == 65544); + r = fclose(fh); + assert(r == 0); + + r = system(nsscache_cmd); + assert(r != -1); + assert(WIFEXITED(r) && WEXITSTATUS(r) == 1); + + fh = fopen("tests/limits", "w"); + assert(fh != NULL); + r = fprintf(fh, "test:%s:42:4711:%s:/home/test:/bin/zsh\n", gecos, gecos); + assert(r == 131049); + r = fclose(fh); + assert(r == 0); + + r = system(nsscache_cmd); + assert(r != -1); + assert(WIFEXITED(r) && WEXITSTATUS(r) == 1); + + // Largest entry which will fit + + fh = fopen("tests/limits", "w"); + assert(fh != NULL); + r = fprintf(fh, "test:x:42:4711:%s:/home/test:/bin/zsh\n", gecos); + assert(r == 65543); + r = fclose(fh); + assert(r == 0); + + r = system(nsscache_cmd); + assert(r != -1); + assert(WIFEXITED(r) && WEXITSTATUS(r) == 0); + + r = rename("tests/passwd.nsscash", "tests/passwd.nsscash.tmp"); + assert(r == 0); + r = rename("tests/limits.nsscash", "tests/passwd.nsscash"); + assert(r == 0); + + // Check if the entry can be retrieved + + struct passwd p; + enum nss_status s; + char tmp[4+1 + 1+1 + 65508 + 10+1 + 8+1]; + int errnop = 0; + + s = _nss_cash_getpwuid_r(42, &p, tmp, sizeof(tmp), &errnop); + assert(s == NSS_STATUS_SUCCESS); + assert(!strcmp(p.pw_name, "test")); + assert(!strcmp(p.pw_passwd, "x")); + assert(p.pw_uid == 42); + assert(p.pw_gid == 4711); + assert(!strcmp(p.pw_gecos, gecos)); + assert(!strcmp(p.pw_dir, "/home/test")); + assert(!strcmp(p.pw_shell, "/bin/zsh")); + + r = rename("tests/passwd.nsscash.tmp", "tests/passwd.nsscash"); + assert(r == 0); + + r = unlink("tests/limits"); + assert(r == 0); +} + int main(void) { test_getpwent(); test_getpwuid(); test_getpwnam(); + test_limits(); + return EXIT_SUCCESS; } diff --git a/passwd.go b/passwd.go index 54da63b..c9f4409 100644 --- a/passwd.go +++ b/passwd.go @@ -23,6 +23,7 @@ import ( "encoding/binary" "fmt" "io" + "math" "sort" "strconv" "strings" @@ -103,6 +104,11 @@ func SerializePasswd(p Passwd) ([]byte, error) { offShell := uint16(data.Len()) data.Write([]byte(p.Shell)) data.WriteByte(0) + // Ensure the offsets can fit the length of this entry + if data.Len() > math.MaxUint16 { + return nil, fmt.Errorf("passwd too large to serialize: %v, %v", + data.Len(), p) + } size := uint16(data.Len()) var res bytes.Buffer // serialized result -- 2.43.2