Forked from jeremytregunna/revert-systemd-userdb-birthdate.patch
Created
March 19, 2026 22:23
-
-
Save bopm/b4668cf7d76b152eb326cb13c0c9baba to your computer and use it in GitHub Desktop.
Patch to revert systemd's userdb birthDate record bits. Valid as of 2026-03-19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md | |
| index 5335e145b5..9d6d8c1d03 100644 | |
| --- a/docs/USER_RECORD.md | |
| +++ b/docs/USER_RECORD.md | |
| @@ -273,9 +273,6 @@ This must be a string, and should follow the semantics defined in the | |
| It's probably wise to use a location string processable by geo-location subsystems, but this is not enforced nor required. | |
| Example: `Berlin, Germany` or `Basement, Room 3a`. | |
| -`birthDate` → A string in ISO 8601 calendar date format (`YYYY-MM-DD`) indicating the user's date | |
| -of birth. The earliest representable year is 1900. This field is optional. | |
| - | |
| `disposition` → A string, one of `intrinsic`, `system`, `dynamic`, `regular`, | |
| `container`, `foreign`, `reserved`. If specified clarifies the disposition of the user, | |
| i.e. the context it is defined in. | |
| diff --git a/man/homectl.xml b/man/homectl.xml | |
| index bb827d1e6b..a59efd7112 100644 | |
| --- a/man/homectl.xml | |
| +++ b/man/homectl.xml | |
| @@ -366,16 +366,6 @@ | |
| <xi:include href="version-info.xml" xpointer="v245"/></listitem> | |
| </varlistentry> | |
| - <varlistentry> | |
| - <term><option>--birth-date=<optional><replaceable>DATE</replaceable></optional></option></term> | |
| - | |
| - <listitem><para>Takes a birth date for the user in ISO 8601 calendar date format | |
| - (<literal>YYYY-MM-DD</literal>). The earliest representable year is 1900. If an empty string is | |
| - passed the birth date is reset to unset.</para> | |
| - | |
| - <xi:include href="version-info.xml" xpointer="v261"/></listitem> | |
| - </varlistentry> | |
| - | |
| <varlistentry> | |
| <term><option>--icon-name=<replaceable>ICON</replaceable></option></term> | |
| diff --git a/src/basic/time-util.c b/src/basic/time-util.c | |
| index 1e426bb8f9..5dd00af952 100644 | |
| --- a/src/basic/time-util.c | |
| +++ b/src/basic/time-util.c | |
| @@ -1892,51 +1892,3 @@ TimestampStyle timestamp_style_from_string(const char *s) { | |
| return TIMESTAMP_US_UTC; | |
| return t; | |
| } | |
| - | |
| -int parse_calendar_date_full(const char *s, bool allow_pre_epoch, usec_t *ret_usec, struct tm *ret_tm) { | |
| - struct tm parsed_tm = {}, copy_tm; | |
| - const char *k; | |
| - int r; | |
| - | |
| - assert(s); | |
| - | |
| - k = strptime(s, "%Y-%m-%d", &parsed_tm); | |
| - if (!k || *k) | |
| - return -EINVAL; | |
| - | |
| - copy_tm = parsed_tm; | |
| - | |
| - usec_t usec = USEC_INFINITY; | |
| - | |
| - if (allow_pre_epoch) { | |
| - /* For birth dates we use timegm() directly since we need to accept pre-epoch dates. | |
| - * timegm() returns (time_t) -1 both on error and for one second before the epoch. | |
| - * Initialize wday to -1 beforehand: if it remains -1 after the call, it's a genuine | |
| - * error; if timegm() changed it, the date was successfully normalized. */ | |
| - copy_tm.tm_wday = -1; | |
| - if (timegm(©_tm) == (time_t) -1 && copy_tm.tm_wday == -1) | |
| - return -EINVAL; | |
| - } else { | |
| - r = mktime_or_timegm_usec(©_tm, /* utc= */ true, &usec); | |
| - if (r < 0) | |
| - return r; | |
| - } | |
| - | |
| - /* Refuse non-normalized dates, e.g. Feb 30 */ | |
| - if (copy_tm.tm_mday != parsed_tm.tm_mday || | |
| - copy_tm.tm_mon != parsed_tm.tm_mon || | |
| - copy_tm.tm_year != parsed_tm.tm_year) | |
| - return -EINVAL; | |
| - | |
| - if (ret_usec) | |
| - *ret_usec = usec; | |
| - if (ret_tm) { | |
| - /* Reset to unset, then fill in only the date fields we parsed and validated */ | |
| - *ret_tm = BIRTH_DATE_UNSET; | |
| - ret_tm->tm_mday = parsed_tm.tm_mday; | |
| - ret_tm->tm_mon = parsed_tm.tm_mon; | |
| - ret_tm->tm_year = parsed_tm.tm_year; | |
| - } | |
| - | |
| - return 0; | |
| -} | |
| diff --git a/src/basic/time-util.h b/src/basic/time-util.h | |
| index 5b6a524c48..bde0b02d03 100644 | |
| --- a/src/basic/time-util.h | |
| +++ b/src/basic/time-util.h | |
| @@ -1,7 +1,6 @@ | |
| /* SPDX-License-Identifier: LGPL-2.1-or-later */ | |
| #pragma once | |
| -#include <limits.h> | |
| #include <time.h> | |
| #include "basic-forward.h" | |
| @@ -182,23 +181,6 @@ const char* etc_localtime(void); | |
| int mktime_or_timegm_usec(struct tm *tm, bool utc, usec_t *ret); | |
| int localtime_or_gmtime_usec(usec_t t, bool utc, struct tm *ret); | |
| -int parse_calendar_date_full(const char *s, bool allow_pre_epoch, usec_t *ret_usec, struct tm *ret_tm); | |
| - | |
| -static inline int parse_calendar_date(const char *s, usec_t *ret) { | |
| - return parse_calendar_date_full(s, /* allow_pre_epoch= */ false, ret, NULL); | |
| -} | |
| - | |
| -#define BIRTH_DATE_UNSET \ | |
| - (const struct tm) { \ | |
| - .tm_year = INT_MIN, \ | |
| - } | |
| - | |
| -#define BIRTH_DATE_IS_SET(tm) ((tm).tm_year != INT_MIN) | |
| - | |
| -static inline int parse_birth_date(const char *s, struct tm *ret) { | |
| - return parse_calendar_date_full(s, /* allow_pre_epoch= */ true, NULL, ret); | |
| -} | |
| - | |
| uint32_t usec_to_jiffies(usec_t usec); | |
| usec_t jiffies_to_usec(uint32_t jiffies); | |
| diff --git a/src/home/homectl.c b/src/home/homectl.c | |
| index 507860cde6..a8ebf85145 100644 | |
| --- a/src/home/homectl.c | |
| +++ b/src/home/homectl.c | |
| @@ -3945,7 +3945,6 @@ static int help(void) { | |
| " --alias=ALIAS Define alias usernames for this account\n" | |
| " --email-address=EMAIL Email address for user\n" | |
| " --location=LOCATION Set location of user on earth\n" | |
| - " --birth-date=[DATE] Set user birth date (YYYY-MM-DD)\n" | |
| " --icon-name=NAME Icon name for user\n" | |
| " -d --home-dir=PATH Home directory\n" | |
| " -u --uid=UID Numeric UID for user\n" | |
| @@ -4114,7 +4113,6 @@ static int parse_argv(int argc, char *argv[]) { | |
| ARG_LOCKED, | |
| ARG_SSH_AUTHORIZED_KEYS, | |
| ARG_LOCATION, | |
| - ARG_BIRTH_DATE, | |
| ARG_ICON_NAME, | |
| ARG_PASSWORD_HINT, | |
| ARG_NICE, | |
| @@ -4201,7 +4199,6 @@ static int parse_argv(int argc, char *argv[]) { | |
| { "alias", required_argument, NULL, ARG_ALIAS }, | |
| { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, | |
| { "location", required_argument, NULL, ARG_LOCATION }, | |
| - { "birth-date", required_argument, NULL, ARG_BIRTH_DATE }, | |
| { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, | |
| { "icon-name", required_argument, NULL, ARG_ICON_NAME }, | |
| { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */ | |
| @@ -4415,22 +4412,6 @@ static int parse_argv(int argc, char *argv[]) { | |
| break; | |
| } | |
| - case ARG_BIRTH_DATE: | |
| - if (isempty(optarg)) { | |
| - r = drop_from_identity("birthDate"); | |
| - if (r < 0) | |
| - return r; | |
| - } else { | |
| - r = parse_birth_date(optarg, /* ret= */ NULL); | |
| - if (r < 0) | |
| - return log_error_errno(r, "Invalid birth date (expected YYYY-MM-DD): %s", optarg); | |
| - | |
| - r = parse_string_field(&arg_identity_extra, "birthDate", optarg); | |
| - if (r < 0) | |
| - return r; | |
| - } | |
| - break; | |
| - | |
| case ARG_CIFS_SERVICE: | |
| if (!isempty(optarg)) { | |
| r = parse_cifs_service(optarg, /* ret_host= */ NULL, /* ret_service= */ NULL, /* ret_path= */ NULL); | |
| diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c | |
| index 11edf2557c..a43328c9fe 100644 | |
| --- a/src/shared/user-record-show.c | |
| +++ b/src/shared/user-record-show.c | |
| @@ -342,8 +342,6 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { | |
| printf(" Email: %s\n", hr->email_address); | |
| if (hr->location) | |
| printf(" Location: %s\n", hr->location); | |
| - if (BIRTH_DATE_IS_SET(hr->birth_date)) | |
| - printf(" Birth Date: %04d-%02d-%02d\n", hr->birth_date.tm_year + 1900, hr->birth_date.tm_mon + 1, hr->birth_date.tm_mday); | |
| if (hr->password_hint) | |
| printf(" Passw. Hint: %s\n", hr->password_hint); | |
| if (hr->icon_name) | |
| diff --git a/src/shared/user-record.c b/src/shared/user-record.c | |
| index 4dfb2c72d7..c65bab4ff4 100644 | |
| --- a/src/shared/user-record.c | |
| +++ b/src/shared/user-record.c | |
| @@ -46,7 +46,6 @@ UserRecord* user_record_new(void) { | |
| .nice_level = INT_MAX, | |
| .not_before_usec = UINT64_MAX, | |
| .not_after_usec = UINT64_MAX, | |
| - .birth_date = BIRTH_DATE_UNSET, | |
| .locked = -1, | |
| .storage = _USER_STORAGE_INVALID, | |
| .access_mode = MODE_INVALID, | |
| @@ -418,28 +417,6 @@ static int json_dispatch_filename_or_path(const char *name, sd_json_variant *var | |
| return 0; | |
| } | |
| -static int json_dispatch_birth_date(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { | |
| - struct tm *ret = ASSERT_PTR(userdata); | |
| - const char *s; | |
| - int r; | |
| - | |
| - if (sd_json_variant_is_null(variant)) { | |
| - *ret = BIRTH_DATE_UNSET; | |
| - return 0; | |
| - } | |
| - | |
| - if (!sd_json_variant_is_string(variant)) | |
| - return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); | |
| - | |
| - s = sd_json_variant_string(variant); | |
| - | |
| - r = parse_birth_date(s, ret); | |
| - if (r < 0) | |
| - return json_log(variant, flags, r, "JSON field '%s' is not a valid ISO 8601 date (expected YYYY-MM-DD).", strna(name)); | |
| - | |
| - return 0; | |
| -} | |
| - | |
| static int json_dispatch_home_directory(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { | |
| char **s = userdata; | |
| const char *n; | |
| @@ -1523,8 +1500,7 @@ int user_group_record_mangle( | |
| /* Personally Identifiable Information (PII) — avoid leaking in logs */ | |
| "realName", | |
| "location", | |
| - "emailAddress", | |
| - "birthDate") | |
| + "emailAddress") | |
| sd_json_variant_sensitive(sd_json_variant_by_key(v, key)); | |
| /* Check if we have the special sections and if they match our flags set */ | |
| @@ -1619,7 +1595,6 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load | |
| { "emailAddress", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, email_address), SD_JSON_STRICT }, | |
| { "iconName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, icon_name), SD_JSON_STRICT }, | |
| { "location", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, location), 0 }, | |
| - { "birthDate", SD_JSON_VARIANT_STRING, json_dispatch_birth_date, offsetof(UserRecord, birth_date), 0 }, | |
| { "disposition", SD_JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 }, | |
| { "lastChangeUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 }, | |
| { "lastPasswordChangeUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 }, | |
| diff --git a/src/shared/user-record.h b/src/shared/user-record.h | |
| index 4eb35d43e4..c3d52d2bd7 100644 | |
| --- a/src/shared/user-record.h | |
| +++ b/src/shared/user-record.h | |
| @@ -1,8 +1,6 @@ | |
| /* SPDX-License-Identifier: LGPL-2.1-or-later */ | |
| #pragma once | |
| -#include <time.h> | |
| - | |
| #include "sd-id128.h" | |
| #include "bitfield.h" | |
| @@ -268,7 +266,6 @@ typedef struct UserRecord { | |
| char *password_hint; | |
| char *icon_name; | |
| char *location; | |
| - struct tm birth_date; | |
| char *blob_directory; | |
| Hashmap *blob_manifest; | |
| diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c | |
| index ba1842b2d6..1cc48201ef 100644 | |
| --- a/src/sysupdate/sysupdate-resource.c | |
| +++ b/src/sysupdate/sysupdate-resource.c | |
| @@ -433,10 +433,24 @@ static int process_magic_file( | |
| if (iovec_memcmp(&IOVEC_MAKE(expected_hash, sizeof(expected_hash)), hash) != 0) | |
| log_warning("Hash of best before marker file '%s' has unexpected value, proceeding anyway.", fn); | |
| + struct tm parsed_tm = {}; | |
| + const char *n = strptime(e, "%Y-%m-%d", &parsed_tm); | |
| + if (!n || *n != 0) { | |
| + /* Doesn't parse? Then it's not a best-before date */ | |
| + log_warning("Found best before marker with an invalid date, ignoring: %s", fn); | |
| + return 0; | |
| + } | |
| + | |
| + struct tm copy_tm = parsed_tm; | |
| usec_t best_before; | |
| - r = parse_calendar_date(e, &best_before); | |
| - if (r < 0) { | |
| - log_warning_errno(r, "Found best before marker with an invalid date, ignoring: %s", fn); | |
| + r = mktime_or_timegm_usec(©_tm, /* utc= */ true, &best_before); | |
| + if (r < 0) | |
| + return log_error_errno(r, "Failed to convert best before time: %m"); | |
| + if (copy_tm.tm_mday != parsed_tm.tm_mday || | |
| + copy_tm.tm_mon != parsed_tm.tm_mon || | |
| + copy_tm.tm_year != parsed_tm.tm_year) { | |
| + /* date was not normalized? (e.g. "30th of feb") */ | |
| + log_warning("Found best before marker with a non-normalized data, ignoring: %s", fn); | |
| return 0; | |
| } | |
| diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c | |
| index d3d4fd5f9b..d5d4992f82 100644 | |
| --- a/src/test/test-time-util.c | |
| +++ b/src/test/test-time-util.c | |
| @@ -1281,62 +1281,4 @@ static int intro(void) { | |
| return EXIT_SUCCESS; | |
| } | |
| -TEST(parse_calendar_date) { | |
| - usec_t usec; | |
| - | |
| - /* Valid dates */ | |
| - ASSERT_OK(parse_calendar_date("2000-01-01", &usec)); | |
| - ASSERT_OK(parse_calendar_date("1970-01-01", &usec)); | |
| - ASSERT_EQ(usec, 0u); /* epoch */ | |
| - ASSERT_OK(parse_calendar_date("2000-02-29", &usec)); /* leap year */ | |
| - | |
| - /* NULL ret is allowed (validation only) */ | |
| - ASSERT_OK(parse_calendar_date("2000-06-15", NULL)); | |
| - | |
| - /* Non-normalized dates */ | |
| - ASSERT_ERROR(parse_calendar_date("2023-02-29", &usec), EINVAL); /* not a leap year */ | |
| - ASSERT_ERROR(parse_calendar_date("2023-04-31", &usec), EINVAL); /* April has 30 days */ | |
| - ASSERT_ERROR(parse_calendar_date("2023-13-01", &usec), EINVAL); /* month 13 */ | |
| - ASSERT_ERROR(parse_calendar_date("2023-00-01", &usec), EINVAL); /* month 0 */ | |
| - | |
| - /* Malformed input */ | |
| - ASSERT_ERROR(parse_calendar_date("", &usec), EINVAL); | |
| - ASSERT_ERROR(parse_calendar_date("not-a-date", &usec), EINVAL); | |
| - ASSERT_ERROR(parse_calendar_date("2023-06-15T00:00:00", &usec), EINVAL); /* trailing time */ | |
| - ASSERT_ERROR(parse_calendar_date("2023/06/15", &usec), EINVAL); /* wrong separator */ | |
| - ASSERT_ERROR(parse_calendar_date("06-15-2023", &usec), EINVAL); /* wrong order */ | |
| -} | |
| - | |
| -TEST(parse_birth_date) { | |
| - struct tm tm; | |
| - | |
| - /* Valid dates */ | |
| - ASSERT_OK(parse_birth_date("2000-06-15", &tm)); | |
| - ASSERT_EQ(tm.tm_year, 100); /* 2000 - 1900 */ | |
| - ASSERT_EQ(tm.tm_mon, 5); /* June, 0-indexed */ | |
| - ASSERT_EQ(tm.tm_mday, 15); | |
| - | |
| - /* Pre-epoch dates */ | |
| - ASSERT_OK(parse_birth_date("1960-03-25", &tm)); | |
| - ASSERT_EQ(tm.tm_year, 60); | |
| - ASSERT_EQ(tm.tm_mon, 2); | |
| - ASSERT_EQ(tm.tm_mday, 25); | |
| - | |
| - /* NULL ret is allowed (validation only) */ | |
| - ASSERT_OK(parse_birth_date("2000-01-01", NULL)); | |
| - | |
| - /* Non-date fields should not be relied upon */ | |
| - ASSERT_OK(parse_birth_date("2000-06-15", &tm)); | |
| - ASSERT_FALSE(BIRTH_DATE_IS_SET(BIRTH_DATE_UNSET)); | |
| - | |
| - /* Non-normalized dates */ | |
| - ASSERT_ERROR(parse_birth_date("2023-02-29", &tm), EINVAL); | |
| - ASSERT_ERROR(parse_birth_date("2023-04-31", &tm), EINVAL); | |
| - | |
| - /* Malformed input */ | |
| - ASSERT_ERROR(parse_birth_date("", &tm), EINVAL); | |
| - ASSERT_ERROR(parse_birth_date("not-a-date", &tm), EINVAL); | |
| - ASSERT_ERROR(parse_birth_date("2023-06-15T00:00:00", &tm), EINVAL); | |
| -} | |
| - | |
| DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro); | |
| diff --git a/src/test/test-user-record.c b/src/test/test-user-record.c | |
| index 480a3f33eb..c807ffe83a 100644 | |
| --- a/src/test/test-user-record.c | |
| +++ b/src/test/test-user-record.c | |
| @@ -96,15 +96,6 @@ TEST(self_changes) { | |
| SD_JSON_BUILD_PAIR_OBJECT("privileged", | |
| SD_JSON_BUILD_PAIR_UNSIGNED("notInHardCodedList", 99999))); | |
| ASSERT_TRUE(user_record_self_changes_allowed(curr, new)); | |
| - | |
| - /* birthDate is NOT self-modifiable (admin-only) */ | |
| - USER(&curr, | |
| - SD_JSON_BUILD_PAIR_STRING("userName", "test"), | |
| - SD_JSON_BUILD_PAIR_STRING("birthDate", "1990-01-01")); | |
| - USER(&new, | |
| - SD_JSON_BUILD_PAIR_STRING("userName", "test"), | |
| - SD_JSON_BUILD_PAIR_STRING("birthDate", "1990-06-15")); | |
| - ASSERT_FALSE(user_record_self_changes_allowed(curr, new)); | |
| } | |
| DEFINE_TEST_MAIN(LOG_INFO); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment