From d2747481603a43b8f37470763fdd0944902f82da Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 18 Nov 2025 09:27:14 +0900 Subject: [PATCH 1/7] libcrypt-util: move looks_like_hashed_password() No functional change, just preparation for later change. --- src/shared/libcrypt-util.c | 32 ++++++++++++++++---------------- src/shared/libcrypt-util.h | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index 594963ac6b..f981c1b997 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -140,22 +140,6 @@ int hash_password_full(const char *password, void **cd_data, int *cd_size, char return strdup_to(ret, p); } -bool looks_like_hashed_password(const char *s) { - /* Returns false if the specified string is certainly not a hashed UNIX password. crypt(5) lists - * various hashing methods. We only reject (return false) strings which are documented to have - * different meanings. - * - * In particular, we allow locked passwords, i.e. strings starting with "!", including just "!", - * i.e. the locked empty password. See also fc58c0c7bf7e4f525b916e3e5be0de2307fef04e. - */ - if (!s) - return false; - - s += strspn(s, "!"); /* Skip (possibly duplicated) locking prefix */ - - return !STR_IN_SET(s, "x", "*"); -} - int test_password_one(const char *hashed_password, const char *password) { _cleanup_(erase_and_freep) void *cd_data = NULL; int cd_size = 0; @@ -186,3 +170,19 @@ int test_password_many(char **hashed_password, const char *password) { return false; } + +bool looks_like_hashed_password(const char *s) { + /* Returns false if the specified string is certainly not a hashed UNIX password. crypt(5) lists + * various hashing methods. We only reject (return false) strings which are documented to have + * different meanings. + * + * In particular, we allow locked passwords, i.e. strings starting with "!", including just "!", + * i.e. the locked empty password. See also fc58c0c7bf7e4f525b916e3e5be0de2307fef04e. + */ + if (!s) + return false; + + s += strspn(s, "!"); /* Skip (possibly duplicated) locking prefix */ + + return !STR_IN_SET(s, "x", "*"); +} diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h index 27c5f055a3..ed2993f82c 100644 --- a/src/shared/libcrypt-util.h +++ b/src/shared/libcrypt-util.h @@ -8,6 +8,6 @@ int hash_password_full(const char *password, void **cd_data, int *cd_size, char static inline int hash_password(const char *password, char **ret) { return hash_password_full(password, NULL, NULL, ret); } -bool looks_like_hashed_password(const char *s); int test_password_one(const char *hashed_password, const char *password); int test_password_many(char **hashed_password, const char *password); +bool looks_like_hashed_password(const char *s); From b26406c9c33f8e65afdf5c87a05e4f6c311ffb17 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 18 Nov 2025 09:52:37 +0900 Subject: [PATCH 2/7] libcrypt-util: drop unused hash_passwrod_full() It is only used by test cases. Not necessary to keep it. --- src/shared/libcrypt-util.c | 10 +++--- src/shared/libcrypt-util.h | 5 +-- src/test/test-libcrypt-util.c | 62 +++++++++++++++-------------------- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index f981c1b997..cff3f586cf 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -119,20 +119,18 @@ static char* systemd_crypt_ra(const char *phrase, const char *setting, void **da #endif -int hash_password_full(const char *password, void **cd_data, int *cd_size, char **ret) { +int hash_password(const char *password, char **ret) { _cleanup_free_ char *salt = NULL; - _cleanup_(erase_and_freep) void *_cd_data = NULL; + _cleanup_(erase_and_freep) void *cd_data = NULL; const char *p; - int r, _cd_size = 0; - - assert(!!cd_data == !!cd_size); + int r, cd_size = 0; r = make_salt(&salt); if (r < 0) return log_debug_errno(r, "Failed to generate salt: %m"); errno = 0; - p = crypt_ra(password, salt, cd_data ?: &_cd_data, cd_size ?: &_cd_size); + p = crypt_ra(password, salt, &cd_data, &cd_size); if (!p) return log_debug_errno(errno_or_else(SYNTHETIC_ERRNO(EINVAL)), CRYPT_RA_NAME "() failed: %m"); diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h index ed2993f82c..e80cfcaf4e 100644 --- a/src/shared/libcrypt-util.h +++ b/src/shared/libcrypt-util.h @@ -4,10 +4,7 @@ #include "shared-forward.h" int make_salt(char **ret); -int hash_password_full(const char *password, void **cd_data, int *cd_size, char **ret); -static inline int hash_password(const char *password, char **ret) { - return hash_password_full(password, NULL, NULL, ret); -} +int hash_password(const char *password, char **ret); int test_password_one(const char *hashed_password, const char *password); int test_password_many(char **hashed_password, const char *password); bool looks_like_hashed_password(const char *s); diff --git a/src/test/test-libcrypt-util.c b/src/test/test-libcrypt-util.c index 92eda2e1a7..f4968d91de 100644 --- a/src/test/test-libcrypt-util.c +++ b/src/test/test-libcrypt-util.c @@ -62,45 +62,37 @@ static int test_hash_password(void) { static void test_hash_password_full(void) { log_info("/* %s */", __func__); - _cleanup_free_ void *cd_data = NULL; - int cd_size = 0; - log_info("sizeof(struct crypt_data): %zu bytes", sizeof(struct crypt_data)); - for (unsigned c = 0; c < 2; c++) - FOREACH_STRING(i, "abc123", "h⸿sło") { - _cleanup_free_ char *hashed; + FOREACH_STRING(i, "abc123", "h⸿sło") { + _cleanup_free_ char *hashed; - if (c == 0) - assert_se(hash_password_full(i, &cd_data, &cd_size, &hashed) == 0); - else - assert_se(hash_password_full(i, NULL, NULL, &hashed) == 0); - log_debug("\"%s\" → \"%s\"", i, hashed); - log_info("crypt_r[a] buffer size: %i bytes", cd_size); + assert_se(hash_password(i, &hashed) == 0); + log_debug("\"%s\" → \"%s\"", i, hashed); - assert_se(test_password_one(hashed, i) == true); - assert_se(test_password_one(i, hashed) <= 0); /* We get an error for non-utf8 */ - assert_se(test_password_one(hashed, "foobar") == false); - assert_se(test_password_many(STRV_MAKE(hashed), i) == true); - assert_se(test_password_many(STRV_MAKE(hashed), "foobar") == false); - assert_se(test_password_many(STRV_MAKE(hashed, hashed, hashed), "foobar") == false); - assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - i) == true); - assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - i) == true); - assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - "") == false); - assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - "") == false); - } + assert_se(test_password_one(hashed, i) == true); + assert_se(test_password_one(i, hashed) <= 0); /* We get an error for non-utf8 */ + assert_se(test_password_one(hashed, "foobar") == false); + assert_se(test_password_many(STRV_MAKE(hashed), i) == true); + assert_se(test_password_many(STRV_MAKE(hashed), "foobar") == false); + assert_se(test_password_many(STRV_MAKE(hashed, hashed, hashed), "foobar") == false); + assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + i) == true); + assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + i) == true); + assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + "") == false); + assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + "") == false); + } } int main(int argc, char *argv[]) { From d5267b22757a638e4f2c3b890e595f3a26041b6f Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 18 Nov 2025 10:28:50 +0900 Subject: [PATCH 3/7] libcrypt-util: add missing assertions --- src/shared/libcrypt-util.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index cff3f586cf..26e907f866 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -12,6 +12,7 @@ #include "strv.h" int make_salt(char **ret) { + assert(ret); #if HAVE_CRYPT_GENSALT_RA const char *e; @@ -86,6 +87,8 @@ int make_salt(char **ret) { /* Provide a poor man's fallback that uses a fixed size buffer. */ static char* systemd_crypt_ra(const char *phrase, const char *setting, void **data, int *size) { + assert(phrase); + assert(setting); assert(data); assert(size); @@ -125,6 +128,9 @@ int hash_password(const char *password, char **ret) { const char *p; int r, cd_size = 0; + assert(password); + assert(ret); + r = make_salt(&salt); if (r < 0) return log_debug_errno(r, "Failed to generate salt: %m"); @@ -143,6 +149,9 @@ int test_password_one(const char *hashed_password, const char *password) { int cd_size = 0; const char *k; + assert(hashed_password); + assert(password); + errno = 0; k = crypt_ra(password, hashed_password, &cd_data, &cd_size); if (!k) { @@ -158,6 +167,8 @@ int test_password_one(const char *hashed_password, const char *password) { int test_password_many(char **hashed_password, const char *password) { int r; + assert(password); + STRV_FOREACH(hpw, hashed_password) { r = test_password_one(*hpw, password); if (r < 0) From 8608d2e910c2a007d4ce2c48aa6ab37523e3d1c2 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 18 Aug 2025 00:13:27 +0900 Subject: [PATCH 4/7] test-libcrypt-util: use DEFINE_TEST_MAIN() and ASSERT_XYZ() Also, tests for make_salt() in test-user-util.c are moved to test-libcrypt-util.c. --- src/test/test-libcrypt-util.c | 115 ++++++++++++++-------------------- src/test/test-user-util.c | 13 ---- 2 files changed, 46 insertions(+), 82 deletions(-) diff --git a/src/test/test-libcrypt-util.c b/src/test/test-libcrypt-util.c index f4968d91de..9f171a2415 100644 --- a/src/test/test-libcrypt-util.c +++ b/src/test/test-libcrypt-util.c @@ -6,9 +6,7 @@ #include "strv.h" #include "tests.h" -static void test_crypt_preferred_method(void) { - log_info("/* %s */", __func__); - +TEST(crypt_preferred_method) { log_info("crypt_preferred_method: %s", #if HAVE_CRYPT_PREFERRED_METHOD crypt_preferred_method() @@ -18,24 +16,26 @@ static void test_crypt_preferred_method(void) { ); } -static void test_make_salt(void) { - log_info("/* %s */", __func__); +TEST(make_salt) { + _cleanup_strv_free_ char **l = NULL; for (int i = 0; i < 10; i++) { _cleanup_free_ char *t; - assert_se(make_salt(&t) == 0); + ASSERT_OK(make_salt(&t)); log_info("%s", t); + + ASSERT_FALSE(strv_contains(l, t)); + ASSERT_OK(strv_consume(&l, TAKE_PTR(t))); } } -static int test_hash_password(void) { - log_info("/* %s */", __func__); +TEST(hash_password) { +#if defined(__powerpc__) && !defined(XCRYPT_VERSION_MAJOR) + return log_tests_skipped("crypt_r() causes a buffer overflow on ppc64el, see https://github.com/systemd/systemd/pull/16981#issuecomment-691203787"); +#endif /* As a warm-up exercise, check if we can hash passwords. */ - - bool have_sane_hash = false; - FOREACH_STRING(hash, "ew3bU1.hoKk4o", "$1$gc5rWpTB$wK1aul1PyBn9AX1z93stk1", @@ -43,72 +43,49 @@ static int test_hash_password(void) { "$5$lGhDrcrao9zb5oIK$05KlOVG3ocknx/ThreqXE/gk.XzFFBMTksc4t2CPDUD", "$6$c7wB/3GiRk0VHf7e$zXJ7hN0aLZapE.iO4mn/oHu6.prsXTUG/5k1AxpgR85ELolyAcaIGRgzfwJs3isTChMDBjnthZyaMCfCNxo9I.", "$y$j9T$$9cKOWsAm4m97WiYk61lPPibZpy3oaGPIbsL4koRe/XD") { - int b; - b = test_password_one(hash, "ppp"); - log_info("%s: %s", hash, yes_no(b)); -#if defined(XCRYPT_VERSION_MAJOR) - /* xcrypt is supposed to always implement all methods. */ - assert_se(b); +#ifndef __GLIBC__ + /* musl does not support yescrypt. */ + if (hash[1] == 'y') { + ASSERT_OK_ZERO(test_password_one(hash, "ppp")); + continue; + } +#elif !defined(XCRYPT_VERSION_MAJOR) + ASSERT_OK(test_password_one(hash, "ppp")); + continue; #endif - - if (b && IN_SET(hash[1], '6', 'y')) - have_sane_hash = true; + ASSERT_OK_POSITIVE(test_password_one(hash, "ppp")); } - return have_sane_hash; -} - -static void test_hash_password_full(void) { - log_info("/* %s */", __func__); - - log_info("sizeof(struct crypt_data): %zu bytes", sizeof(struct crypt_data)); - FOREACH_STRING(i, "abc123", "h⸿sło") { _cleanup_free_ char *hashed; - assert_se(hash_password(i, &hashed) == 0); + ASSERT_OK(hash_password(i, &hashed)); log_debug("\"%s\" → \"%s\"", i, hashed); - assert_se(test_password_one(hashed, i) == true); - assert_se(test_password_one(i, hashed) <= 0); /* We get an error for non-utf8 */ - assert_se(test_password_one(hashed, "foobar") == false); - assert_se(test_password_many(STRV_MAKE(hashed), i) == true); - assert_se(test_password_many(STRV_MAKE(hashed), "foobar") == false); - assert_se(test_password_many(STRV_MAKE(hashed, hashed, hashed), "foobar") == false); - assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - i) == true); - assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - i) == true); - assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - "") == false); - assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ - hashed, - "$y$j9T$SAayASazWZIQeJd9AS02m/$"), - "") == false); - } + ASSERT_OK_POSITIVE(test_password_one(hashed, i)); + ASSERT_LE(test_password_one(i, hashed), 0); /* We get an error for non-utf8 */ + ASSERT_OK_ZERO(test_password_one(hashed, "foobar")); + ASSERT_OK_POSITIVE(test_password_many(STRV_MAKE(hashed), i)); + ASSERT_OK_ZERO(test_password_many(STRV_MAKE(hashed), "foobar")); + ASSERT_OK_ZERO(test_password_many(STRV_MAKE(hashed, hashed, hashed), "foobar")); + ASSERT_OK_POSITIVE(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + i)); + ASSERT_OK_POSITIVE(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + i)); + ASSERT_OK_ZERO(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + "")); + ASSERT_OK_ZERO(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + "")); + } } -int main(int argc, char *argv[]) { - test_setup_logging(LOG_DEBUG); - -#if defined(__powerpc__) && !defined(XCRYPT_VERSION_MAJOR) - return log_tests_skipped("crypt_r() causes a buffer overflow on ppc64el, see https://github.com/systemd/systemd/pull/16981#issuecomment-691203787"); -#endif - - test_crypt_preferred_method(); - test_make_salt(); - - if (!test_hash_password()) - return log_tests_skipped("crypt doesn't support yescrypt or sha512crypt"); - - test_hash_password_full(); - - return 0; -} +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c index d822df61fb..47bd01f9d0 100644 --- a/src/test/test-user-util.c +++ b/src/test/test-user-util.c @@ -4,7 +4,6 @@ #include "alloc-util.h" #include "format-util.h" -#include "libcrypt-util.h" #include "log.h" #include "memory-util.h" #include "path-util.h" @@ -342,18 +341,6 @@ TEST(get_group_creds) { test_get_group_creds_one("65534", NOBODY_GROUP_NAME, GID_NOBODY); } -TEST(make_salt) { - _cleanup_free_ char *s, *t; - - ASSERT_OK(make_salt(&s)); - log_info("got %s", s); - - ASSERT_OK(make_salt(&t)); - log_info("got %s", t); - - ASSERT_NOT_STREQ(s, t); -} - TEST(in_gid) { ASSERT_OK(in_gid(getgid())); ASSERT_OK(in_gid(getegid())); From 5863641bb718777c289d3693bfa9bddd8ffc32b6 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sun, 17 Aug 2025 23:03:44 +0900 Subject: [PATCH 5/7] Require libxcrypt-4.4.0 or newer and drop support of libcrypt libcrypt was no longer built by default since glibc-2.38, and it has been completely removed since glibc-2.39. Let's always use libxcrypt, unless when building with musl. As already major distribution already have libxcrypt-4.4.x, hence let's also bump the required minimum version to 4.4.0. libxcrypt cannot be built with musl, hence the previous fallback logic in libcrypt-util.c are moved to musl/crypt.c. Note, libxcrypt-4.4.0 was released on 2018-11-20. See also #38608. --- README | 2 +- meson.build | 21 ++----- src/include/musl/crypt.h | 13 ++++ src/libc/musl/crypt.c | 111 ++++++++++++++++++++++++++++++++++ src/libc/musl/meson.build | 1 + src/shared/libcrypt-util.c | 98 +----------------------------- src/test/test-libcrypt-util.c | 15 +---- 7 files changed, 135 insertions(+), 126 deletions(-) create mode 100644 src/include/musl/crypt.h create mode 100644 src/libc/musl/crypt.c diff --git a/README b/README index 83493b0417..0fd153bebb 100644 --- a/README +++ b/README @@ -212,7 +212,7 @@ REQUIREMENTS: newer though. TL;DR: turn audit off, still. glibc >= 2.31 - libxcrypt or glibc (<= 2.38 built with --enable-crypt) + libxcrypt >= 4.4.0 libmount >= 2.30 (from util-linux) (util-linux *must* be built without --enable-libmount-support-mtab) libseccomp >= 2.4.0 (optional) diff --git a/meson.build b/meson.build index cc26bbd63e..cf23ed51ef 100644 --- a/meson.build +++ b/meson.build @@ -1046,23 +1046,14 @@ else libatomic = [] endif -libcrypt = dependency('libcrypt', 'libxcrypt', required : false) -if not libcrypt.found() - # fallback to use find_library() if libcrypt is provided by glibc, e.g. for LibreELEC. - libcrypt = cc.find_library('crypt') +if get_option('libc') == 'musl' + libcrypt = [] +else + libcrypt = dependency('libcrypt', 'libxcrypt', + required : true, + version : '>=4.4.0') endif -foreach func : [ - 'crypt_ra', # since libxcrypt-4.0.0 - 'crypt_gensalt_ra', # since libxcrypt-4.0.0 - 'crypt_preferred_method', # since libxcrypt-4.4.0 -] - - have = cc.has_function(func, prefix : '''#include ''', args : '-D_GNU_SOURCE', - dependencies : libcrypt) - conf.set10('HAVE_' + func.to_upper(), have) -endforeach - bpf_framework = get_option('bpf-framework') bpf_compiler = get_option('bpf-compiler') libbpf = dependency('libbpf', diff --git a/src/include/musl/crypt.h b/src/include/musl/crypt.h new file mode 100644 index 0000000000..64570f7046 --- /dev/null +++ b/src/include/musl/crypt.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include_next + +const char* missing_crypt_preferred_method(void); +#define crypt_preferred_method missing_crypt_preferred_method + +char* missing_crypt_gensalt_ra(const char *prefix, unsigned long count, const char *rbytes, int nrbytes); +#define crypt_gensalt_ra missing_crypt_gensalt_ra + +char* missing_crypt_ra(const char *phrase, const char *setting, void **data, int *size); +#define crypt_ra missing_crypt_ra diff --git a/src/libc/musl/crypt.c b/src/libc/musl/crypt.c new file mode 100644 index 0000000000..d44113fdc6 --- /dev/null +++ b/src/libc/musl/crypt.c @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include +#include +#include +#include + +const char* missing_crypt_preferred_method(void) { + return "$6$"; +} + +char* missing_crypt_gensalt_ra(const char *prefix, unsigned long count, const char *rbytes, int nrbytes) { + static const char table[] = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "./"; + + static_assert(sizeof(table) == 64U + 1U); + + /* This doesn't do anything but SHA512, and silently ignore all arguments, to make it legacy-free and + * minimize the implementation. */ + + /* Insist on the best randomness by getrandom(), this is about keeping passwords secret after all. */ + uint8_t raw[16]; + for (size_t i = 0; i < sizeof(raw);) { + size_t n = sizeof(raw) - i; + ssize_t l = getrandom(raw + i, n, 0); + if (l < 0) + return NULL; + if (l == 0) { + /* Weird, should never happen. */ + errno = EIO; + return NULL; + } + + if ((size_t) l == n) + break; /* Done reading, success. */ + + i += l; + /* Interrupted by a signal; keep going. */ + } + + /* "$6$" + salt + "$" + NUL */ + char *salt = malloc(3 + sizeof(raw) + 1 + 1); + if (!salt) { + errno = ENOMEM; + return NULL; + } + + /* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */ + char *p = stpcpy(salt, "$6$"); + for (size_t i = 0; i < sizeof(raw); i++) + *p++ = table[raw[i] & 63]; + *p++ = '$'; + *p = '\0'; + + return salt; +} + +char* missing_crypt_ra(const char *phrase, const char *setting, void **data, int *size) { + struct crypt_data *buf = NULL; + bool allocated = false; + + if (!phrase || !setting || !data || !size) { + errno = EINVAL; + return NULL; + } + + if (*data) { + if (*size != sizeof(struct crypt_data)) { + errno = EINVAL; + return NULL; + } + + buf = *data; + } else { + if (*size != 0) { + errno = EINVAL; + return NULL; + } + + buf = calloc(1, sizeof(struct crypt_data)); + if (!buf) { + errno = ENOMEM; + return NULL; + } + + allocated = true; + } + + /* crypt_r may return a pointer to an invalid hashed password on error. Our callers expect NULL on + * error, so let's just return that. */ + + char *t = crypt_r(phrase, setting, buf); + if (!t || t[0] == '*') { + if (allocated) + free(buf); + return NULL; + } + + if (allocated) { + *data = buf; + *size = sizeof(struct crypt_data); + } + return t; +} diff --git a/src/libc/musl/meson.build b/src/libc/musl/meson.build index d40a2f1911..4fabac7bcc 100644 --- a/src/libc/musl/meson.build +++ b/src/libc/musl/meson.build @@ -5,6 +5,7 @@ if get_option('libc') != 'musl' endif libc_wrapper_sources += files( + 'crypt.c', 'getopt.c', 'printf.c', 'stdio.c', diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index 26e907f866..6910c5f527 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -1,33 +1,23 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include "alloc-util.h" #include "errno-util.h" #include "libcrypt-util.h" #include "log.h" -#include "random-util.h" /* IWYU pragma: keep */ #include "string-util.h" #include "strv.h" int make_salt(char **ret) { - assert(ret); - -#if HAVE_CRYPT_GENSALT_RA const char *e; char *salt; - /* If we have crypt_gensalt_ra() we default to the "preferred method" (i.e. usually yescrypt). - * crypt_gensalt_ra() is usually provided by libxcrypt. */ + assert(ret); e = secure_getenv("SYSTEMD_CRYPT_PREFIX"); if (!e) -#if HAVE_CRYPT_PREFERRED_METHOD e = crypt_preferred_method(); -#else - e = "$6$"; -#endif log_debug("Generating salt for hash prefix: %s", e); @@ -37,91 +27,8 @@ int make_salt(char **ret) { *ret = salt; return 0; -#else - /* If crypt_gensalt_ra() is not available, we use SHA512 and generate the salt on our own. */ - - static const char table[] = - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789" - "./"; - - uint8_t raw[16]; - char *salt, *j; - size_t i; - int r; - - /* This is a bit like crypt_gensalt_ra(), but doesn't require libcrypt, and doesn't do anything but - * SHA512, i.e. is legacy-free and minimizes our deps. */ - - assert_cc(sizeof(table) == 64U + 1U); - - log_debug("Generating fallback salt for hash prefix: $6$"); - - /* Insist on the best randomness by setting RANDOM_BLOCK, this is about keeping passwords secret after all. */ - r = crypto_random_bytes(raw, sizeof(raw)); - if (r < 0) - return r; - - salt = new(char, 3+sizeof(raw)+1+1); - if (!salt) - return -ENOMEM; - - /* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */ - j = stpcpy(salt, "$6$"); - for (i = 0; i < sizeof(raw); i++) - j[i] = table[raw[i] & 63]; - j[i++] = '$'; - j[i] = 0; - - *ret = salt; - return 0; -#endif } -#if HAVE_CRYPT_RA -# define CRYPT_RA_NAME "crypt_ra" -#else -# define CRYPT_RA_NAME "crypt_r" - -/* Provide a poor man's fallback that uses a fixed size buffer. */ - -static char* systemd_crypt_ra(const char *phrase, const char *setting, void **data, int *size) { - assert(phrase); - assert(setting); - assert(data); - assert(size); - - /* We allocate the buffer because crypt(3) says: struct crypt_data may be quite large (32kB in this - * implementation of libcrypt; over 128kB in some other implementations). This is large enough that - * it may be unwise to allocate it on the stack. */ - - if (!*data) { - *data = new0(struct crypt_data, 1); - if (!*data) { - errno = ENOMEM; - return NULL; - } - - *size = (int) (sizeof(struct crypt_data)); - } - - char *t = crypt_r(phrase, setting, *data); - if (!t) - return NULL; - - /* crypt_r may return a pointer to an invalid hashed password on error. Our callers expect NULL on - * error, so let's just return that. */ - if (t[0] == '*') - return NULL; - - return t; -} - -#define crypt_ra systemd_crypt_ra - -#endif - int hash_password(const char *password, char **ret) { _cleanup_free_ char *salt = NULL; _cleanup_(erase_and_freep) void *cd_data = NULL; @@ -138,8 +45,7 @@ int hash_password(const char *password, char **ret) { errno = 0; p = crypt_ra(password, salt, &cd_data, &cd_size); if (!p) - return log_debug_errno(errno_or_else(SYNTHETIC_ERRNO(EINVAL)), - CRYPT_RA_NAME "() failed: %m"); + return log_debug_errno(errno_or_else(SYNTHETIC_ERRNO(EINVAL)), "crypt_ra() failed: %m"); return strdup_to(ret, p); } diff --git a/src/test/test-libcrypt-util.c b/src/test/test-libcrypt-util.c index 9f171a2415..92142fc9ef 100644 --- a/src/test/test-libcrypt-util.c +++ b/src/test/test-libcrypt-util.c @@ -7,13 +7,7 @@ #include "tests.h" TEST(crypt_preferred_method) { - log_info("crypt_preferred_method: %s", -#if HAVE_CRYPT_PREFERRED_METHOD - crypt_preferred_method() -#else - "(not available)" -#endif - ); + log_info("crypt_preferred_method: %s", crypt_preferred_method()); } TEST(make_salt) { @@ -31,10 +25,6 @@ TEST(make_salt) { } TEST(hash_password) { -#if defined(__powerpc__) && !defined(XCRYPT_VERSION_MAJOR) - return log_tests_skipped("crypt_r() causes a buffer overflow on ppc64el, see https://github.com/systemd/systemd/pull/16981#issuecomment-691203787"); -#endif - /* As a warm-up exercise, check if we can hash passwords. */ FOREACH_STRING(hash, "ew3bU1.hoKk4o", @@ -50,9 +40,6 @@ TEST(hash_password) { ASSERT_OK_ZERO(test_password_one(hash, "ppp")); continue; } -#elif !defined(XCRYPT_VERSION_MAJOR) - ASSERT_OK(test_password_one(hash, "ppp")); - continue; #endif ASSERT_OK_POSITIVE(test_password_one(hash, "ppp")); } From 8de783a288912a13b65ca269f01f3bc912d1aedb Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 25 Oct 2025 14:59:54 +0900 Subject: [PATCH 6/7] libcrypt: allow to build systemd without libcrypt/libxcrypt libcrypt is only used by firstboot, homed, and sysusers, which can be disabled by meson option. Let's not require the library unconditionally. --- meson.build | 18 +++++++----------- meson_options.txt | 2 ++ src/shared/libcrypt-util.c | 6 +++++- src/shared/libcrypt-util.h | 9 +++++++++ src/test/meson.build | 1 + 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/meson.build b/meson.build index cf23ed51ef..4f98dbd9da 100644 --- a/meson.build +++ b/meson.build @@ -685,15 +685,6 @@ conf.set('GPERF_LEN_TYPE', gperf_len_type, ##################################################################### -foreach header : [ - 'crypt.h', -] - - if not cc.has_header(header) - error(f'Header file @header@ not found') - endif -endforeach - foreach header : [ 'gshadow.h', 'nss.h', @@ -1048,11 +1039,14 @@ endif if get_option('libc') == 'musl' libcrypt = [] + have = get_option('libcrypt').allowed() else libcrypt = dependency('libcrypt', 'libxcrypt', - required : true, + required : get_option('libcrypt'), version : '>=4.4.0') + have = libcrypt.found() endif +conf.set10('HAVE_LIBCRYPT', have) bpf_framework = get_option('bpf-framework') bpf_compiler = get_option('bpf-compiler') @@ -1585,10 +1579,11 @@ conf.set10('ENABLE_SYSUPDATED', have2) conf.set10('ENABLE_STORAGETM', get_option('storagetm')) have = get_option('homed').require( + conf.get('HAVE_LIBCRYPT') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_LIBFDISK') == 1 and conf.get('HAVE_LIBCRYPTSETUP') == 1, - error_message : 'openssl, fdisk and libcryptsetup required').allowed() + error_message : 'libcrypt, openssl, fdisk, and libcryptsetup required').allowed() conf.set10('ENABLE_HOMED', have) have = have and conf.get('HAVE_PAM') == 1 @@ -3115,6 +3110,7 @@ foreach tuple : [ ['gnutls'], ['libarchive'], ['libbpf'], + ['libcrypt'], ['libcryptsetup'], ['libcryptsetup-plugins'], ['libcurl'], diff --git a/meson_options.txt b/meson_options.txt index 4ce3c7faca..5061869483 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -426,6 +426,8 @@ option('pwquality', type : 'feature', deprecated : { 'true' : 'enabled', 'false' description : 'libpwquality support') option('microhttpd', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'libµhttpd support') +option('libcrypt', type : 'feature', + description : 'libcrypt/libxcrypt support') option('libcryptsetup', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'libcryptsetup support') option('libcryptsetup-plugins', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index 6910c5f527..e8b0b72e35 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include +#if HAVE_LIBCRYPT +# include +#endif #include "alloc-util.h" #include "errno-util.h" @@ -9,6 +11,7 @@ #include "string-util.h" #include "strv.h" +#if HAVE_LIBCRYPT int make_salt(char **ret) { const char *e; char *salt; @@ -85,6 +88,7 @@ int test_password_many(char **hashed_password, const char *password) { return false; } +#endif bool looks_like_hashed_password(const char *s) { /* Returns false if the specified string is certainly not a hashed UNIX password. crypt(5) lists diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h index e80cfcaf4e..626668c710 100644 --- a/src/shared/libcrypt-util.h +++ b/src/shared/libcrypt-util.h @@ -3,8 +3,17 @@ #include "shared-forward.h" +#if HAVE_LIBCRYPT int make_salt(char **ret); int hash_password(const char *password, char **ret); int test_password_one(const char *hashed_password, const char *password); int test_password_many(char **hashed_password, const char *password); + +#else + +static inline int hash_password(const char *password, char **ret) { + return -EOPNOTSUPP; +} +#endif + bool looks_like_hashed_password(const char *s); diff --git a/src/test/meson.build b/src/test/meson.build index a21f85c2ec..7b77f3bd58 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -346,6 +346,7 @@ executables += [ test_template + { 'sources' : files('test-libcrypt-util.c'), 'dependencies' : libcrypt, + 'conditions' : ['HAVE_LIBCRYPT'], 'timeout' : 120, }, test_template + { From d0b224f845ce001a28d8e8c77410732617ab5b65 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 25 Oct 2025 13:41:33 +0900 Subject: [PATCH 7/7] libcrypt-util: turn into dlopen() dependency Note, this drops logging only test case for crypt_preferred_method(), as that requires explicitly dlopen() the library. But, we should test that make_salt() and friends automatically dlopen() it. --- README | 2 +- meson.build | 2 + src/firstboot/meson.build | 1 - src/home/meson.build | 4 -- src/shared/libcrypt-util.c | 75 ++++++++++++++++++++++++++++++++--- src/shared/libcrypt-util.h | 4 ++ src/shared/meson.build | 2 +- src/test/meson.build | 1 - src/test/test-dlopen-so.c | 2 + src/test/test-libcrypt-util.c | 6 --- 10 files changed, 80 insertions(+), 19 deletions(-) diff --git a/README b/README index 0fd153bebb..2d6d30dc78 100644 --- a/README +++ b/README @@ -212,7 +212,7 @@ REQUIREMENTS: newer though. TL;DR: turn audit off, still. glibc >= 2.31 - libxcrypt >= 4.4.0 + libxcrypt >= 4.4.0 (optional) libmount >= 2.30 (from util-linux) (util-linux *must* be built without --enable-libmount-support-mtab) libseccomp >= 2.4.0 (optional) diff --git a/meson.build b/meson.build index 4f98dbd9da..ff8cb279a8 100644 --- a/meson.build +++ b/meson.build @@ -1039,11 +1039,13 @@ endif if get_option('libc') == 'musl' libcrypt = [] + libcrypt_cflags = [] have = get_option('libcrypt').allowed() else libcrypt = dependency('libcrypt', 'libxcrypt', required : get_option('libcrypt'), version : '>=4.4.0') + libcrypt_cflags = libcrypt.partial_dependency(includes: true, compile_args: true) have = libcrypt.found() endif conf.set10('HAVE_LIBCRYPT', have) diff --git a/src/firstboot/meson.build b/src/firstboot/meson.build index 28c1d2703a..e83eded6ad 100644 --- a/src/firstboot/meson.build +++ b/src/firstboot/meson.build @@ -6,6 +6,5 @@ executables += [ 'public' : true, 'conditions' : ['ENABLE_FIRSTBOOT'], 'sources' : files('firstboot.c'), - 'dependencies' : libcrypt, }, ] diff --git a/src/home/meson.build b/src/home/meson.build index dbb374ce4a..1efee1619e 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -64,7 +64,6 @@ executables += [ 'sources' : systemd_homed_sources, 'extract' : systemd_homed_extract_sources, 'dependencies' : [ - libcrypt, libm, libopenssl, threads, @@ -80,7 +79,6 @@ executables += [ ], 'dependencies' : [ libblkid_cflags, - libcrypt, libfdisk, libopenssl, libp11kit_cflags, @@ -93,7 +91,6 @@ executables += [ 'sources' : homectl_sources, 'objects' : ['systemd-homed'], 'dependencies' : [ - libcrypt, libdl, libopenssl, libp11kit_cflags, @@ -112,7 +109,6 @@ modules += [ 'conditions' : ['HAVE_PAM'], 'sources' : pam_systemd_home_sources, 'dependencies' : [ - libcrypt, libintl, libpam_misc, libpam, diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index e8b0b72e35..85069314be 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -5,6 +5,7 @@ #endif #include "alloc-util.h" +#include "dlfcn-util.h" #include "errno-util.h" #include "libcrypt-util.h" #include "log.h" @@ -12,19 +13,79 @@ #include "strv.h" #if HAVE_LIBCRYPT +static void *libcrypt_dl = NULL; + +static DLSYM_PROTOTYPE(crypt_gensalt_ra) = NULL; +static DLSYM_PROTOTYPE(crypt_preferred_method) = NULL; +static DLSYM_PROTOTYPE(crypt_ra) = NULL; + +int dlopen_libcrypt(void) { +#ifdef __GLIBC__ + static int cached = 0; + int r; + + if (libcrypt_dl) + return 0; /* Already loaded */ + + if (cached < 0) + return cached; /* Already tried, and failed. */ + + /* Several distributions like Debian/Ubuntu and OpenSUSE provide libxcrypt as libcrypt.so.1, + * while others like Fedora/CentOS and Arch provide it as libcrypt.so.2. */ + ELF_NOTE_DLOPEN("crypt", + "Support for hashing passwords", + ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED, + "libcrypt.so.2", "libcrypt.so.1"); + + _cleanup_(dlclosep) void *dl = NULL; + r = dlopen_safe("libcrypt.so.2", &dl, /* reterr_dlerror= */ NULL); + if (r < 0) { + const char *dle = NULL; + r = dlopen_safe("libcrypt.so.1", &dl, &dle); + if (r < 0) { + log_debug_errno(r, "libcrypt.so.2/libcrypt.so.1 is not available: %s", dle ?: STRERROR(r)); + return (cached = -EOPNOTSUPP); /* turn into recognizable error */ + } + log_debug("Loaded 'libcrypt.so.1' via dlopen()"); + } else + log_debug("Loaded 'libcrypt.so.2' via dlopen()"); + + r = dlsym_many_or_warn( + dl, LOG_DEBUG, + DLSYM_ARG(crypt_gensalt_ra), + DLSYM_ARG(crypt_preferred_method), + DLSYM_ARG(crypt_ra)); + if (r < 0) + return (cached = r); + + libcrypt_dl = TAKE_PTR(dl); +#else + libcrypt_dl = NULL; + sym_crypt_gensalt_ra = missing_crypt_gensalt_ra; + sym_crypt_preferred_method = missing_crypt_preferred_method; + sym_crypt_ra = missing_crypt_ra; +#endif + return 0; +} + int make_salt(char **ret) { const char *e; char *salt; + int r; assert(ret); + r = dlopen_libcrypt(); + if (r < 0) + return r; + e = secure_getenv("SYSTEMD_CRYPT_PREFIX"); if (!e) - e = crypt_preferred_method(); + e = sym_crypt_preferred_method(); log_debug("Generating salt for hash prefix: %s", e); - salt = crypt_gensalt_ra(e, 0, NULL, 0); + salt = sym_crypt_gensalt_ra(e, 0, NULL, 0); if (!salt) return -errno; @@ -46,7 +107,7 @@ int hash_password(const char *password, char **ret) { return log_debug_errno(r, "Failed to generate salt: %m"); errno = 0; - p = crypt_ra(password, salt, &cd_data, &cd_size); + p = sym_crypt_ra(password, salt, &cd_data, &cd_size); if (!p) return log_debug_errno(errno_or_else(SYNTHETIC_ERRNO(EINVAL)), "crypt_ra() failed: %m"); @@ -55,14 +116,18 @@ int hash_password(const char *password, char **ret) { int test_password_one(const char *hashed_password, const char *password) { _cleanup_(erase_and_freep) void *cd_data = NULL; - int cd_size = 0; + int r, cd_size = 0; const char *k; assert(hashed_password); assert(password); + r = dlopen_libcrypt(); + if (r < 0) + return r; + errno = 0; - k = crypt_ra(password, hashed_password, &cd_data, &cd_size); + k = sym_crypt_ra(password, hashed_password, &cd_data, &cd_size); if (!k) { if (errno == ENOMEM) return -ENOMEM; diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h index 626668c710..3f79916cbc 100644 --- a/src/shared/libcrypt-util.h +++ b/src/shared/libcrypt-util.h @@ -4,6 +4,7 @@ #include "shared-forward.h" #if HAVE_LIBCRYPT +int dlopen_libcrypt(void); int make_salt(char **ret); int hash_password(const char *password, char **ret); int test_password_one(const char *hashed_password, const char *password); @@ -11,6 +12,9 @@ int test_password_many(char **hashed_password, const char *password); #else +static inline int dlopen_libcrypt(void) { + return -EOPNOTSUPP; +} static inline int hash_password(const char *password, char **ret) { return -EOPNOTSUPP; } diff --git a/src/shared/meson.build b/src/shared/meson.build index e0b86e0b81..36d38f4ed9 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -367,7 +367,7 @@ libshared_deps = [threads, libaudit_cflags, libblkid_cflags, libbpf_cflags, - libcrypt, + libcrypt_cflags, libcryptsetup_cflags, libdl, libdw_cflags, diff --git a/src/test/meson.build b/src/test/meson.build index 7b77f3bd58..6a26eb3f87 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -345,7 +345,6 @@ executables += [ }, test_template + { 'sources' : files('test-libcrypt-util.c'), - 'dependencies' : libcrypt, 'conditions' : ['HAVE_LIBCRYPT'], 'timeout' : 120, }, diff --git a/src/test/test-dlopen-so.c b/src/test/test-dlopen-so.c index 47b8470ffb..10b8da1e9c 100644 --- a/src/test/test-dlopen-so.c +++ b/src/test/test-dlopen-so.c @@ -11,6 +11,7 @@ #include "idn-util.h" #include "libarchive-util.h" #include "libaudit-util.h" +#include "libcrypt-util.h" #include "libfido2-util.h" #include "libmount-util.h" #include "main-func.h" @@ -52,6 +53,7 @@ static int run(int argc, char **argv) { ASSERT_DLOPEN(dlopen_libarchive, HAVE_LIBARCHIVE); ASSERT_DLOPEN(dlopen_libaudit, HAVE_AUDIT); ASSERT_DLOPEN(dlopen_libblkid, HAVE_BLKID); + ASSERT_DLOPEN(dlopen_libcrypt, HAVE_LIBCRYPT); ASSERT_DLOPEN(dlopen_libfido2, HAVE_LIBFIDO2); ASSERT_DLOPEN(dlopen_libkmod, HAVE_KMOD); ASSERT_DLOPEN(dlopen_libmount, HAVE_LIBMOUNT); diff --git a/src/test/test-libcrypt-util.c b/src/test/test-libcrypt-util.c index 92142fc9ef..792b2b2ef9 100644 --- a/src/test/test-libcrypt-util.c +++ b/src/test/test-libcrypt-util.c @@ -1,15 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "libcrypt-util.h" #include "strv.h" #include "tests.h" -TEST(crypt_preferred_method) { - log_info("crypt_preferred_method: %s", crypt_preferred_method()); -} - TEST(make_salt) { _cleanup_strv_free_ char **l = NULL;