summary refs log tree commit diff
path: root/src/nix-setuid-helper/nix-setuid-helper.cc
blob: 26c457fd93c369cc276d08f860d429aea65c8b9b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#include "config.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

#include <pwd.h>
#include <grp.h>

#include <iostream>
#include <vector>

#include "util.hh"

using namespace nix;


extern char * * environ;


/* Recursively change the ownership of `path' to user `uidTo' and
   group `gidTo'.  `path' must currently be owned by user `uidFrom',
   or, if `uidFrom' is -1, by group `gidFrom'. */
static void secureChown(uid_t uidFrom, gid_t gidFrom,
    uid_t uidTo, gid_t gidTo, const Path & path)
{
    format error = format("cannot change ownership of `%1%'") % path;
    
    struct stat st;
    if (lstat(path.c_str(), &st) == -1)
        /* Important: don't give any detailed error messages here.
           Otherwise, the Nix account can discover information about
           the existence of paths that it doesn't normally have access
           to. */
        throw Error(error);

    if (uidFrom != (uid_t) -1) {
        assert(uidFrom != 0);
        if (st.st_uid != uidFrom)
            throw Error(error);
    } else {
        assert(gidFrom != 0);
        if (st.st_gid != gidFrom)
            throw Error(error);
    }

    assert(uidTo != 0 && gidTo != 0);

#if HAVE_LCHOWN
    if (lchown(path.c_str(), uidTo, gidTo) == -1)
        throw Error(error);
#else
    if (!S_ISLNK(st.st_mode) &&
        chown(path.c_str(), uidTo, gidTo) == -1)
        throw Error(error);
#endif

    if (S_ISDIR(st.st_mode)) {
        Strings names = readDirectory(path);
	for (Strings::iterator i = names.begin(); i != names.end(); ++i)
            /* !!! recursion; check stack depth */
	    secureChown(uidFrom, gidFrom, uidTo, gidTo, path + "/" + *i);
    }
}


static uid_t nameToUid(const string & userName)
{
    struct passwd * pw = getpwnam(userName.c_str());
    if (!pw)
        throw Error(format("user `%1%' does not exist") % userName);
    return pw->pw_uid;
}


static void checkIfBuildUser(const StringSet & buildUsers,
    const string & userName)
{
    if (buildUsers.find(userName) == buildUsers.end())
        throw Error(format("user `%1%' is not a member of the build users group")
            % userName);
}


/* Run `program' under user account `targetUser'.  `targetUser' should
   be a member of `buildUsersGroup'.  The ownership of the current
   directory is changed from the Nix user (uidNix) to the target
   user. */
static void runBuilder(uid_t uidNix, gid_t gidBuildUsers,
    const StringSet & buildUsers, const string & targetUser,
    string program, int argc, char * * argv, char * * env)
{
    uid_t uidTargetUser = nameToUid(targetUser);

    /* Sanity check. */
    if (uidTargetUser == 0)
        throw Error("won't setuid to root");

    /* Verify that the target user is a member of the build users
       group. */
    checkIfBuildUser(buildUsers, targetUser);
    
    /* Chown the current directory, *if* it is owned by the Nix
       account.  The idea is that the current directory is the
       temporary build directory in /tmp or somewhere else, and we
       don't want to create that directory here. */
    secureChown(uidNix, (gid_t) -1, uidTargetUser, gidBuildUsers, ".");

    /* Set the real, effective and saved gid.  Must be done before
       setuid(), otherwise it won't set the real and saved gids. */
    if (setgroups(0, 0) == -1)
        throw SysError("cannot clear the set of supplementary groups");

    if (setgid(gidBuildUsers) == -1 ||
        getgid() != gidBuildUsers ||
        getegid() != gidBuildUsers)
        throw SysError("setgid failed");

    /* Set the real, effective and saved uid. */
    if (setuid(uidTargetUser) == -1 ||
        getuid() != uidTargetUser ||
        geteuid() != uidTargetUser)
        throw SysError("setuid failed");

    /* Execute the program. */
    std::vector<const char *> args;
    for (int i = 0; i < argc; ++i)
        args.push_back(argv[i]);
    args.push_back(0);

    environ = env;

    /* Glibc clears TMPDIR in setuid programs (see
       sysdeps/generic/unsecvars.h in the Glibc sources), so bring it
       back. */
    setenv("TMPDIR", getenv("NIX_BUILD_TOP"), 1);
    
    if (execv(program.c_str(), (char * *) &args[0]) == -1)
        throw SysError(format("cannot execute `%1%'") % program);
}


