1 /* PAM setquota module
2
3 This PAM module sets disk quota when a session begins.
4
5 Copyright © 2006 Ruslan Savchenko <savrus@mexmat.net>
6 Copyright © 2010 Shane Tzen <shane@ict.usc.edu>
7 Copyright © 2012-2020 Sven Hartge <sven@svenhartge.de>
8 Copyright © 2016 Keller Fuchs <kellerfuchs@hashbang.sh>
9 */
10
11 #include <sys/types.h>
12 #include <sys/quota.h>
13 #include <linux/quota.h>
14 #include <pwd.h>
15 #include <syslog.h>
16 #include <errno.h>
17 #include <mntent.h>
18 #include <stdio.h>
19 #include <stdbool.h>
20
21 #include <security/pam_modules.h>
22 #include <security/_pam_macros.h>
23 #include <security/pam_ext.h>
24 #include <security/pam_modutil.h>
25 #include "pam_inline.h"
26
27 #ifndef PATH_LOGIN_DEFS
28 # define PATH_LOGIN_DEFS "/etc/login.defs"
29 #endif
30
31 #define MAX_UID_VALUE 0xFFFFFFFFUL
32
33 struct pam_params {
34 uid_t start_uid;
35 uid_t end_uid;
36 const char *fs;
37 size_t fs_len;
38 int overwrite;
39 int debug;
40 };
41
42 static inline void
43 debug(pam_handle_t *pamh, const struct if_dqblk *p,
44 const char *device, const char *dbgprefix) {
45 pam_syslog(pamh, LOG_DEBUG, "%s device=%s bsoftlimit=%llu bhardlimit=%llu "
46 "isoftlimit=%llu ihardlimit=%llu btime=%llu itime=%llu",
47 dbgprefix, device,
48 (unsigned long long) p->dqb_bsoftlimit,
49 (unsigned long long) p->dqb_bhardlimit,
50 (unsigned long long) p->dqb_isoftlimit,
51 (unsigned long long) p->dqb_ihardlimit,
52 (unsigned long long) p->dqb_btime,
53 (unsigned long long) p->dqb_itime);
54 }
55
56 static unsigned long long
57 str_to_dqb_num(pam_handle_t *pamh, const char *str, const char *param) {
58 char *ep = NULL;
59
60 errno = 0;
61 long long temp = strtoll(str, &ep, 10);
62 if (temp < 0 || str == ep || *ep != '\0' || errno !=0) {
63 pam_syslog(pamh, LOG_ERR, "Parameter \"%s=%s\" invalid, setting to 0", param, str);
64 return 0;
65 }
66 else {
67 return temp;
68 }
69 }
70
71 static bool
72 parse_dqblk(pam_handle_t *pamh, int argc, const char **argv, struct if_dqblk *p) {
73 bool bhard = false, bsoft = false, ihard = false, isoft = false;
74
75 /* step through arguments */
76 for (; argc-- > 0; ++argv) {
77 const char *str;
78 if ((str = pam_str_skip_prefix(*argv, "bhardlimit=")) != NULL) {
79 p->dqb_bhardlimit = str_to_dqb_num(pamh, str, "bhardlimit");
80 p->dqb_valid |= QIF_BLIMITS;
81 bhard = true;
82 } else if ((str = pam_str_skip_prefix(*argv, "bsoftlimit=")) != NULL) {
83 p->dqb_bsoftlimit = str_to_dqb_num(pamh, str, "bsoftlimit");
84 p->dqb_valid |= QIF_BLIMITS;
85 bsoft = true;
86 } else if ((str = pam_str_skip_prefix(*argv, "ihardlimit=")) != NULL) {
87 p->dqb_ihardlimit = str_to_dqb_num(pamh, str, "ihardlimit");
88 p->dqb_valid |= QIF_ILIMITS;
89 ihard = true;
90 } else if ((str = pam_str_skip_prefix(*argv, "isoftlimit=")) != NULL) {
91 p->dqb_isoftlimit = str_to_dqb_num(pamh, str, "isoftlimit");
92 p->dqb_valid |= QIF_ILIMITS;
93 isoft = true;
94 } else if ((str = pam_str_skip_prefix(*argv, "btime=")) != NULL) {
95 p->dqb_btime = str_to_dqb_num(pamh, str, "btime");
96 p->dqb_valid |= QIF_BTIME;
97 } else if ((str = pam_str_skip_prefix(*argv, "itime=")) != NULL) {
98 p->dqb_itime = str_to_dqb_num(pamh, str, "itime");
99 p->dqb_valid |= QIF_ITIME;
100 }
101 }
102
103 /* return FALSE if a softlimit or hardlimit has been set
104 * independently of its counterpart.
105 */
106 return !(bhard ^ bsoft) && !(ihard ^ isoft);
107 }
108
109 /* inspired by pam_usertype_get_id */
110 static uid_t
111 str_to_uid(pam_handle_t *pamh, const char *value, uid_t default_value, const char *param) {
112 unsigned long ul;
113 char *ep;
114 uid_t uid;
115
116 errno = 0;
117 ul = strtoul(value, &ep, 10);
118 if (!(ul >= MAX_UID_VALUE
119 || (uid_t)ul >= MAX_UID_VALUE
120 || (errno != 0 && ul == 0)
121 || value == ep
122 || *ep != '\0')) {
123 uid = (uid_t)ul;
124 } else {
125 pam_syslog(pamh, LOG_ERR, "Parameter \"%s=%s\" invalid, "
126 "setting to %u", param, value, default_value);
127 uid = default_value;
128 }
129
130 return uid;
131 }
132
133 static void
134 parse_params(pam_handle_t *pamh, int argc, const char **argv, struct pam_params *p) {
135 /* step through arguments */
136 for (; argc-- > 0; ++argv) {
137 const char *str;
138 char *ep = NULL;
139 if ((str = pam_str_skip_prefix(*argv, "startuid=")) != NULL) {
140 p->start_uid = str_to_uid(pamh, str, p->start_uid, "startuid");
141 } else if ((str = pam_str_skip_prefix(*argv, "enduid=")) != NULL) {
142 p->end_uid = str_to_uid(pamh, str, p->end_uid, "enduid");
143 } else if ((str = pam_str_skip_prefix(*argv, "fs=")) != NULL) {
144 p->fs = str;
145 p->fs_len = strlen(str);
146 /* Mask the unnecessary '/' from the end of fs parameter */
147 if (p->fs_len > 1 && p->fs[p->fs_len - 1] == '/')
148 --p->fs_len;
149 } else if ((str = pam_str_skip_prefix(*argv, "overwrite=")) != NULL) {
150 errno = 0;
151 p->overwrite = strtol(str, &ep, 10);
152 if (*ep != '\0' || str == ep || errno !=0 || (p->overwrite < 0)) {
153 pam_syslog(pamh, LOG_ERR, "Parameter \"overwrite=%s\" invalid, "
154 "setting to 0", str);
155 p->overwrite = 0;
156 }
157 } else if ((str = pam_str_skip_prefix(*argv, "debug=")) != NULL) {
158 errno = 0;
159 p->debug = strtol(str, &ep, 10);
160 if (*ep != '\0' || str == ep || errno != 0 || (p->debug < 0)) {
161 pam_syslog(pamh, LOG_ERR, "Parameter \"debug=%s\" invalid, "
162 "setting to 0", str);
163 p->debug = 0;
164 }
165 }
166 }
167 }
168
169 int
170 pam_sm_open_session(pam_handle_t *pamh, int flags UNUSED,
171 int argc, const char **argv)
172 {
173 int retval;
174 char *val, *mntdevice = NULL;
175 const void *user;
176 const struct passwd *pwd;
177 struct pam_params param = {
178 .start_uid = PAM_USERTYPE_UIDMIN,
179 .end_uid = 0,
180 .fs = NULL };
181 struct if_dqblk ndqblk;
182 FILE *fp;
183 size_t mnt_len = 0, match_size = 0;
184 #ifdef HAVE_GETMNTENT_R
185 char buf[BUFSIZ];
186 struct mntent ent;
187 #endif
188 const struct mntent *mnt;
189 const char *service;
190
191 if (pam_get_item(pamh, PAM_SERVICE, (const void **)&service) != PAM_SUCCESS)
192 service = "";
193
194 /* Get UID_MIN for default start_uid from login.defs */
195 val = pam_modutil_search_key(pamh, PATH_LOGIN_DEFS, "UID_MIN");
196
197 /* Should UID_MIN be undefined, use current value of param.start_uid
198 * pre-defined as PAM_USERTYPE_UIDMIN set by configure as safe
199 * starting UID to avoid setting a quota for root and system
200 * users if startuid= parameter is absent.
201 */
202 if (val) {
203 param.start_uid = str_to_uid(pamh, val, param.start_uid, PATH_LOGIN_DEFS":UID_MIN");
204 }
205
206 /* Parse parameter values
207 * Must come after pam_modutil_search_key so that the current system
208 * default for UID_MIN is already in p.start_uid to serve as default
209 * for str_to_uid in case of a parse error.
210 * */
211 parse_params(pamh, argc, argv, ¶m);
212
213 if (param.debug >= 1)
214 pam_syslog(pamh, LOG_DEBUG, "Config: startuid=%u enduid=%u fs=%s "
215 "debug=%d overwrite=%d",
216 param.start_uid, param.end_uid,
217 param.fs ? param.fs : "(none)",
218 param.debug, param.overwrite);
219
220 /* Determine the user name so we can get the home directory */
221 retval = pam_get_item(pamh, PAM_USER, &user);
222 if (retval != PAM_SUCCESS || user == NULL || *(const char *)user == '\0') {
223 pam_syslog(pamh, LOG_NOTICE, "user unknown");
224 return PAM_USER_UNKNOWN;
225 }
226
227 /* Get the password entry */
228 pwd = pam_modutil_getpwnam(pamh, user);
229 if (pwd == NULL) {
230 pam_syslog(pamh, LOG_NOTICE, "user unknown");
231 return PAM_USER_UNKNOWN;
232 }
233
234 /* Check if we should not set quotas for user */
235 if ((pwd->pw_uid < param.start_uid) ||
236 ((param.end_uid >= param.start_uid) && (param.start_uid != 0) &&
237 (pwd->pw_uid > param.end_uid)))
238 return PAM_SUCCESS;
239
240 /* Find out what device the filesystem is hosted on */
241 if ((fp = setmntent("/proc/mounts", "r")) == NULL) {
242 pam_syslog(pamh, LOG_ERR, "Unable to open /proc/mounts");
243 return PAM_PERM_DENIED;
244 }
245
246 while (
247 #ifdef HAVE_GETMNTENT_R
248 (mnt = getmntent_r(fp, &ent, buf, sizeof(buf))) != NULL
249 #else
250 (mnt = getmntent(fp)) != NULL
251 #endif
252 ) {
253 /* If param.fs is not specified use filesystem with users homedir
254 * as default.
255 */
256 if (param.fs == NULL) {
257 /* Mask trailing / from mnt->mnt_dir, to get a leading / on the
258 * remaining suffix returned by pam_str_skip_prefix_len()
259 */
260 for (mnt_len = strlen(mnt->mnt_dir); mnt_len > 0; --mnt_len)
261 if (mnt->mnt_dir[mnt_len - 1] != '/')
262 break;
263 const char *s;
264 if (param.debug >= 2)
265 pam_syslog(pamh, LOG_DEBUG, "Trying to match pw_dir=\"%s\" "
266 "with mnt_dir=\"%s\"", pwd->pw_dir, mnt->mnt_dir);
267 /*
268 * (mnt_len > match_size) Only try matching the mnt_dir if its length
269 * is longer than the last matched length, trying to find the longest
270 * mnt_dir for a given pwd_dir.
271 *
272 * (mnt_len == 0 && mnt->mnt_dir[0] == '/') special-cases the
273 * root-dir /, which is the only mnt_dir with a trailing '/', which
274 * got masked earlier.
275 */
276 if ((mnt_len > match_size || (mnt_len == 0 && mnt->mnt_dir[0] == '/')) &&
277 (s = pam_str_skip_prefix_len(pwd->pw_dir, mnt->mnt_dir, mnt_len)) != NULL &&
278 s[0] == '/') {
279 free(mntdevice);
280 if ((mntdevice = strdup(mnt->mnt_fsname)) == NULL) {
281 pam_syslog(pamh, LOG_CRIT, "Memory allocation error");
282 endmntent(fp);
283 return PAM_PERM_DENIED;
284 }
285 match_size = mnt_len;
286 if (param.debug >= 2)
287 pam_syslog(pamh, LOG_DEBUG, "Found pw_dir=\"%s\" in mnt_dir=\"%s\" "
288 "with suffix=\"%s\" on device=\"%s\"", pwd->pw_dir,
289 mnt->mnt_dir, s, mntdevice);
290 }
291 /* param.fs has been specified, find exactly matching filesystem */
292 } else if ((strncmp(param.fs, mnt->mnt_dir, param.fs_len) == 0
293 && mnt->mnt_dir[param.fs_len] == '\0') ||
294 (strncmp(param.fs, mnt->mnt_fsname, param.fs_len) == 0
295 && mnt->mnt_fsname[param.fs_len] == '\0' )) {
296 free(mntdevice);
297 if ((mntdevice = strdup(mnt->mnt_fsname)) == NULL) {
298 pam_syslog(pamh, LOG_CRIT, "Memory allocation error");
299 endmntent(fp);
300 return PAM_PERM_DENIED;
301 }
302 if (param.debug >= 2)
303 pam_syslog(pamh, LOG_DEBUG, "Found fs=\"%s\" in mnt_dir=\"%s\" "
304 "on device=\"%s\"", param.fs, mnt->mnt_dir, mntdevice);
305 }
306 }
307
308 endmntent(fp);
309
310 if (mntdevice == NULL) {
311 pam_syslog(pamh, LOG_ERR, "Filesystem or device not found: %s", param.fs ? param.fs : pwd->pw_dir);
312 return PAM_PERM_DENIED;
313 }
314
315 /* Get limits */
316 if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), mntdevice, pwd->pw_uid,
317 (void *)&ndqblk) == -1) {
318 pam_syslog(pamh, LOG_ERR, "fail to get limits for user %s : %m",
319 pwd->pw_name);
320 free(mntdevice);
321 return PAM_PERM_DENIED;
322 }
323
324 if (param.debug >= 1)
325 debug(pamh, &ndqblk, mntdevice, "Quota read:");
326
327 /* Only overwrite if quotas aren't already set or if overwrite is set */
328 if ((ndqblk.dqb_bsoftlimit == 0 && ndqblk.dqb_bhardlimit == 0 &&
329 ndqblk.dqb_isoftlimit == 0 && ndqblk.dqb_ihardlimit == 0) ||
330 param.overwrite == 1) {
331
332 /* Parse new limits
333 * Exit with an error should only the hard- or softlimit be
334 * configured but not both.
335 * This avoids errors, inconsistencies and possible race conditions
336 * during setquota.
337 */
338 ndqblk.dqb_valid = 0;
339 if (!parse_dqblk(pamh, argc, argv, &ndqblk)) {
340 pam_syslog(pamh, LOG_ERR,
341 "Both soft- and hardlimits for %s need to be configured "
342 "at the same time!", mntdevice);
343 free(mntdevice);
344 return PAM_PERM_DENIED;
345 }
346
347 /* Nothing changed? Are no limits defined at all in configuration? */
348 if (ndqblk.dqb_valid == 0) {
349 pam_syslog(pamh, LOG_AUTH | LOG_WARNING, "no limits defined in "
350 "configuration for user %s on %s", pwd->pw_name, mntdevice);
351 free(mntdevice);
352 return PAM_IGNORE;
353 }
354
355 /* Set limits */
356 if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), mntdevice, pwd->pw_uid,
357 (void *)&ndqblk) == -1) {
358 pam_syslog(pamh, LOG_ERR, "failed to set limits for user %s on %s: %m",
359 pwd->pw_name, mntdevice);
360 free(mntdevice);
361 return PAM_PERM_DENIED;
362 }
363 if (param.debug >= 1)
364 debug(pamh, &ndqblk, mntdevice, "Quota set:");
365
366 /* End module */
367 free(mntdevice);
368 return PAM_SUCCESS;
369
370 } else {
371 /* Quota exists and overwrite!=1 */
372 if (param.debug >= 1) {
373 pam_syslog(pamh, LOG_DEBUG, "Quota already exists for user %s "
374 "on %s, not overwriting it without \"overwrite=1\"",
375 pwd->pw_name, mntdevice);
376 }
377 /* End module */
378 free(mntdevice);
379 return PAM_IGNORE;
380 }
381
382 }
383
384 int
385 pam_sm_close_session(pam_handle_t *pamh UNUSED, int flags UNUSED,
386 int argc UNUSED, const char **argv UNUSED)
387 {
388 return PAM_SUCCESS;
389 }