public inbox for glibc-cvs@sourceware.org
help / color / mirror / Atom feed
* [glibc/maskray/grte] Integrate nss_{borg, cache} local changes from glibc-2.18 to 2.19
@ 2021-08-27 23:23 Fangrui Song
0 siblings, 0 replies; only message in thread
From: Fangrui Song @ 2021-08-27 23:23 UTC (permalink / raw)
To: glibc-cvs
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=eb0025167791355d35cd52776d3976db03e0530e
commit eb0025167791355d35cd52776d3976db03e0530e
Author: Paul Pluzhnikov <ppluzhnikov@google.com>
Date: Fri Feb 28 12:54:06 2014 -0800
Integrate nss_{borg,cache} local changes from glibc-2.18 to 2.19
Diff:
---
include/grp.h | 1 +
include/pwd.h | 2 +
include/shadow.h | 1 +
nss/Makefile | 12 +-
nss/Versions | 29 ++
nss/function.def | 10 +
nss/nss_borg/borg-pwd.c | 171 ++++++++
nss/nss_cache/nss_cache.c | 978 ++++++++++++++++++++++++++++++++++++++++++++++
nss/nss_cache/nss_cache.h | 65 +++
shlib-versions | 2 +
10 files changed, 1268 insertions(+), 3 deletions(-)
diff --git a/include/grp.h b/include/grp.h
index 871701adbe..16dd8bd2e7 100644
--- a/include/grp.h
+++ b/include/grp.h
@@ -53,6 +53,7 @@ extern enum nss_status _nss_ ## service ##_initgroups_dyn \
long int *size, gid_t **groupsp, long int limit, \
int *errnop);
+DECLARE_NSS_PROTOTYPES (cache)
DECLARE_NSS_PROTOTYPES (compat)
DECLARE_NSS_PROTOTYPES (files)
DECLARE_NSS_PROTOTYPES (hesiod)
diff --git a/include/pwd.h b/include/pwd.h
index fc995065d9..de8272cdd9 100644
--- a/include/pwd.h
+++ b/include/pwd.h
@@ -45,8 +45,10 @@ extern enum nss_status _nss_ ## service ##_getpwent_r \
(struct passwd *result, char *buffer, \
size_t buflen, int *errnop);
+DECLARE_NSS_PROTOTYPES (cache)
DECLARE_NSS_PROTOTYPES (compat)
DECLARE_NSS_PROTOTYPES (files)
+DECLARE_NSS_PROTOTYPES (borg)
DECLARE_NSS_PROTOTYPES (hesiod)
DECLARE_NSS_PROTOTYPES (nis)
DECLARE_NSS_PROTOTYPES (nisplus)
diff --git a/include/shadow.h b/include/shadow.h
index 366ea83482..3b7bf777d9 100644
--- a/include/shadow.h
+++ b/include/shadow.h
@@ -41,6 +41,7 @@ extern enum nss_status _nss_ ## service ## _getspnam_r \
(const char *name, struct spwd *pwd, \
char *buffer, size_t buflen, int *errnop);
+DECLARE_NSS_PROTOTYPES (cache)
DECLARE_NSS_PROTOTYPES (compat)
DECLARE_NSS_PROTOTYPES (files)
DECLARE_NSS_PROTOTYPES (hesiod)
diff --git a/nss/Makefile b/nss/Makefile
index a5cd2aacae..375f060b65 100644
--- a/nss/Makefile
+++ b/nss/Makefile
@@ -73,7 +73,7 @@ tests += tst-cancel-getpwuid_r
endif
# Specify rules for the nss_* modules. We have some services.
-services := files db compat
+services := files db compat borg cache
extra-libs = $(services:%=libnss_%)
# These libraries will be built in the `others' pass rather than
@@ -87,6 +87,8 @@ vpath %.c $(subdir-dirs) ../locale/programs ../intl
libnss_files-routines := $(addprefix files-,$(databases)) \
files-initgroups files-init
+libnss_borg-routines := borg-pwd
+libnss_cache-routines := nss_cache
libnss_db-dbs := $(addprefix db-,\
$(filter-out hosts network key alias,\
@@ -104,10 +106,14 @@ install-others += $(inst_vardbdir)/Makefile
# Build static module into libc if requested
libnss_files-inhibit-o = $(filter-out .os,$(object-suffixes))
libnss_db-inhibit-o = $(filter-out .os,$(object-suffixes))
+libnss_borg-inhibit-o = $(filter-out .os,$(object-suffixes))
+libnss_cache-inhibit-o = $(filter-out .os,$(object-suffixes))
libnss_compat-inhibit-o = $(filter-out .os,$(object-suffixes))
ifeq ($(build-static-nss),yes)
-routines += $(libnss_files-routines)
-static-only-routines += $(libnss_files-routines)
+routines += $(libnss_files-routines) $(libnss_borg-routines) \
+ $(libnss_cache-routines)
+static-only-routines += $(libnss_files-routines) $(libnss_borg-routines) \
+ $(libnss_cache-routines)
tests-static += tst-nss-static
endif
extra-test-objs += nss_test1.os nss_test2.os
diff --git a/nss/Versions b/nss/Versions
index db8c887720..f472d64d59 100644
--- a/nss/Versions
+++ b/nss/Versions
@@ -174,3 +174,32 @@ libnss_compat {
_nss_compat_initgroups_dyn;
}
}
+
+libnss_borg {
+ GLIBC_PRIVATE {
+ _nss_borg_setpwent;
+ _nss_borg_endpwent;
+ _nss_borg_getpwent_r;
+ _nss_borg_getpwnam_r;
+ _nss_borg_getpwuid_r;
+ }
+}
+
+libnss_cache {
+ GLIBC_PRIVATE {
+ _nss_cache_setpwent;
+ _nss_cache_endpwent;
+ _nss_cache_getpwent_r;
+ _nss_cache_getpwuid_r;
+ _nss_cache_getpwnam_r;
+ _nss_cache_setgrent;
+ _nss_cache_endgrent;
+ _nss_cache_getgrent_r;
+ _nss_cache_getgrgid_r;
+ _nss_cache_getgrnam_r;
+ _nss_cache_setspent;
+ _nss_cache_endspent;
+ _nss_cache_getspent_r;
+ _nss_cache_getspnam_r;
+ }
+}
diff --git a/nss/function.def b/nss/function.def
index 1a38d5a852..6464a921e7 100644
--- a/nss/function.def
+++ b/nss/function.def
@@ -31,6 +31,9 @@ DEFINE_ENT (files, ether)
DEFINE_ENT (files, gr)
DEFINE_GET (files, grgid)
DEFINE_GET (files, grnam)
+DEFINE_ENT (cache, gr)
+DEFINE_GET (cache, grgid)
+DEFINE_GET (cache, grnam)
/* hosts */
DEFINE_ENT (files, host)
@@ -62,6 +65,11 @@ DEFINE_GETBY (files, proto, number)
DEFINE_ENT (files, pw)
DEFINE_GET (files, pwnam)
DEFINE_GET (files, pwuid)
+DEFINE_GET (borg, pwnam) /* /etc/passwd2 */
+DEFINE_GET (borg, pwuid)
+DEFINE_ENT (cache, pw)
+DEFINE_GET (cache, pwnam)
+DEFINE_GET (cache, pwuid)
/* rpc */
DEFINE_ENT (files, rpc)
@@ -76,3 +84,5 @@ DEFINE_GETBY (files, serv, port)
/* shadow */
DEFINE_ENT (files, sp)
DEFINE_GET (files, spnam)
+DEFINE_ENT (cache, sp)
+DEFINE_GET (cache, spnam)
diff --git a/nss/nss_borg/borg-pwd.c b/nss/nss_borg/borg-pwd.c
new file mode 100644
index 0000000000..dc8206a489
--- /dev/null
+++ b/nss/nss_borg/borg-pwd.c
@@ -0,0 +1,171 @@
+// Copyright 2004 Google Inc.
+// Author: Paul Menage
+
+// An NSS module that extends local user account lookup to the file /etc/passwd.borg
+
+#include <stdio.h>
+#include <pwd.h>
+#include <sys/types.h>
+#include <nss.h>
+#include <errno.h>
+#include <string.h>
+
+#ifdef NSSBORG_STANDALONE
+#include <pthread.h>
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+#define NSSBORG_LOCK pthread_mutex_lock(&mutex)
+#define NSSBORG_UNLOCK pthread_mutex_unlock(&mutex)
+#else
+#include <bits/libc-lock.h>
+__libc_lock_define_initialized (static, lock)
+#define NSSBORG_LOCK __libc_lock_lock (lock)
+#define NSSBORG_UNLOCK __libc_lock_unlock (lock);
+#endif
+
+static FILE *f;
+
+#define DEBUG(fmt, ...)
+
+// _nss_borg_setpwent_locked()
+// Internal setup routine
+
+static enum nss_status _nss_borg_setpwent_locked(void) {
+
+ DEBUG("Opening passwd.borg\n");
+ f = fopen("/etc/passwd.borg", "r");
+
+ if (f) {
+ return NSS_STATUS_SUCCESS;
+ } else {
+ return NSS_STATUS_UNAVAIL;
+ }
+}
+
+// _nss_borg_setpwent()
+// Called by NSS to open the passwd file
+// Oddly, NSS passes a boolean saying whether to keep the database file open; ignore it
+
+enum nss_status _nss_borg_setpwent(int stayopen) {
+ enum nss_status ret;
+ NSSBORG_LOCK;
+ ret = _nss_borg_setpwent_locked();
+ NSSBORG_UNLOCK;
+ return ret;
+}
+
+// _nss_borg_endpwent_locked()
+// Internal close routine
+
+static enum nss_status _nss_borg_endpwent_locked(void) {
+
+ DEBUG("Closing passwd.borg\n");
+ if (f) {
+ fclose(f);
+ f = NULL;
+ }
+ return NSS_STATUS_SUCCESS;
+}
+
+// _nss_borg_endpwent()
+// Called by NSS to close the passwd file
+
+enum nss_status _nss_borg_endpwent() {
+ enum nss_status ret;
+ NSSBORG_LOCK;
+ ret = _nss_borg_endpwent_locked();
+ NSSBORG_UNLOCK;
+ return ret;
+}
+
+// _nss_borg_getpwent_r_locked()
+// Called internally to return the next entry from the passwd file
+
+static enum nss_status _nss_borg_getpwent_r_locked(struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ enum nss_status ret;
+
+ if (fgetpwent_r(f, result, buffer, buflen, &result) == 0) {
+ DEBUG("Returning user %d:%s\n", result->pw_uid, result->pw_name);
+ ret = NSS_STATUS_SUCCESS;
+ } else {
+ *errnop = errno;
+ switch (*errnop) {
+ case ERANGE:
+ ret = NSS_STATUS_TRYAGAIN;
+ break;
+ case ENOENT:
+ default:
+ ret = NSS_STATUS_NOTFOUND;
+ }
+ }
+
+ return ret;
+}
+
+// _nss_borg_getpwent_r()
+// Called by NSS (I think) to look up next entry in passwd file
+enum nss_status _nss_borg_getpwent_r(struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ enum nss_status ret;
+ NSSBORG_LOCK;
+ ret = _nss_borg_getpwent_r_locked(result, buffer, buflen, errnop);
+ NSSBORG_UNLOCK;
+ return ret;
+}
+
+// _nss_borg_getpwuid_r()
+// Find a user account by uid
+
+enum nss_status _nss_borg_getpwuid_r(uid_t uid, struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ enum nss_status ret;
+
+ NSSBORG_LOCK;
+ ret = _nss_borg_setpwent_locked();
+ DEBUG("Looking for uid %d\n", uid);
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_borg_getpwent_r_locked(result, buffer, buflen, errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (result->pw_uid == uid)
+ break;
+ }
+ }
+
+ _nss_borg_endpwent_locked();
+ NSSBORG_UNLOCK;
+
+ return ret;
+}
+
+// _nss_borg_getpwnam_r()
+// Find a user account by name
+
+enum nss_status _nss_borg_getpwnam_r(const char *name, struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ enum nss_status ret;
+
+ NSSBORG_LOCK;
+ ret = _nss_borg_setpwent_locked();
+ DEBUG("Looking for user %s\n", name);
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_borg_getpwent_r_locked(result, buffer, buflen, errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (!strcmp(result->pw_name, name))
+ break;
+ }
+ }
+
+ _nss_borg_endpwent_locked();
+ NSSBORG_UNLOCK;
+
+ return ret;
+}
diff --git a/nss/nss_cache/nss_cache.c b/nss/nss_cache/nss_cache.c
new file mode 100644
index 0000000000..6613dbbeea
--- /dev/null
+++ b/nss/nss_cache/nss_cache.c
@@ -0,0 +1,978 @@
+/* Copyright 2009 Google Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA
+ */
+
+/* An NSS module which adds supports for file maps with a trailing .cache
+ * suffix (/etc/passwd.cache, /etc/group.cache, and /etc/shadow.cache)
+ */
+
+#include "nss_cache.h"
+
+// Locking implementation: use pthreads.
+#include <pthread.h>
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+
+#pragma weak pthread_mutex_lock
+#define NSS_CACHE_LOCK() do { \
+ if (&pthread_mutex_lock != NULL) pthread_mutex_lock(&mutex); \
+ } while (0)
+
+#pragma weak pthread_mutex_unlock
+#define NSS_CACHE_UNLOCK() do { \
+ if (&pthread_mutex_unlock != NULL) pthread_mutex_unlock(&mutex); \
+ } while (0)
+
+static FILE *p_file = NULL;
+static FILE *g_file = NULL;
+static FILE *s_file = NULL;
+static char p_filename[NSS_CACHE_PATH_LENGTH] = "/etc/passwd.cache";
+static char g_filename[NSS_CACHE_PATH_LENGTH] = "/etc/group.cache";
+static char s_filename[NSS_CACHE_PATH_LENGTH] = "/etc/shadow.cache";
+
+/* Common return code routine for all *ent_r_locked functions.
+ * We need to return TRYAGAIN if the underlying files guy raises ERANGE,
+ * so that our caller knows to try again with a bigger buffer.
+ */
+
+static inline enum nss_status _nss_cache_ent_bad_return_code(int errnoval) {
+ enum nss_status ret;
+
+ switch (errnoval) {
+ case ERANGE:
+ DEBUG("ERANGE: Try again with a bigger buffer\n");
+ ret = NSS_STATUS_TRYAGAIN;
+ break;
+ case ENOENT:
+ default:
+ DEBUG("ENOENT or default case: Not found\n");
+ ret = NSS_STATUS_NOTFOUND;
+ };
+ return ret;
+}
+
+//
+// Binary search routines below here
+//
+
+// _nss_cache_bsearch_lookup()
+// Binary search through a sorted nss file for a single record.
+
+enum nss_status _nss_cache_bsearch_lookup(FILE *file,
+ struct nss_cache_args *args,
+ int *errnop) {
+ enum nss_cache_match (*lookup)(
+ FILE *,
+ struct nss_cache_args *
+ ) = args->lookup_function;
+ long min = 0;
+ long max;
+ long pos;
+
+ // get the size of the file
+ if (fseek(file, 0, SEEK_END) != 0) {
+ DEBUG("fseek fail\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+ max = ftell(file);
+
+ // binary search until we are within 100 chars of the right line
+ while (min + 100 < max) {
+ pos = (min + max) / 2;
+
+ if (fseek(file, pos, SEEK_SET) != 0) {
+ DEBUG("fseek fail\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ // scan forward to the start of the next line.
+ for (;;) {
+ int c = getc_unlocked(file);
+ if (c == EOF) break;
+ ++pos;
+ if (c == '\n') break;
+ }
+
+ // break if we've stopped making progress in this loop (long lines)
+ if (pos <= min || pos >= max) {
+ break;
+ }
+
+ // see if this line matches
+ switch (lookup(file, args)) {
+ case NSS_CACHE_EXACT:
+ return NSS_STATUS_SUCCESS; // done!
+ case NSS_CACHE_HIGH:
+ max = pos;
+ continue; // search again
+ case NSS_CACHE_LOW:
+ min = pos;
+ continue; // search again
+ case NSS_CACHE_ERROR:
+ if (errno == ERANGE) {
+ // let the caller retry
+ *errnop = errno;
+ return _nss_cache_ent_bad_return_code(*errnop);
+ }
+ DEBUG("expected error %s [errno=%d] from lookup function\n",
+ strerror(errno), errno);
+ return NSS_STATUS_UNAVAIL;
+ }
+ }
+
+ // fall back on a linear search in the remaining space
+ DEBUG("Switching to linear scan\n");
+ pos = min - 100; // back 100, might be in the middle of the right line
+ if (fseek(file, pos, SEEK_SET) != 0) {
+ DEBUG("fseek fail\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ while (pos < max) {
+ switch (lookup(file, args)) {
+ case NSS_CACHE_EXACT:
+ return NSS_STATUS_SUCCESS;
+ case NSS_CACHE_HIGH:
+ case NSS_CACHE_LOW:
+ pos = ftell(file);
+ continue;
+ case NSS_CACHE_ERROR:
+ if (errno == ERANGE) {
+ // let the caller retry
+ *errnop = errno;
+ return _nss_cache_ent_bad_return_code(*errnop);
+ }
+ break;
+ }
+ break;
+ }
+
+ return NSS_STATUS_NOTFOUND;
+}
+
+// _nss_cache_bsearch()
+// If a sorted nss file is present, attempt a binary search on it.
+
+enum nss_status _nss_cache_bsearch(struct nss_cache_args *args, int *errnop) {
+ FILE *file = NULL;
+ struct stat system_file;
+ struct stat sorted_file;
+ enum nss_status ret;
+
+ file = fopen(args->sorted_filename, "r");
+ if (file == NULL) {
+ DEBUG("error opening %s\n", args->sorted_filename);
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ // if the sorted file is older than the system file, do not risk stale
+ // data and abort
+ // TODO(vasilios): should be a compile or runtime option
+ if (stat(args->system_filename, &system_file) != 0) {
+ DEBUG("failed to stat %s\n", args->system_filename);
+ fclose(file);
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (fstat(fileno(file), &sorted_file) != 0) {
+ DEBUG("failed to stat %s\n", args->sorted_filename);
+ fclose(file);
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (difftime(system_file.st_mtime, sorted_file.st_mtime) > 0) {
+ DEBUG("%s may be stale, aborting lookup\n", args->sorted_filename);
+ fclose(file);
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ ret = _nss_cache_bsearch_lookup(file, args, errnop);
+
+ fclose(file);
+ return ret;
+
+}
+
+//
+// Routines for passwd map defined below here
+//
+
+// _nss_cache_setpwent_path()
+// Helper function for testing
+
+extern char* _nss_cache_setpwent_path(const char *path) {
+
+ DEBUG("%s %s\n", "Setting p_filename to", path);
+ return strncpy(p_filename, path, NSS_CACHE_PATH_LENGTH - 1);
+
+}
+
+// _nss_cache_pwuid_wrap()
+// Internal wrapper for binary searches, using uid-specific calls.
+
+static enum nss_cache_match _nss_cache_pwuid_wrap(FILE *file,
+ struct nss_cache_args *args) {
+ struct passwd *result = args->lookup_result;
+ uid_t *uid = args->lookup_value;
+
+ if (fgetpwent_r(file, result, args->buffer, args->buflen, &result) == 0) {
+ if (result->pw_uid == *uid) {
+ DEBUG("SUCCESS: found user %d:%s\n", result->pw_uid, result->pw_name);
+ return NSS_CACHE_EXACT;
+ }
+ DEBUG("Failed match at uid %d\n", result->pw_uid);
+ if (result->pw_uid > *uid) {
+ return NSS_CACHE_HIGH;
+ } else {
+ return NSS_CACHE_LOW;
+ }
+ }
+
+ return NSS_CACHE_ERROR;
+}
+
+// _nss_cache_pwnam_wrap()
+// Internal wrapper for binary searches, using username-specific calls.
+
+static enum nss_cache_match _nss_cache_pwnam_wrap(FILE *file,
+ struct nss_cache_args *args) {
+ struct passwd *result = args->lookup_result;
+ char *name = args->lookup_value;
+ int ret;
+
+ if (fgetpwent_r(file, result, args->buffer, args->buflen, &result) == 0) {
+ ret = strcoll(result->pw_name, name);
+ if (ret == 0) {
+ DEBUG("SUCCESS: found user %s\n", result->pw_name);
+ return NSS_CACHE_EXACT;
+ }
+ DEBUG("Failed match at name %s\n", result->pw_name);
+ if (ret > 0) {
+ return NSS_CACHE_HIGH;
+ } else {
+ return NSS_CACHE_LOW;
+ }
+ }
+
+ return NSS_CACHE_ERROR;
+}
+
+// _nss_cache_setpwent_locked()
+// Internal setup routine
+
+static enum nss_status _nss_cache_setpwent_locked(void) {
+
+ DEBUG("%s %s\n", "Opening", p_filename);
+ p_file = fopen(p_filename, "r");
+
+ if (p_file) {
+ return NSS_STATUS_SUCCESS;
+ } else {
+ return NSS_STATUS_UNAVAIL;
+ }
+}
+
+// _nss_cache_setpwent()
+// Called by NSS to open the passwd file
+// 'stayopen' parameter is ignored.
+
+enum nss_status _nss_cache_setpwent(int stayopen) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_setpwent_locked();
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_endpwent_locked()
+// Internal close routine
+
+static enum nss_status _nss_cache_endpwent_locked(void) {
+
+ DEBUG("Closing passwd.cache\n");
+ if (p_file) {
+ fclose(p_file);
+ p_file = NULL;
+ }
+ return NSS_STATUS_SUCCESS;
+}
+
+// _nss_cache_endpwent()
+// Called by NSS to close the passwd file
+
+enum nss_status _nss_cache_endpwent(void) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_endpwent_locked();
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_getpwent_r_locked()
+// Called internally to return the next entry from the passwd file
+
+static enum nss_status _nss_cache_getpwent_r_locked(struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ enum nss_status ret = NSS_STATUS_SUCCESS;
+
+ if (p_file == NULL) {
+ DEBUG("p_file == NULL, going to setpwent\n");
+ ret = _nss_cache_setpwent_locked();
+ }
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ if (fgetpwent_r(p_file, result, buffer, buflen, &result) == 0) {
+ DEBUG("Returning user %d:%s\n", result->pw_uid, result->pw_name);
+ } else {
+ *errnop = errno;
+ ret = _nss_cache_ent_bad_return_code(*errnop);
+ }
+ }
+
+ return ret;
+}
+
+// _nss_cache_getpwent_r()
+// Called by NSS to look up next entry in passwd file
+
+enum nss_status _nss_cache_getpwent_r(struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_getpwent_r_locked(result, buffer, buflen, errnop);
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_getpwuid_r()
+// Find a user account by uid
+
+enum nss_status _nss_cache_getpwuid_r(uid_t uid, struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ char filename[NSS_CACHE_PATH_LENGTH];
+ struct nss_cache_args args;
+ enum nss_status ret;
+
+ strncpy(filename, p_filename, NSS_CACHE_PATH_LENGTH - 1);
+ if (strlen(filename) > NSS_CACHE_PATH_LENGTH - 7) {
+ DEBUG("filename too long\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncat(filename, ".byuid", 6);
+
+ args.sorted_filename = filename;
+ args.system_filename = p_filename;
+ args.lookup_function = _nss_cache_pwuid_wrap;
+ args.lookup_value = &uid;
+ args.lookup_result = result;
+ args.buffer = buffer;
+ args.buflen = buflen;
+
+ DEBUG("Binary search for uid %d\n", uid);
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_bsearch(&args, errnop);
+
+ // TODO(vasilios): make this a runtime or compile-time option, as this slows
+ // down legitimate misses as the trade off for safety.
+ if (ret == NSS_STATUS_NOTFOUND) {
+ DEBUG("Binary search returned nothing.\n");
+ ret = NSS_STATUS_UNAVAIL;
+ }
+
+ if (ret == NSS_STATUS_UNAVAIL) {
+ DEBUG("Binary search failed, falling back to full linear search\n");
+ ret = _nss_cache_setpwent_locked();
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_cache_getpwent_r_locked(result,
+ buffer,
+ buflen,
+ errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (result->pw_uid == uid)
+ break;
+ }
+ }
+ }
+
+ _nss_cache_endpwent_locked();
+ NSS_CACHE_UNLOCK();
+
+ return ret;
+}
+
+// _nss_cache_getpwnam_r()
+// Find a user account by name
+
+enum nss_status _nss_cache_getpwnam_r(const char *name, struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ char *pw_name;
+ char filename[NSS_CACHE_PATH_LENGTH];
+ struct nss_cache_args args;
+ enum nss_status ret;
+
+ NSS_CACHE_LOCK();
+
+ // name is a const char, we need a non-const copy
+ pw_name = malloc(strlen(name) + 1);
+ if (pw_name == NULL) {
+ DEBUG("malloc error\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncpy(pw_name, name, strlen(name) + 1);
+
+ strncpy(filename, p_filename, NSS_CACHE_PATH_LENGTH - 1);
+ if (strlen(filename) > NSS_CACHE_PATH_LENGTH - 8) {
+ DEBUG("filename too long\n");
+ free(pw_name);
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncat(filename, ".byname", 7);
+
+ args.sorted_filename = filename;
+ args.system_filename = p_filename;
+ args.lookup_function = _nss_cache_pwnam_wrap;
+ args.lookup_value = pw_name;
+ args.lookup_result = result;
+ args.buffer = buffer;
+ args.buflen = buflen;
+
+ DEBUG("Binary search for user %s\n", pw_name);
+ ret = _nss_cache_bsearch(&args, errnop);
+
+ // TODO(vasilios): make this a runtime or compile-time option, as this slows
+ // down legitimate misses as the trade off for safety.
+ if (ret == NSS_STATUS_NOTFOUND) {
+ DEBUG("Binary search returned nothing.\n");
+ ret = NSS_STATUS_UNAVAIL;
+ }
+
+ if (ret == NSS_STATUS_UNAVAIL) {
+ DEBUG("Binary search failed, falling back to full linear search\n");
+ ret = _nss_cache_setpwent_locked();
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_cache_getpwent_r_locked(result,
+ buffer,
+ buflen,
+ errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (!strcmp(result->pw_name, name))
+ break;
+ }
+ }
+ }
+
+ free(pw_name);
+ _nss_cache_endpwent_locked();
+ NSS_CACHE_UNLOCK();
+
+ return ret;
+}
+
+//
+// Routines for group map defined here.
+//
+
+// _nss_cache_setgrent_path()
+// Helper function for testing
+
+extern char* _nss_cache_setgrent_path(const char *path) {
+
+ DEBUG("%s %s\n", "Setting g_filename to", path);
+ return strncpy(g_filename, path, NSS_CACHE_PATH_LENGTH - 1);
+
+}
+
+// _nss_cache_setgrent_locked()
+// Internal setup routine
+
+static enum nss_status _nss_cache_setgrent_locked(void) {
+
+ DEBUG("%s %s\n", "Opening", g_filename);
+ g_file = fopen(g_filename, "r");
+
+ if (g_file) {
+ return NSS_STATUS_SUCCESS;
+ } else {
+ return NSS_STATUS_UNAVAIL;
+ }
+}
+
+// _nss_cache_grgid_wrap()
+// Internal wrapper for binary searches, using gid-specific calls.
+
+static enum nss_cache_match _nss_cache_grgid_wrap(FILE *file,
+ struct nss_cache_args *args) {
+ struct group *result = args->lookup_result;
+ gid_t *gid = args->lookup_value;
+
+ if (fgetgrent_r(file, result, args->buffer, args->buflen, &result) == 0) {
+ if (result->gr_gid == *gid) {
+ DEBUG("SUCCESS: found group %d:%s\n", result->gr_gid, result->gr_name);
+ return NSS_CACHE_EXACT;
+ }
+ DEBUG("Failed match at gid %d\n", result->gr_gid);
+ if (result->gr_gid > *gid) {
+ return NSS_CACHE_HIGH;
+ } else {
+ return NSS_CACHE_LOW;
+ }
+ }
+
+ return NSS_CACHE_ERROR;
+}
+
+// _nss_cache_grnam_wrap()
+// Internal wrapper for binary searches, using groupname-specific calls.
+
+static enum nss_cache_match _nss_cache_grnam_wrap(FILE *file,
+ struct nss_cache_args *args) {
+ struct group *result = args->lookup_result;
+ char *name = args->lookup_value;
+ int ret;
+
+ if (fgetgrent_r(file, result, args->buffer, args->buflen, &result) == 0) {
+ ret = strcoll(result->gr_name, name);
+ if (ret == 0) {
+ DEBUG("SUCCESS: found group %s\n", result->gr_name);
+ return NSS_CACHE_EXACT;
+ }
+ DEBUG("Failed match at name %s\n", result->gr_name);
+ if (ret > 0) {
+ return NSS_CACHE_HIGH;
+ } else {
+ return NSS_CACHE_LOW;
+ }
+ }
+
+ return NSS_CACHE_ERROR;
+}
+
+// _nss_cache_setgrent()
+// Called by NSS to open the group file
+// 'stayopen' parameter is ignored.
+
+enum nss_status _nss_cache_setgrent(int stayopen) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_setgrent_locked();
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_endgrent_locked()
+// Internal close routine
+
+static enum nss_status _nss_cache_endgrent_locked(void) {
+
+ DEBUG("Closing group.cache\n");
+ if (g_file) {
+ fclose(g_file);
+ g_file = NULL;
+ }
+ return NSS_STATUS_SUCCESS;
+}
+
+// _nss_cache_endgrent()
+// Called by NSS to close the group file
+
+enum nss_status _nss_cache_endgrent(void) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_endgrent_locked();
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_getgrent_r_locked()
+// Called internally to return the next entry from the group file
+
+static enum nss_status _nss_cache_getgrent_r_locked(struct group *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ enum nss_status ret = NSS_STATUS_SUCCESS;
+
+ if (g_file == NULL) {
+ DEBUG("g_file == NULL, going to setgrent\n");
+ ret = _nss_cache_setgrent_locked();
+ }
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ fpos_t position;
+
+ fgetpos(g_file, &position);
+ if (fgetgrent_r(g_file, result, buffer, buflen, &result) == 0) {
+ DEBUG("Returning group %s (%d)\n", result->gr_name, result->gr_gid);
+ } else {
+ /* Rewind back to where we were just before, otherwise the data read
+ * into the buffer is probably going to be lost because there's no
+ * guarantee that the caller is going to have preserved the line we
+ * just read. Note that glibc's nss/nss_files/files-XXX.c does
+ * something similar in CONCAT(_nss_files_get,ENTNAME_r) (around
+ * line 242 in glibc 2.4 sources).
+ */
+ fsetpos(g_file, &position);
+ *errnop = errno;
+ ret = _nss_cache_ent_bad_return_code(*errnop);
+ }
+ }
+
+ return ret;
+}
+
+// _nss_cache_getgrent_r()
+// Called by NSS to look up next entry in group file
+
+enum nss_status _nss_cache_getgrent_r(struct group *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_getgrent_r_locked(result, buffer, buflen, errnop);
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_getgrgid_r()
+// Find a group by gid
+
+enum nss_status _nss_cache_getgrgid_r(gid_t gid, struct group *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ char filename[NSS_CACHE_PATH_LENGTH];
+ struct nss_cache_args args;
+ enum nss_status ret;
+
+ strncpy(filename, g_filename, NSS_CACHE_PATH_LENGTH - 1);
+ if (strlen(filename) > NSS_CACHE_PATH_LENGTH - 7) {
+ DEBUG("filename too long\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncat(filename, ".bygid", 6);
+
+ args.sorted_filename = filename;
+ args.system_filename = g_filename;
+ args.lookup_function = _nss_cache_grgid_wrap;
+ args.lookup_value = &gid;
+ args.lookup_result = result;
+ args.buffer = buffer;
+ args.buflen = buflen;
+
+ DEBUG("Binary search for gid %d\n", gid);
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_bsearch(&args, errnop);
+
+ // TODO(vasilios): make this a runtime or compile-time option, as this slows
+ // down legitimate misses as the trade off for safety.
+ if (ret == NSS_STATUS_NOTFOUND) {
+ DEBUG("Binary search returned nothing.\n");
+ ret = NSS_STATUS_UNAVAIL;
+ }
+
+ if (ret == NSS_STATUS_UNAVAIL) {
+ DEBUG("Binary search failed, falling back to full linear search\n");
+ ret = _nss_cache_setgrent_locked();
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_cache_getgrent_r_locked(result,
+ buffer,
+ buflen,
+ errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (result->gr_gid == gid)
+ break;
+ }
+ }
+ }
+
+ _nss_cache_endgrent_locked();
+ NSS_CACHE_UNLOCK();
+
+ return ret;
+}
+
+// _nss_cache_getgrnam_r()
+// Find a group by name
+
+enum nss_status _nss_cache_getgrnam_r(const char *name, struct group *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ char *gr_name;
+ char filename[NSS_CACHE_PATH_LENGTH];
+ struct nss_cache_args args;
+ enum nss_status ret;
+
+ NSS_CACHE_LOCK();
+
+ // name is a const char, we need a non-const copy
+ gr_name = malloc(strlen(name) + 1);
+ if (gr_name == NULL) {
+ DEBUG("malloc error\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncpy(gr_name, name, strlen(name) + 1);
+
+ strncpy(filename, g_filename, NSS_CACHE_PATH_LENGTH - 1);
+ if (strlen(filename) > NSS_CACHE_PATH_LENGTH - 8) {
+ DEBUG("filename too long\n");
+ free(gr_name);
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncat(filename, ".byname", 7);
+
+ args.sorted_filename = filename;
+ args.system_filename = g_filename;
+ args.lookup_function = _nss_cache_grnam_wrap;
+ args.lookup_value = gr_name;
+ args.lookup_result = result;
+ args.buffer = buffer;
+ args.buflen = buflen;
+
+ DEBUG("Binary search for group %s\n", gr_name);
+ ret = _nss_cache_bsearch(&args, errnop);
+
+ // TODO(vasilios): make this a runtime or compile-time option, as this slows
+ // down legitimate misses as the trade off for safety.
+ if (ret == NSS_STATUS_NOTFOUND) {
+ DEBUG("Binary search returned nothing.\n");
+ ret = NSS_STATUS_UNAVAIL;
+ }
+
+ if (ret == NSS_STATUS_UNAVAIL) {
+ DEBUG("Binary search failed, falling back to full linear search\n");
+ ret = _nss_cache_setgrent_locked();
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_cache_getgrent_r_locked(result,
+ buffer,
+ buflen,
+ errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (!strcmp(result->gr_name, name))
+ break;
+ }
+ }
+ }
+
+ free(gr_name);
+ _nss_cache_endgrent_locked();
+ NSS_CACHE_UNLOCK();
+
+ return ret;
+}
+
+//
+// Routines for shadow map defined here.
+//
+
+// _nss_cache_setspent_path()
+// Helper function for testing
+
+extern char* _nss_cache_setspent_path(const char *path) {
+
+ DEBUG("%s %s\n", "Setting s_filename to", path);
+ return strncpy(s_filename, path, NSS_CACHE_PATH_LENGTH - 1);
+
+}
+
+// _nss_cache_setspent_locked()
+// Internal setup routine
+
+static enum nss_status _nss_cache_setspent_locked(void) {
+
+ DEBUG("%s %s\n", "Opening", g_filename);
+ s_file = fopen(s_filename, "r");
+
+ if (s_file) {
+ return NSS_STATUS_SUCCESS;
+ } else {
+ return NSS_STATUS_UNAVAIL;
+ }
+}
+
+// _nss_cache_spnam_wrap()
+// Internal wrapper for binary searches, using shadow-specific calls.
+
+static enum nss_cache_match _nss_cache_spnam_wrap(FILE *file,
+ struct nss_cache_args *args) {
+ struct spwd *result = args->lookup_result;
+ char *name = args->lookup_value;
+ int ret;
+
+ if (fgetspent_r(file, result, args->buffer, args->buflen, &result) == 0) {
+ ret = strcoll(result->sp_namp, name);
+ if (ret == 0) {
+ DEBUG("SUCCESS: found user %s\n", result->sp_namp);
+ return NSS_CACHE_EXACT;
+ }
+ DEBUG("Failed match at name %s\n", result->sp_namp);
+ if (ret > 0) {
+ return NSS_CACHE_HIGH;
+ } else {
+ return NSS_CACHE_LOW;
+ }
+ }
+
+ return NSS_CACHE_ERROR;
+}
+
+// _nss_cache_setspent()
+// Called by NSS to open the shadow file
+// 'stayopen' parameter is ignored.
+
+enum nss_status _nss_cache_setspent(int stayopen) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_setspent_locked();
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_endspent_locked()
+// Internal close routine
+
+static enum nss_status _nss_cache_endspent_locked(void) {
+
+ DEBUG("Closing shadow.cache\n");
+ if (s_file) {
+ fclose(s_file);
+ s_file = NULL;
+ }
+ return NSS_STATUS_SUCCESS;
+}
+
+// _nss_cache_endspent()
+// Called by NSS to close the shadow file
+
+enum nss_status _nss_cache_endspent(void) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_endspent_locked();
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_getspent_r_locked()
+// Called internally to return the next entry from the shadow file
+
+static enum nss_status _nss_cache_getspent_r_locked(struct spwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ enum nss_status ret = NSS_STATUS_SUCCESS;
+
+ if (s_file == NULL) {
+ DEBUG("s_file == NULL, going to setspent\n");
+ ret = _nss_cache_setspent_locked();
+ }
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ if (fgetspent_r(s_file, result, buffer, buflen, &result) == 0) {
+ DEBUG("Returning shadow entry %s\n", result->sp_namp);
+ } else {
+ *errnop = errno;
+ ret = _nss_cache_ent_bad_return_code(*errnop);
+ }
+ }
+
+ return ret;
+}
+
+// _nss_cache_getspent_r()
+// Called by NSS to look up next entry in the shadow file
+
+enum nss_status _nss_cache_getspent_r(struct spwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ enum nss_status ret;
+ NSS_CACHE_LOCK();
+ ret = _nss_cache_getspent_r_locked(result, buffer, buflen, errnop);
+ NSS_CACHE_UNLOCK();
+ return ret;
+}
+
+// _nss_cache_getspnam_r()
+// Find a user by name
+
+enum nss_status _nss_cache_getspnam_r(const char *name, struct spwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+ char *sp_namp;
+ char filename[NSS_CACHE_PATH_LENGTH];
+ struct nss_cache_args args;
+ enum nss_status ret;
+
+ NSS_CACHE_LOCK();
+
+ // name is a const char, we need a non-const copy
+ sp_namp = malloc(strlen(name) + 1);
+ if (sp_namp == NULL) {
+ DEBUG("malloc error\n");
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncpy(sp_namp, name, strlen(name) + 1);
+
+ strncpy(filename, s_filename, NSS_CACHE_PATH_LENGTH - 1);
+ if (strlen(filename) > NSS_CACHE_PATH_LENGTH - 8) {
+ DEBUG("filename too long\n");
+ free(sp_namp);
+ return NSS_STATUS_UNAVAIL;
+ }
+ strncat(filename, ".byname", 7);
+
+ args.sorted_filename = filename;
+ args.system_filename = s_filename;
+ args.lookup_function = _nss_cache_spnam_wrap;
+ args.lookup_value = sp_namp;
+ args.lookup_result = result;
+ args.buffer = buffer;
+ args.buflen = buflen;
+
+ DEBUG("Binary search for user %s\n", sp_namp);
+ ret = _nss_cache_bsearch(&args, errnop);
+
+ // TODO(vasilios): make this a runtime or compile-time option, as this slows
+ // down legitimate misses as the trade off for safety.
+ if (ret == NSS_STATUS_NOTFOUND) {
+ DEBUG("Binary search returned nothing.\n");
+ ret = NSS_STATUS_UNAVAIL;
+ }
+
+ if (ret == NSS_STATUS_UNAVAIL) {
+ DEBUG("Binary search failed, falling back to full linear search\n");
+ ret = _nss_cache_setspent_locked();
+
+ if (ret == NSS_STATUS_SUCCESS) {
+ while ((ret = _nss_cache_getspent_r_locked(result,
+ buffer,
+ buflen,
+ errnop))
+ == NSS_STATUS_SUCCESS) {
+ if (!strcmp(result->sp_namp, name))
+ break;
+ }
+ }
+ }
+
+ free(sp_namp);
+ _nss_cache_endspent_locked();
+ NSS_CACHE_UNLOCK();
+
+ return ret;
+}
diff --git a/nss/nss_cache/nss_cache.h b/nss/nss_cache/nss_cache.h
new file mode 100644
index 0000000000..8ce4b97d33
--- /dev/null
+++ b/nss/nss_cache/nss_cache.h
@@ -0,0 +1,65 @@
+/* Copyright 2009 Google Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA
+ */
+
+#include <errno.h>
+#include <grp.h>
+#include <nss.h>
+#include <stdlib.h>
+#include <pwd.h>
+#include <shadow.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifndef NSS_CACHE_H
+#define NSS_CACHE_H
+
+#ifdef DEBUG
+#undef DEBUG
+#define DEBUG(fmt, args...) do { fprintf(stderr, fmt, ##args); } while (0)
+#else
+#define DEBUG(fmt, ...) do { } while (0)
+#endif /* DEBUG */
+
+#define NSS_CACHE_PATH_LENGTH 255
+extern char* _nss_cache_setpwent_path(const char *path);
+extern char* _nss_cache_setgrent_path(const char *path);
+extern char* _nss_cache_setspent_path(const char *path);
+
+enum nss_cache_match {
+ NSS_CACHE_EXACT = 0,
+ NSS_CACHE_HIGH = 1,
+ NSS_CACHE_LOW = 2,
+ NSS_CACHE_ERROR = 3,
+};
+
+struct nss_cache_args {
+ char *system_filename;
+ char *sorted_filename;
+ void *lookup_function;
+ void *lookup_value;
+ void *lookup_result;
+ char *buffer;
+ size_t buflen;
+};
+
+#endif /* NSS_CACHE_H */
diff --git a/shlib-versions b/shlib-versions
index b9cb99d2fb..749346e267 100644
--- a/shlib-versions
+++ b/shlib-versions
@@ -48,6 +48,8 @@ libnss_nisplus=2
libnss_ldap=2
libnss_hesiod=2
libnss_db=2
+libnss_borg=2
+libnss_cache=2
# Tests for NSS. They must have the same NSS_SHLIB_REVISION number as
# the rest.
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2021-08-27 23:23 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-08-27 23:23 [glibc/maskray/grte] Integrate nss_{borg, cache} local changes from glibc-2.18 to 2.19 Fangrui Song
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).