From: James Jones Date: Sat, 10 Dec 2022 00:09:09 +0000 (-0600) Subject: Deal with dl_open_by_name() toctou (CIS #1400053) (#4799) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=181ae63889075fd475f007c62eabc78ac9e83265;p=thirdparty%2Ffreeradius-server.git Deal with dl_open_by_name() toctou (CIS #1400053) (#4799) * Deal with dl_open_by_name() toctou (CIS #1400053) dl_open_by_name() gives you the option of looking though a list of directories for a library, using the dlopen() function. If a dlopen() succeeds, dl_open_by_name() succeeds and all is well. If it fails, though, the question is why? If it doesn't exist in that directory, you want to try the next on the list. If it does, permissions are likely to be the problem, so report the issue and return failure. The previous code uses access() on the path if the dlopen() fails, hence toctou. Unfortunately, there's not a good way to deal with it. There is no dlopenat(), so one can't avoid toctou with dlopenat() followed by accessat(). The only indication of why dlopen() failed is in the string returned by dlerror()... which only guarantees that the string is human-readable, NUL-terminated, and doesn't end with a newline. Looking at the result of dlerror() when the dlopen() for the directory list fails suggests that it has the following format: path COLON SPACE COLON SPACE strerror_output In the particular cases in the tests, that last part was always "No such file or directory", what strerror() returns for the ENOENT dl_open_by_name() is checking for. As the least bad alternative, the code looks in the dlerror() output for fr_syserror(ENOENT) to decide. * Add note Co-authored-by: Arran Cudbard-Bell --- diff --git a/src/lib/util/dl.c b/src/lib/util/dl.c index 6527d9b01cf..de82285d560 100644 --- a/src/lib/util/dl.c +++ b/src/lib/util/dl.c @@ -528,7 +528,7 @@ dl_t *dl_by_name(dl_loader_t *dl_loader, char const *name, void *uctx, bool uctx ctx = paths = talloc_typed_strdup(NULL, search_path); while ((path = strsep(&paths, ":")) != NULL) { - int access_mode = R_OK | X_OK; + char *dlerror_txt; /* * Trim the trailing slash @@ -538,40 +538,52 @@ dl_t *dl_by_name(dl_loader_t *dl_loader, char const *name, void *uctx, bool uctx path = talloc_typed_asprintf(ctx, "%s/%s%s", path, name, DL_EXTENSION); handle = dlopen(path, flags); - if (handle) { - talloc_free(path); - break; - } + talloc_free(path); + if (handle) break; -#ifdef AT_ACCESS - access_mode |= AT_ACCESS; -#endif + /* + * There's no dlopenat(), so one can't use it and later + * check acessatat() to avoid toctou. + * + * The only indication of why dlopen() failed is thus the + * contents of the string dlerror() returns, but all the + * man page says about that is that it's "human readable", + * NUL-terminated, and doesn't end with a newline. + * + * The following attempts to use the dlerror() output to + * determine whether dlopen() failed because path doesn't + * exist. This goes beyond what the API specifies (Hyrum's + * Law strikes again!), but the alternatives are + * + * 1. looking into the data structure dlerror() uses + * 2. let the toctou remain + * + * both of which seem worse. + */ /* - * Check if the dlopen() failed - * because of access permissions. + * If the file doesn't exist, continue with the next element + * of "path". Otherwise, stop looking for more libraries + * and instead complain about access permissions. */ - /* coverity[fs_check_call] */ - if (access(path, access_mode) < 0) { - /* - * It doesn't exist, - * continue with the next - * element of "path". - */ - if (errno == ENOENT) continue; - - /* - * Stop looking for more - * libraries, and instead - * complain about access - * permissions. - */ - fr_strerror_printf("Access check failed for %s - %s", path, fr_syserror(errno)); - talloc_free(path); + dlerror_txt = dlerror(); + + /* + * Yes, this really is the only way of getting the errno + * from the dlopen API. + * + if (!strstr(dlerror_txt, fr_syserror(ENOENT))) { +#ifndef __linux__ + int access_mode = R_OK | X_OK; + +#ifdef AT_ACCESS + access_mode |= AT_ACCESS; +#endif + if (access(path, access_mode) < 0 && errno == ENOENT) continue; +#endif + fr_strerror_printf("Access check failed: %s", dlerror_txt); break; } - - talloc_free(path); } /*