void killBuildUser(gid_t gidBuildUsers,
    const StringSet & buildUsers, const string & userName)
{
    uid_t uid = nameToUid(userName);
    
    /* Verify that the user whose processes we are to kill is a member
       of the build users group. */
    checkIfBuildUser(buildUsers, userName);

    assert(uid != 0);

    killUser(uid);
}


#ifndef NIX_SETUID_CONFIG_FILE
#define NIX_SETUID_CONFIG_FILE "/etc/nix-setuid.conf"
#endif


static void run(int argc, char * * argv) 
{
    char * * oldEnviron = environ;
    
    setuidCleanup();

    if (geteuid() != 0)
        throw Error("nix-setuid-wrapper must be setuid root");


    /* Read the configuration file.  It should consist of two words:
       
       <nix-user-name> <nix-builders-group>

       The first is the privileged account under which the main Nix
       processes run (i.e., the supposed caller).  It should match our
       real uid.  The second is the Unix group to which the Nix
       builders belong (and nothing else!). */
    string configFile = NIX_SETUID_CONFIG_FILE;
    AutoCloseFD fdConfig = open(configFile.c_str(), O_RDONLY);
    if (fdConfig == -1)
        throw SysError(format("opening `%1%'") % configFile);

    /* Config file should be owned by root. */
    struct stat st;
    if (fstat(fdConfig, &st) == -1) throw SysError("statting file");
    if (st.st_uid != 0)
        throw Error(format("`%1%' not owned by root") % configFile);
    if (st.st_mode & (S_IWGRP | S_IWOTH))
        throw Error(format("`%1%' should not be group or world-writable") % configFile);

    Strings tokens = tokenizeString(readFile(fdConfig));

    fdConfig.close();

    if (tokens.size() != 2)
        throw Error(format("parse error in `%1%'") % configFile);

    Strings::iterator i = tokens.begin();
    string nixUser = *i++;
    string buildUsersGroup = *i++;


    /* Check that the caller (real uid) is the one allowed to call
       this program. */
    uid_t uidNix = nameToUid(nixUser);
    if (uidNix != getuid())
        throw Error("you are not allowed to call this program, go away");
    
    
    /* Get the gid and members of buildUsersGroup. */
    struct group * gr = getgrnam(buildUsersGroup.c_str());
    if (!gr)
        throw Error(format("group `%1%' does not exist") % buildUsersGroup);
    gid_t gidBuildUsers = gr->gr_gid;

    StringSet buildUsers;
    for (char * * p = gr->gr_mem; *p; ++p)
        buildUsers.insert(*p);

    
    /* Perform the desired command. */
    if (argc < 2)
        throw Error("invalid arguments");

    string command(argv[1]);

    if (command == "run-builder") {
        /* Syntax: nix-setuid-helper run-builder <username> <program>
             <arg0 arg1...> */
        if (argc < 4) throw Error("missing user name / program name");
        runBuilder(uidNix, gidBuildUsers, buildUsers,
            argv[2], argv[3], argc - 4, argv + 4, oldEnviron);
    }

    else if (command == "get-ownership") {
        /* Syntax: nix-setuid-helper get-ownership <path> */
        if (argc != 3) throw Error("missing path");
        secureChown((uid_t) -1, gidBuildUsers, uidNix, gidBuildUsers, argv[2]);
    }

    else if (command == "kill") {
        /* Syntax: nix-setuid-helper kill <username> */
        if (argc != 3) throw Error("missing user name");
        killBuildUser(gidBuildUsers, buildUsers, argv[2]);
    }

    else throw Error ("invalid command");
}


int main(int argc, char * * argv)
{
    try {
        run(argc, argv);
    } catch (Error & e) {
        std::cerr << e.msg() << std::endl;
        return 1;
    }
}