Bug 2496 - Solaris only provides 16 or 32 groups
Summary: Solaris only provides 16 or 32 groups
Status: RESOLVED WONTFIX
Alias: None
Product: Samba 3.0
Classification: Unclassified
Component: User/Group Accounts (show other bugs)
Version: 3.0.12
Hardware: All Solaris
: P3 enhancement
Target Milestone: none
Assignee: Samba Bugzilla Account
QA Contact: Samba QA Contact
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2005-03-19 11:10 UTC by David S. Collier-Brown
Modified: 2014-06-02 16:44 UTC (History)
1 user (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description David S. Collier-Brown 2005-03-19 11:10:15 UTC
Solaris (and other unixes) has a limit of 16 to 32
elements in the groupls list.  The 16 limit comes
from an NFS standard, and was loosened to 32 for
the kernel at a later date.

  This results in failures to allow access to files
which were acessable when served from Windows. The
limit on Linux has been increased in the kernel to
avoid this limitation.

See also bugs 1184, 1191 and 1212 and Solaris bug 4088757.
The next comment contains the prposed solution, a
shared library.
Comment 1 David S. Collier-Brown 2005-03-19 11:20:56 UTC
/*
 * libgroups - a library for Samba on Solaris to allow
 *	an arbitrarily large number of groups.
 *
 *
 * Problem Description:
 *     Unix has a system-wide limit on the number of groups
 * a user may be in. Samba, which provides file service
 * to Windows clients, needs to support larger numbers
 * of groups per user. This is due to the Windows use of
 * groups, which typically results in more than 16 or
 * 32 groups for a user. At the moment, only Linux has
 * enoug groups for a medium to large Windows AD domain.
 * 
 *     I therefor wrote this interposer library (at home)
 * to remove this limitation on a per-process basis.
 * It keeps an unbounded list of groups and treats the 
 * standard Solaris groups list as a cache. Before opening 
 * a file, the interposer checks to see if the open would 
 * fail because of a group not being in the cache, and if so 
 * will  move it to the head of the ache, shifting the
 * other entries down.
 *
 * Caveats: 
 *     This was written for Samba, which is setuid-root.
 * As the library requries root permissions for setgroups, 
 * it will ONLY work if the program is setuid root or
 * otherwise has the ablity to call setgroups from the
 * middle of the open interposer function. The library will
 * try to abort immediately on startup if it does not
 * have sufficient privelege.
 *
 * License: to be discussed: I'd like to use CDDL & LGPL.
 */
#include <stdio.h>	/* For printf(). */
#include <dlfcn.h>	/* For dlopen(). */
#include <link.h>
#include <assert.h>	/* For assert() macro. */
#include <fcntl.h>	/* For open(). */
#include <sys/stat.h>	/* For open(). */
#include <sys/types.h>	/* For open(). */
#include <unistd.h>	/* For getgroups(). */
#include <unistd.h>	/* For setgroups(). */
#include <stdarg.h>	/* For varargs functions. */
#include <sys/types.h>	/* For stat(). */
#include <sys/stat.h>
#include <errno.h>	/* For errno. */
#include <stdlib.h>	/* For calloc(). */
#include <sys/types.h>	/* For setuid(). */
#include <string.h>	/* For memcpy(). */
#define DEBUG 1
#include "debug.h"	/* Local debugging library */

/* Library instance variables. */
static long	Maxgroups = -1;
static int	Ngroups = -1;
static gid_t	*Groups = NULL;
static void	*libc_p = NULL;
static void	*open_p = NULL; 
static void	*getgroups_p = NULL;
static void	*setgroups_p = NULL;
extern void	perror(const char *p);

/* Static functions */
static long in_groups(gid_t gid, gid_t *groups, long n_groups);
static void reorder_groups(gid_t gid, long position, gid_t *groups);
#define MIN(a, b) ((int)(a) < (int)(b))? (int)(a): (int)(b)
#define MAX(a, b) ((int)(a) > (int)(b))? (int)(a): (int)(b)

/*
 * init function -- set up pointers to underlying functions 
 *	and the initialgroups list. Can fail due to an
 *	out-of-memory condition, library failure or from 
 *	being called by a program with insufficient 
 *	priveledge. In any of these cases, abort immediately
 *	so the LD_RELOAD can be removed.
 */
 void
libgroups_init() {
	int	rc;

	debugi("libgroups_init");
#ifdef DELAY
	sleep(30); /* To allow dbx to attach for debugging. */
#endif

	/* Get function pointers first. */
	libc_p = dlopen("/usr/lib/libc.so.1", RTLD_LAZY);
	assert(libc_p != NULL);
	open_p = dlsym(libc_p, "open");
	assert(open_p != NULL);
	getgroups_p = dlsym(libc_p, "getgroups");
	assert(getgroups_p != NULL);
	setgroups_p = dlsym(libc_p, "setgroups");
	assert(setgroups_p != NULL);

	/* Crash if we can't even start. */
	assert(getuid() == 0 || geteuid() == 0);

	/* Get parameters for this process. */
	Maxgroups = sysconf(_SC_NGROUPS_MAX);
	assert(Maxgroups != 0);
	debugf(("Maxgroups = %ld\n", Maxgroups));

	debugf(("Calling the libc getgroups.\n"));
	Ngroups = ((int (*)(int, gid_t *))getgroups_p)(0, NULL);
	if (Ngroups > 0) {
		/* It's possible to initially have 0 groups. */
		Groups = (gid_t *)calloc(Ngroups, sizeof(gid_t));
		assert(Groups != NULL);
		rc = ((int (*)(int, gid_t *))getgroups_p)(Ngroups, Groups);
		assert(rc != -1);
	}
	debugf(("Ngroups = %d\n", Ngroups));
	debugo();
}


/*
 * open -- intercept open, see if the groups cache needs to be updated.
 *	This will work so long as the number of groups the
 *	file is in in its ACL is 15 (or 31). After that many
 *	group ACLs it's purely a matter of luck.  So don't
 *	set more than 15 group ACLs per file, use user ACLs.
 */
 int 
open(const char *path, int oflag, /* mode_t mode */ ...) {
	va_list ap;
	struct stat st;
	gid_t g;
	long position;

	debugi("open");
	va_start(ap, oflag); /* For varaargs. */

	/* It's important to have the groups set before trying to */
	/* do the open, lest we grant permission we shouldn't in */
	/* what's usually called the "707" case, where a group is */
	/* prohibited while "other" is permitted. */

	/* Before trying to open the file, stat it to find the group. */
	if (stat(path, &st) == -1) {
		/* Errno is set by stat. */
		debugf(("open failed to stat %s\n", path));
		debugo();
		return -1;
	}
	g = st.st_gid;

	/* See if the group is in the user's group list. */
	if ((position = in_groups(g, Groups, Ngroups)) != -1) { 
		/* If it's not in the first 16 (or 32), move it up. */
		if (position >= Maxgroups) { 
			reorder_groups(g, position, Groups);
		} 
	}

	/* Now do the same for each group in the ACL. */	

	/* Finally, return the result of calling the underlying open(). */
	debugo();
	return ((int (*)(const char *, int, mode_t))open_p)(path, oflag, va_arg(ap,
mode_t));
}

/*
 * getgroups -- intercept getgroups, return arbitrarily long groups lists
 */
 int 
getgroups(int gidsetsize, gid_t *grouplist) {

	debugi("getgroups");
	debugf(("gidsetsize = %d\n", gidsetsize));
	if (gidsetsize == 0) {
		debugo();
		return Ngroups;
	}
	else if (gidsetsize < 0) {
		/* No groups, so do nothing. */	
		debugf(("getgroups: asked for %d groups\n", gidsetsize));
		errno = EINVAL;
		debugo();
		return -1;
	}
	else if (Groups == NULL) {
		/* We ran out of memory enlarging it! */
		debugf(("getgroups: Groups == NULL\n"));
		errno = ENOMEM;
		debugo();
		return -1;
	}
	else {
		(void) memcpy(grouplist, Groups, gidsetsize * (int)sizeof(gid_t));
		debugo();
		return 0;
	}
}

/*
 * setgroups -- intercept setgroups, allow arbitrarily long groups lists
 *	but just set the allowed maximum in the cache.
 */
  int 
setgroups(int ngroups, const gid_t *grouplist) {

	debugi("setgroups");
	debugf(("ngroups = %d\n", ngroups));
	debugf(("Ngroups =%d\n", Ngroups));

	/* Adjust size upwards if necessary. */
	if (ngroups > Ngroups) {
		/* Ngroups may be -1 initially.*/
		Ngroups = MAX(ngroups,Ngroups);

		if ((Groups = (gid_t *)realloc(Groups,
		    Ngroups * (int)sizeof(gid_t))) == NULL) {
			debugf(("getgroups: realloc(..., %d) failed\n",\
				Ngroups * (int)sizeof(gid_t)));
			errno = ENOMEM;
			debugo();
			return -1;
		}
		debugf(("after realloc, Ngroups = %d\n", Ngroups));
	}
	Ngroups = ngroups;

	/* Set Groups. */
	(void) memcpy(Groups, grouplist, Ngroups*(int)sizeof(gid_t));

	/* Set the cache, the system's groups list. */
	debugf(("about to call setgroups_p\n"));
	debugo();
	return ((int (*)(int, const gid_t *))setgroups_p)(
		MIN(Maxgroups, Ngroups), grouplist);
}

/* 
 * in_groups -- see if g is in the groups list, and
 *	if so return it's position, from 0 to EOList.
 */
 static long 
in_groups(gid_t gid, gid_t *groups, long n_groups) {
	int	i;
	gid_t	*p;

	debugi("in_groups");
	p = &groups[0];
	for (i=0; i < (int)n_groups; i++,p++) {
		if (gid == *p) {
			debugo();
			return i;
		}
	}
	debugo();
	return -1;
}

/*
 * reorder_groups -- prepend group[position] to groups,
 *	shifting the others right to fill that element.
 */
 static void 
reorder_groups(gid_t gid, long position, gid_t *groups) {
	gid_t	uid, *p, *q;
	int	i;

	/* Shift everything up to but not including gid */
	/* one to the right, filling the place gid was. */
	debugi("reorder_groups");
	p = &groups[position];
	q = p-1;
	for (i=(int)position; i >= 1; i--) {
		*p-- = *q--;
	}
	/* Put gid in the space created at the beginning. */
	groups[0] = gid;

	/* Set the cache to this new sequence. */
	if ((uid = getuid()) != 0) {
		(void) setuid(geteuid());
	}
	((int (*)(int, const gid_t *))setgroups_p)(
		MIN(Maxgroups, Ngroups), groups);
	(void) setuid(uid);
	debugo();
}

/*
 * * debug.h -- the usual trace macros. 
 */
#ifdef DEBUG
#define debugi(fcn) \
        char *debugFcnName = fcn; \
        (void) printf("begin %s\n", fcn)
#define debugo() \
        (void) printf("end %s\n", debugFcnName)
#define debugf(p) \
	(void) printf("\t"); \
	(void) printf p
#else
/* Make them disappear. */
#define debugi(fcn)
#define debugo()
#define debugf(p)
#endif

/*
 * a dummy program to try to open and fail unless gid
 *	101 is within the first 16 in the user's groups.
 *
 */
#include <stdio.h>
#include <fcntl.h>	/* For open(). */
#include <sys/stat.h>	/* For open(). */
#include <sys/types.h>	/* For open(). */
#include <unistd.h>	/* For setgid(). */

int main() {
	gid_t groups[] = { 
             /*  1  2  3  4  5  6  7  8  */
		10, 0, 1, 2, 3, 4, 5, 6, 
	     /*  9 10 11  12  13  14  15     16  */
		 7, 8, 9, 12, 14, 25, 60001, 60002, 
             /* 17     18   19 */
		65534, 100, 101 };
#ifdef INLINE
	libgroups_init();
#endif

	/* Put 101 in as the 19th group. */
	printf("test_program: calling setgroups(19,groups)\n");
	if (setgroups(19,groups) == -1) {
		perror("testProgram: setgroups failed");
		return -1;
	}

	/* Now open a file readable by group 101 */
	(void) printf("test_program: calling open(\"./cant_read_me\", 2)\n");
	if (open("./cant_read_me", 2) == -1) {
		perror("can't open ./cant_read_me");
		return -1;
	}
	else {
		printf("Opened ./cant_read_me \n");
		return 0;
	}
}

And the test file is
-rwxrwx---   1 57964    101           37 Mar  8 17:46 cant_read_me
containing
froggy> suid -c 'cat cant_read_me'
The secret word is "wannabuyaduck?"


Comment 2 Gerald (Jerry) Carter (dead mail address) 2006-07-05 13:11:08 UTC
Dave, Interesting approach but I really don't want smbd playing 
these types of neverending games.
Comment 3 David S. Collier-Brown 2009-08-13 12:28:59 UTC
New version of the workaround LD_PRELOAD library, staring with the makefile.

#
# libgroups -- build an interposer library
#
LINTOPTS=-errchk -s -errsecurity -errtags -erroff=E_NAME_DEF_NOT_USED2,E_NAME_DECL_NOT_USED_DEF2,E_GLOBAL_COULD_BE_STATIC2,E_CAST_INT_TO_SMALL_INT,E_FUNC_VAR_UNUSED
DEBUGOPTS=-DDEBUG 
CCOPTS=${DEBUGOPTS} -g -KPIC -xregs=no%appl -errshort=short -c 
LDOPTS=-G -z defs

all: libgroups.so.1 test_program

libgroups.so.1: libgroups.c 
        lint ${LINTOPTS} libgroups.c
        cc ${CCOPTS} libgroups.c
        ld ${LDOPTS} -o libgroups.so.1 libgroups.o -lc -ldl
                # Formerly used -z initarray=libgroups_init 
        suid -c 'mv ./libgroups.so.1 /usr/lib/secure/libgroups.so.1'

test_program: test_program.c
        cc ${TESTOPTS} -g -errshort=short -o test_program \
                test_program.c
        suid -c 'chown root ./test_program; chmod u+s ./test_program'

# version with libgroups linked in for easy debugging.
test_program2: test_program.c
        rm -f ./test_program
        cc ${TESTOPTS} -DDELAY -g -errshort=short -o test_program \
                test_program.c libgroups.o -lc -ldl


test: libgroups.so.1 test_program cant_read_me # aclfile
        test_program ./cant_read_me || echo "failed correctly, no perms"
        suid -c 'chown root ./test_program; chmod u+s ./test_program'
        # Test creation of ordinary files.
        rm -f ./i_dont_exist 
        suid -c 'LD_PRELOAD=/usr/lib/secure/libgroups.so.1 test_program ./i_dont_exist"
        rm -f ./i_dont_exist 
        # Test opening of files with limited permissions.
        test_program ./cant_read_me || echo "failed, as intended"
        suid -c 'LD_PRELOAD=/usr/lib/secure/libgroups.so.1 test_program ./cant_read_me'

test2: cant_read_me # for using a debugger
        -rm test_program
        make -e test_program2
        test_program ./cant_read_me

# Data file for test_program
cant_read_me:
        echo "The secret word is wannabuyaduck?" >./cant_read_me
        chmod 750 ./cant_read_me
        suid -c 'chown 101:101 ./cant_read_me'

#acltest: acltest.c aclfile
#       cc ${COPTS} -o acltest acltest.c
#       acltest ./aclfile

# Data file for acl test
#aclfile:
#       touch ./aclfile
#       chmod a-rwx aclfile
#       setfacl -m g:101:rwx aclfile
#       getfacl aclfile

clean:
        -rm -f libgroups.so.1 test_program *.o core cant_read_me aclfile
        -suid -c 'rm /usr/lib/secure/libgroups.so.1'

tar: clean
        tar cvf ../libgroups.tar .
        gzip ../libgroups.tar


Comment 4 David S. Collier-Brown 2009-08-13 12:30:41 UTC
(In reply to comment #3)


.\"     @(#) open.so.1 
.\"
.TH libgroups.so.1 1 
.AT 3
.SH NAME
libgroups.so.1 \- shared library to allow longer group lists

.SH SYNOPSIS
.B LD_PRELOAD=./libgroups.so.1 
program [args] ...
.sp 0
.SH DESCRIPTION
.I libgroups
intercepts calls to open, getgroups and setgroups and
manages an arbitrarily-long group list.  If a file
to be opened is at least readable by one of the groups
in the group list, the group will be moved into the 
first 16 (or 23) entries of the supplementary group list.
This allows the kernel to be able to use the group to compute
acessability for the file.

>PP
In effect, it's treating the kernel grpoups list as a cache.

.PP
This will not work if the file has 17 or more group
ACLs and the one actually needed is the 17th: the cache
won't have the right ACL in it. Such is a seriously 
unlikely situation.

.PP
This library requires the caller have sufficient permissions
to call getgroups/setgroups. If they do not, or if an
inernal error occurs, 
.IR abort
(3C) 
will be called. 

.SH FILES
.sp 0

.SH "SEE ALSO"

.SH BUGS
This release does not check ACLs, just the per-user and file
groups.

.SH DIAGNOSTICS

.SH COMPATIBILITY
It is primarily written for use by Samba, which requires very long
group lists to interoperate with Active Directory.

.SH CHANGES

.SH ACKNOWLEDGEMENTS
Thanks to Volker Lendecke, Jeremy Allison, Gerald (Jerry) Carter and
the rest of the team at samba-technical@lists.samba.org for
pointing out the implications of short groups lists.
.SH AUTHOR
Davd Collier-Brown, davecb@spamcop.net
Comment 5 David S. Collier-Brown 2009-08-13 12:31:39 UTC
/*
 * a dummy program to try to open and fail unless gid
 *      101 is within the first 16 in the user's groups.
 *
 */
#include <stdio.h>
#include <fcntl.h>      /* For open(). */
#include <sys/stat.h>   /* For open(). */
#include <sys/types.h>  /* For open(). */
#include <unistd.h>     /* For setgid(). */
#include <syslog.h>     /* For syslog(). */
#include <stdlib.h>     /* For exit(). */
#include <errno.h>      /* For errno. */

int main(int argc, char *argv[]) {
        int     n, fd;
        gid_t groups[] = { 
             /*  1  2  3  4  5  6  7  8  */
                10, 0, 1, 2, 3, 4, 5, 6, 
             /*  9 10 11  12  13  14  15     16  */
                 7, 8, 9, 12, 14, 25, 60001, 60002, 
             /* 17     18   19 */
                65534, 100, 101 };

        if (argc != 2) {
                printf("Usage: test_program file\n");
                (void) exit(1);
        }
        printf("test_program: starting mainline.\n");
        openlog("test_program.c", LOG_PID|LOG_CONS|LOG_NOWAIT, LOG_USER);

        /* Put 101 in as the 19th group. */
        printf("test_program: calling setgroups(19,groups)\n");
        syslog(LOG_ERR, "about to call setgroups");
        if ((n = setgroups(19,groups)) == -1) {
                perror("testProgram: setgroups failed");
                return -1;
        }
        if ((n = getgroups(0,groups)) != 19) {
                printf("test_program: getgroups(0,...) returned %d.\n",n);
                return -1;
        }
        else {
                printf("test_program: 19 groups set.\n");
        }
        syslog(LOG_ERR, "about to open %s", argv[1]);

        /* Now open a file readable by group 101 */
        (void) printf("test_program: calling open(\"%s\", O_RDONLY)\n", argv[1]);
        if ((fd = open(argv[1], O_RDONLY)) != -1) {
                printf("Opened %s\n", argv[1]);
                (void) close(fd);
        }
        else if (errno == ENOENT) {
                (void) printf("test_program: calling open(\"%s\",O_CREAT)\n",
                        argv[1]);
                if ((fd = open(argv[1], O_RDONLY|O_CREAT)) != -1) {
                        printf("Opened %s\n", argv[1]);
                        (void) close(fd);
                }
                else {

                        perror("Couldn't create");
                        (void) exit(1);
                }
        }
        else {
                perror("Couldn't open");
                (void) exit(1);
        }
}

Comment 6 David S. Collier-Brown 2009-08-13 12:32:29 UTC
(/*
 * * debug.h -- the usual trace macros. 
 */
#ifdef DEBUG
char    d_indent[81] = "                                                                               ";
int     d_offset = 0;
#define debugi(fcn) \
        char *debugFcnName = fcn; \
        (void) printf("%sbegin %s\n",&d_indent[80-(++d_offset*4)], fcn)
#define debugo() \
        (void) printf("%send %s\n",&d_indent[80-(d_offset--*4)], debugFcnName)
#define debugf(a) (void) printf("%s    ",&d_indent[80-(d_offset*4)]); printf a
#else
/* Make them disappear. */
#define debugi(fcn)
#define debugo()
#define debugf(a)
#endif


Comment 7 David S. Collier-Brown 2009-08-13 12:33:41 UTC
/*
 * libgroups - a library for Samba on Solaris to allow
 *      an arbitrarily large number of groups.
 *
 * Copyright 2005, 2009 David Collier-Brown.
 *
 * This file is distributed under both the LGPL and CDDL to
 * be usable with both Samba and Open Solaris. If this licensing
 * fails that end, I reserve the right to disclaim copyright and
 * place it in the public domain so that both parties can
 * seperately claim copyright upon their copies.
 *                      David Collier-Brown, March 2005.
 *
 * Problem Description:
 *     Unix has a system-wide limit on the number of groups
 * a user may be in. Samba, which provides file service
 * to Windows clients, needs to support larger numbers
 * of groups per user. This is due to the Windows use of
 * groups, which typically results in more than 16 or
 * 32 groups for a user. At the moment, only Linux has
 * enoug groups for a medium to large Windows AD domain.
 * 
 *     I therefor wrote this interposer library (at home)
 * to remove this limitation on a per-process basis.
 * It keeps an unbounded list of groups and treats the 
 * standard Solaris groups list as a cache. Before opening 
 * a file, the interposer checks to see if the open would 
 * fail because of a group not being in the cache, and if so 
 * will  move it to the head of the ache, shifting the
 * other entries down.
 *
 * Caveats: 
 *     This was written for Samba, which is setuid-root.
 * As the library requries root permissions for setgroups, 
 * it will ONLY work if the program is setuid root or
 * otherwise has the ablity to call setgroups from the
 * middle of the open interposer function. The library will
 * try to abort immediately on startup if it does not
 * have sufficient privelege.
 * 
 * License:
 *
 * For information re dual licensing, see the Groklaw article
 * at http://www.groklaw.net/article.php?story=20050126023359386
 *
 *     The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only.
 * See the file usr/src/LICENSING.NOTICE in this distribution or
 * http://www.opensolaris.org/license/ for details.
 *
 *     This library is free software; you can redistribute it 
 * and/or modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Library General Public License for more details.
 * 
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 */
#include <stdio.h>      /* For printf(). */
#include <dlfcn.h>      /* For dlopen(). */
#include <link.h>
#include <assert.h>     /* For assert() macro. */
#include <fcntl.h>      /* For open(). */
#include <sys/stat.h>   /* For open(). */
#include <sys/types.h>  /* For open(). */
#include <unistd.h>     /* For getgroups(). */
#include <unistd.h>     /* For setgroups(). */
#include <stdarg.h>     /* For varargs functions. */
#include <sys/types.h>  /* For stat(). */
#include <sys/stat.h>
#include <errno.h>      /* For errno. */
#include <stdlib.h>     /* For calloc(). */
#include <sys/types.h>  /* For setuid(). */
#include <string.h>     /* For memcpy(). */
#include <syslog.h>     /* For syslog(). */
#include <sys/acl.h>    /* For acl(). */
#include "debug.h"      /* Local debugging library */

/* Library instance variables. */
static int      Initialized = 0;
static long     OS_Maxgroups = -1;
static int      Ngroups = -1;
static gid_t    *Groups = NULL;
static void     *libc_p = NULL;
static void     *open_p = NULL; 
static void     *getgroups_p = NULL;
static void     *setgroups_p = NULL;
extern void     perror(const char *p);


/* Static functions */
static long in_groups(gid_t gid, gid_t *groups, long n_groups);
static void reorder_groups(gid_t gid, long position, gid_t *groups);
#ifdef ACL
static int get_acl_groups(const char *name, gid_t *groups);
#endif
static void printgroups(const char *prefix);
#define MIN(a, b) ((int)(a) < (int)(b))? (int)(a): (int)(b)
#define MAX(a, b) ((int)(a) > (int)(b))? (int)(a): (int)(b)
#define ACLGROUPSIZE 32

/*
 * init function -- set up pointers to underlying functions 
 *      and the initialgroups list. Can fail due to an
 *      out-of-memory condition, library failure or from 
 *      being called by a program with insufficient 
 *      priveledge. In any of these cases, abort immediately
 *      so the LD_PRELOAD can be removed.
 */
 void
libgroups_init() {
        int     rc;

        assert(Initialized == 0);
        debugi("libgroups_init");
        /* This will be overridden if the app uses syslog too. */
        openlog("libgroups", LOG_PID|LOG_CONS|LOG_NOWAIT, LOG_USER); 
#ifdef DELAY
        sleep(30); /* To allow dbx to attach for debugging. */
#endif

        /* Get function pointers first. */
        libc_p = dlopen("/usr/lib/libc.so.1", RTLD_LAZY);
        assert(libc_p != NULL);
        open_p = dlsym(libc_p, "open");
        assert(open_p != NULL);
        getgroups_p = dlsym(libc_p, "getgroups");
        assert(getgroups_p != NULL);
        setgroups_p = dlsym(libc_p, "setgroups");
        assert(setgroups_p != NULL);

        /* Crash if we can't even start. */
        assert(getuid() == 0 || geteuid() == 0);

        /* Get parameters for this process. */
        OS_Maxgroups = sysconf(_SC_NGROUPS_MAX);
        assert(OS_Maxgroups != 0);
        debugf(("OS_Maxgroups = %ld\n", OS_Maxgroups));

        Ngroups = ((int (*)(int, gid_t *))getgroups_p)(0, NULL);
        if (Ngroups > 0) {
                /* It's possible to initially have 0 groups. */
                Groups = (gid_t *)calloc(Ngroups, sizeof(gid_t));
                assert(Groups != NULL);
                debugf(("Calling getgroups_p\n"));
                rc = ((int (*)(int, gid_t *))getgroups_p)(Ngroups, Groups);
                assert(rc != -1);
        }
        Initialized = 1;
        debugf(("Ngroups = %d\n", Ngroups));
        printgroups("at end of init function");
        debugo();
}



/*
 * open -- intercept open, see if the groups cache needs to be updated.
 *      This makes infinitely long group lists work. Not
 *      so for group ACLs, though. It will work so long as the number
 *      of groups the file has in its ACL is < 15 (or 31). After that 
 *      many group ACLs it's purely a matter of luck. So use user ACLs.
 *      The ACL code has now been commented out, for lack of a 
 *      necessaty and sufficient test: Samba doesn't need it.
 */
 int 
open(const char *path, int oflag, /* mode_t mode */ ...) {
        va_list ap;
        mode_t  mode;
        struct stat st;
        gid_t   g;
        int     rc;
#ifdef ACL
        gid_t   ag, aclgroups[ACLGROUPSIZE];
        int     i, n;
#endif /* ACL */
        long position;

        debugi("open");
        debugf(("open called with (%s, %d, ...)\n", path, oflag));
        va_start(ap, oflag); /* For varargs. */
        va_end(ap);

        /* It's important to have the groups set before trying to */
        /* do the open, lest we grant permission we shouldn't in */
        /* what's usually called the "707" case, where a group is */
        /* prohibited while "other" is permitted. */
        if (Initialized == 0) {
                libgroups_init();
        }

        /* Before trying to open the file, stat it to find the group. */
        if (stat(path, &st) != -1) {
                /* The file existed, so get its group. */
                g = st.st_gid;
        }
        else if (errno == ENOENT) {
                /* We're about to create it, so short-circuit rest. */
                rc = ((int (*)(const char *, int, mode_t))open_p)(path, oflag,
                                                        va_arg(ap, mode_t));
                if (rc == -1) {
                        debugf(("open failed\n"));
                        syslog(LOG_ERR, "open failed with errno %d (%s)",
                                errno, strerror(errno));
                }
                return rc;
        }
        else {
                debugf(("open failed to stat %s\n", path));
                debugo();
                return -1;
        }

        /* See if the group is in the user's group list. */
        if ((position = in_groups(g, Groups, Ngroups)) != -1) { 
                /* If it's not in the first 16 (or 32), move it up. */
                if (position >= OS_Maxgroups) { 
                        /* Sideffect - syslog's the change. */
                        reorder_groups(g, position, Groups);
                } 
        }
#ifdef ACL
        /* Now do the same for each additional group in the ACL. */
        n = get_acl_groups(path, aclgroups);
        for (i=0; i < n; i++) {
                ag = aclgroups[i];
                if ((position = in_groups(ag, Groups, Ngroups)) != -1) { 
                        if (position >= OS_Maxgroups) { 
                                reorder_groups(ag, position, Groups);
                        }
                }
        }
#endif /* ACL */
        printgroups("after acl check");

        /* Finally, return the result of calling the underlying open(). */
        debugf(("calling open_p\n"));
        rc = ((int (*)(const char *, int, mode_t))open_p)(path, oflag, 
                                                        va_arg(ap, mode_t));
        if (rc == -1) {
                debugf(("open failed\n"));
                syslog(LOG_ERR, "open failed with errno %d (%s)",
                        errno, strerror(errno));
        }
        va_end(ap);
        debugo();
        return rc;

}

/*
 * getgroups -- intercept getgroups, return arbitrarily long groups lists
 */
 int 
getgroups(int gidsetsize, gid_t *grouplist) {

        debugi("getgroups");
        debugf(("gidsetsize = %d\n", gidsetsize));
        if (Initialized == 0) {
                libgroups_init();
        }
        if (gidsetsize == 0) {
                debugo();
                return Ngroups;
        }
        else if (gidsetsize < 0) {
                /* No groups, so do nothing. */
                debugf(("getgroups: asked for %d groups\n", gidsetsize));
                errno = EINVAL;
                debugo();
                return -1;
        }
        else if (Groups == NULL) {
                /* We ran out of memory enlarging it! */
                debugf(("getgroups: Groups == NULL\n"));
                errno = ENOMEM;
                debugo();
                return -1;
        }
        else {
                (void) memcpy(grouplist, Groups, gidsetsize * (int)sizeof(gid_t));
                debugo();
                return 0;
        }
}

/*
 * setgroups -- intercept setgroups, allow arbitrarily long groups lists
 *      but just set the allowed maximum in the cache.
 */
  int 
setgroups(int gidsetsize, const gid_t *grouplist) {

        debugi("setgroups");
        debugf(("gidsetsize = %d\n", gidsetsize));
        debugf(("Ngroups =%d\n", Ngroups));
        if (Initialized == 0) {
                libgroups_init();
        }

        /* Adjust size upwards if necessary. */
        if (gidsetsize > Ngroups) {
                /* Ngroups may be -1 initially.*/
                Ngroups = MAX(gidsetsize,Ngroups);

                if ((Groups = (gid_t *)realloc(Groups,
                    Ngroups * (int)sizeof(gid_t))) == NULL) {
                        debugf(("getgroups: realloc(..., %d) failed\n",\
                                Ngroups * (int)sizeof(gid_t)));
                        errno = ENOMEM;
                        debugo();
                        return -1;
                }
                debugf(("after realloc, Ngroups = %d\n", Ngroups));
        }
        Ngroups = gidsetsize;

        /* Set Groups. */
        (void) memcpy(Groups, grouplist, Ngroups*(int)sizeof(gid_t));

        /* Set the cache, the system's groups list. */
        debugf(("about to call setgroups_p\n"));
        debugo();
        return ((int (*)(int, const gid_t *))setgroups_p)(
                MIN(OS_Maxgroups, Ngroups), grouplist);
}

/* 
 * in_groups -- see if g is in the groups list, and
 *      if so return it's position, from 0 to EOList.
 */
 static long 
in_groups(gid_t gid, gid_t *groups, long n_groups) {
        int     i;
        gid_t   *p;

        debugi("in_groups");
        assert(Initialized == 1);
        p = &groups[0];
        for (i=0; i < (int)n_groups; i++,p++) {
                if (gid == *p) {
                        debugo();
                        return i;
                }
        }
        debugo();
        return -1;
}

/*
 * reorder_groups -- prepend group[position] to groups,
 *      shifting the others right to fill that element.
 */
 static void 
reorder_groups(gid_t gid, long position, gid_t *groups) {
        gid_t   uid, *p, *q;
        int     i, rc;

        assert(Initialized == 1);
        /* Shift everything up to but not including gid */
        /* one to the right, filling the place gid was. */
        debugi("reorder_groups");
        debugf(("libgroups.so.1 moved gid %ld to to front of cache\n", gid));
        /* One of the two non-debug log calls of the change. */
        syslog(LOG_ERR, "libgroups.so.1 moved gid %ld "
                "to front of cache", gid);

        p = &groups[position];
        q = p-1;
        for (i=(int)position; i >= 1; i--) {
                *p-- = *q--;
        }
        /* Put gid in the space created at the beginning. */
        groups[0] = gid;

        /* Set the cache to this new sequence. */
        debugf(("uid = %ld, euid = %ld\n", getuid(), geteuid()));
        if ((uid = getuid()) != 0) {
                (void) setuid(geteuid());
                debugf(("uid = %ld\n", getuid()));
        }
        debugf(("calling setgroups_p\n"));
        rc = ((int (*)(int, const gid_t *))setgroups_p)(
                MIN(OS_Maxgroups, Ngroups), groups);
        assert(rc == 0);
        (void) setuid(uid);
        printgroups("in reorder_groups");
        debugo();
}


#ifdef ACL
/*
 * get_acl_groups -- get any group acls for the named file.
 */
 static int 
get_acl_groups(const char *name, gid_t *groups)  {
        int     i, n_groups, n_acls;
        aclent_t aclbuf[100], *p;

        debugi("get_acl_groups");
        assert(Initialized == 1);
        if ((n_acls = acl(name, GETACL, ACLGROUPSIZE, aclbuf)) == -1) {
                debugf(("No acls on %s\n", name));
                debugo();
                return 0;
        }
        p = &aclbuf[0];
        for (i=0,n_groups=0; i < n_acls; i++,p++) {
                if (p->a_type == GROUP) {
                        /* Additional groups. */
                        debugf(("acl contained group %ld\n", p->a_id));
                        groups[n_groups++] = p->a_id;
                }
        }
        debugf(("%d acls on %s\n", n_groups, name));
        debugo();
        return n_groups;
}
#endif /* ACL */

/*
 * printgroups -- debugging function
 */
 /*ARGSUSED*/
 static void
printgroups(const char *prefix) {
        int     i;

        assert(Initialized == 1);
        debugf(("%s, Groups = ", prefix));
        for (i=0; i < Ngroups; i++) {
                (void) printf("%ld ", Groups[i]);
                if (i == (int)OS_Maxgroups-1 && i < (Ngroups+1)) {
                        (void) printf(" (end of cache), plus ");
                }
        }
        (void) printf("\n");
}
Comment 8 Jeremy Allison 2009-08-13 14:41:23 UTC
Thanks a lot for this Dave, it should definitely help. However, the real fix is for Solaris to fix this issue for user space code. If they've already done this in the kernel for their own cifs code, then they really must make this available to other implementations - otherwise we have some legitimate complaints similar to the ones Microsoft had to answer for.
Jeremy.
Comment 9 Casper Dik 2014-06-02 09:59:07 UTC
For the record: Solaris 11 allows upto 1024 groups per user; this fix was
also backported to Solaris 10u10. This is not the default and the administrator
needs to add "set ngroups_max = 1024" to /etc/system and then reboot the system.
Comment 10 Jeremy Allison 2014-06-02 16:44:53 UTC
(In reply to comment #9)
> For the record: Solaris 11 allows upto 1024 groups per user; this fix was
> also backported to Solaris 10u10. This is not the default and the administrator
> needs to add "set ngroups_max = 1024" to /etc/system and then reboot the
> system.

Thanks for the note Casper. I'll make sure anyone on the samba lists who is suffering from this problem is pointed at this info.

Cheers,

Jeremy.