diff --git a/BUGS-NEWS b/BUGS-NEWS index af9e301..56145d4 100644 --- a/BUGS-NEWS +++ b/BUGS-NEWS @@ -2,7 +2,45 @@ NEWS ==== This file summarizes user-visible changes to hostnamed, most recent first. -See CHANGELOG.md for the full commit-level history. +See the NEWS file for the full changelog. + +0.3.0 (2026-10-14) +------------------ +* BUILD CHANGE: polkit is now required. The -Dpolkit option is gone; + remove it from any meson setup command line or packaging script. +* Hostname validation is stricter (RFC 1123): labels over 63 + characters, empty labels such as "a..b", and labels beginning or + ending with "-" are now rejected. +* GetProductUUID now returns 16 raw bytes like systemd-hostnamed + (it previously returned the 36 ASCII characters of the textual + UUID). Clients that parsed the old form must be updated. It now + returns an error instead of an empty array when no UUID exists. +* Fixed spurious PropertiesChanged signals for Chassis and IconName + on every file change. +* IconName is derived from the effective chassis, so a CHASSIS value + in /etc/machine-info now selects the matching icon. +* DefaultHostname is now populated and HostnameSource can report + "default". +* SetPrettyHostname only shortens a value to its first label when the + value is a valid hostname; free-form labels containing dots are + kept intact. +* Hardware information falls back to the device tree on systems + without DMI (HardwareModel, HardwareVendor, Chassis). +* HardwareVendor and HardwareModel now skip empty and placeholder + DMI strings ("System manufacturer", "System Product Name", "To Be + Filled By O.E.M.", ...) and fall back from sys_vendor/product_name + to board_vendor/board_name, so desktop boards that only fill in + the baseboard record report their real vendor and model. +* GetHardwareSerial returns the first usable of product_serial, + board_serial and chassis_serial instead of chassis_serial only, + skipping placeholder values such as "Default string" and + "System Serial Number". On systems without DMI it falls back to + the device-tree serial-number property. +* Chassis detection recognises server, embedded and mini-PC + SMBIOS types; slack/hostnamectl uses the same table. +* Authorization that needs interactive authentication now fails with + org.freedesktop.DBus.Error.InteractiveAuthorizationRequired so + clients can retry with interactive=true. 0.2.6 (2026-06-20) ----------------------- @@ -58,7 +96,7 @@ See CHANGELOG.md for the full commit-level history. BUGS ==== -Known issues and limitations as of the Unreleased version above. +Known issues and limitations as of version 0.3.0. Please report new issues at https://github.com/nater1983/hostnamed/issues * GetProductUUID and GetHardwareSerial default to the auth_admin @@ -91,3 +129,35 @@ Please report new issues at https://github.com/nater1983/hostnamed/issues subject's next login/session start, consistent with how PolKit group subjects are evaluated; this is expected behavior but has surprised testers expecting it to apply live. +* Chassis is never reported as "vm" or "container": no virtualization + detection is performed, only machine-info, SMBIOS and device-tree + chassis information. +* The placeholder list is a fixed set of known strings; a board + that reports some other unfilled template text will still show + that text. product_version is deliberately not consulted, because + on many systems it holds a revision rather than a model name. +* On platforms with a device tree, HardwareVendor is the lowercase + vendor prefix of the "compatible" property (e.g. "raspberrypi"), + not a marketing name, and many boards provide no chassis-type, in + which case Chassis stays empty. +* After the D-Bus name is lost the daemon tries to re-acquire it, but + the startup path is not idempotent and the shared system-bus + connection exits the process on close by default, so recovery from + a dbus-daemon restart is not reliable; run hostnamed under a + supervisor. (Not yet reproduced; based on code review.) +* /etc/machine-info is re-read and re-parsed several times per + resync, only double-quoted values are understood, and the first + matching key wins rather than the last. +* hostnamectl creates /etc/machine-info through mktemp, leaving it + mode 0600 until chmod'd, and "hostnamectl set-hostname" overwrites + PRETTY_HOSTNAME with the short name. The chassis case tables in + hostnamectl must be kept in sync with the daemon by hand. +* hostnamed calls sethostname() with the full value of /etc/HOSTNAME. + Slackware's rc.S is believed to use only the short name (text + before the first dot) for the kernel hostname; verify against the + installed rc.S if you use a fully qualified /etc/HOSTNAME. +* The daemon never calls bindtextdomain(), so translations under po/ + are not loaded by hostnamed itself. +* hostnamed.SlackBuild defaults VERSION to "2026_master.3" rather + than the meson project version and copies a "BUG-NEWS" file that is + actually named BUGS-NEWS (the failure is hidden by "|| true"). diff --git a/NEWS b/NEWS index b4f88d4..b727e4c 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,33 @@ All notable changes to `hostnamed` are documented in this file, derived from the project's git history. +## [0.3.0] - 2026-10-14 + +### Changed (build / compatibility) +- **polkit is now a hard build dependency.** The `-Dpolkit=disabled` / `polkit` feature option has been removed, along with the unused `HAVE_POLKIT` define and every `polkit_found` conditional in the meson files. Previously a build without polkit failed at `#include ` anyway, and without polkit nothing could authorise the Set* methods. Anyone passing `-Dpolkit=...` to `meson setup` must drop it. +- Hostname validation (`SetHostname`, `SetStaticHostname`) is now strict RFC 1123: labels of 1-63 characters, no empty labels (`a..b`), no leading/trailing dot, and no label that starts or ends with `-`. Names that were previously accepted but are invalid under these rules are now rejected with `InvalidArgs`. +- `GetProductUUID` now returns the 16 raw UUID bytes (as systemd-hostnamed does) instead of the 36-character ASCII string as `ay`. It returns a `NotFound` error, rather than an empty array, when the UUID is unreadable, malformed, or the all-zero / all-0xFF placeholder. + +### Fixed +- Fixed bogus `PropertiesChanged` emissions: `Chassis` and `IconName` were blanked and re-set on every resync, producing duplicate dict keys and a signal on every inotify event. The effective value (machine-info, then fallbacks) is now computed first and updated once; three file touches now produce one signal instead of ten. +- `IconName` is now derived from the effective chassis string, so `CHASSIS=laptop` in `/etc/machine-info` without an `ICON_NAME` yields `computer-laptop` rather than `computer`. +- `DefaultHostname` is now populated (`DEFAULT_HOSTNAME` from os-release, else `localhost`); it was always empty. +- `HostnameSource` can now report `default` (static hostname unset and kernel hostname unset, `(none)`, or equal to the default); it previously only ever reported `static` or `transient`. +- `SetPrettyHostname` no longer truncates arbitrary labels at the first dot. The leading-label reduction now applies only when the whole value is a valid hostname, so `Bob's PC v2.0` is stored as given while `dair.example.net` still becomes `dair`. +- Polkit challenges are now reported as `org.freedesktop.DBus.Error.InteractiveAuthorizationRequired` when the caller passed `interactive=false` but authentication is possible, instead of a generic `AUTH_FAILED`, so GUI clients can retry with an agent dialog (matches localed). + +### Added +- Device-tree fallbacks for platforms without DMI (ARM, RISC-V, e.g. Raspberry Pi): `HardwareModel` from `/sys/firmware/devicetree/base/model`, `HardwareVendor` from the vendor prefix of the first `compatible` entry, and `Chassis` from `chassis-type` when it is a known value. +- `HardwareVendor` is now taken from DMI `sys_vendor`, then `board_vendor`, then the device tree; `HardwareModel` from `product_name`, then `board_name`, then the device tree. Empty values and the unfilled template strings firmware leaves behind (`System manufacturer`, `System Product Name`, `To Be Filled By O.E.M.`, `Default string`, ...) are skipped at each step, so boards such as the ASUS PRIME B550-PLUS now report the real board name and vendor instead of the placeholder. `slack/hostnamectl` uses the same logic and the same placeholder list (it now also reads the device tree and honours `DMI_DIR` / `DT_DIR` for testing). +- `GetHardwareSerial` now returns the first usable serial from DMI `product_serial`, `board_serial`, `chassis_serial` (it previously read `chassis_serial` only, which many boards leave as `Default string`), skipping the same placeholder values, and falls back to the device-tree `serial-number` property on platforms without a DMI serial (e.g. Raspberry Pi). +- Expanded chassis table: rack-mount, multi-system and blade types map to `server`; IoT gateway and embedded PC map to `embedded`; all-in-one, pizza-box, space-saving, lunch-box, sealed-case, mini-PC and stick-PC map to `desktop`. The case tables in `slack/hostnamectl` were updated to match. + +## [0.2.7.1] - 2026-06-27 to 2026-09-01 +- Added validation for hostname metadata values and corrected the machine-info path constant. +- Man pages for `hostnamed(8)` and `hostnamectl(1)` added and compressed in the SlackBuild. +- New and updated translations (Arabic Latin script, Indonesian, Estonian and others) via Weblate; project version bumped to 0.2.7 then 0.2.7.1. +- Removed `sbindir` paths from the build. + ## [0.2.6] - 2026-06-20 ### Security diff --git a/meson.build b/meson.build index 2368a7a..2479144 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'hostnamed', 'c', - version : '0.2.7.1', + version : '0.3.0', license : 'GPL-2.0-or-later', meson_version : '>= 0.58.0', default_options : [ diff --git a/slack/hostnamectl b/slack/hostnamectl index 1f0a6d1..7817573 100644 --- a/slack/hostnamectl +++ b/slack/hostnamectl @@ -45,7 +45,30 @@ get_os_release() { } dmi_read() { - cat "/sys/class/dmi/id/$1" 2>/dev/null || true + cat "${DMI_DIR:-/sys/class/dmi/id}/$1" 2>/dev/null || true +} + +# True for empty values and the unfilled template strings firmware leaves in +# DMI identity fields (e.g. ASUS: sys_vendor "System manufacturer", +# product_name "System Product Name"). Keep in sync with +# rcl_dmi_is_placeholder() in src/rcl-hostname.c. +dmi_is_placeholder() { + case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" in + ""|"system manufacturer"|"system product name"|"system version"| \ + "to be filled by o.e.m."|"default string"|"not applicable"| \ + "not specified"|"n/a"|"none"|"unknown"|"undefined"|"o.e.m."|"oem"| \ + "type1productconfigid"|"all series"|"system serial number"| \ + "chassis serial number"|"base board serial number"|"serial number") return 0 ;; + esac + return 1 +} + +# First usable argument, else empty. +first_usable() { + local v + for v in "$@"; do + dmi_is_placeholder "$v" || { printf '%s' "$v"; return; } + done } machine_info_get() { @@ -82,12 +105,13 @@ init_machine_info() { local ct ct=$(dmi_read chassis_type) case "$ct" in - 3|4|6|7) chassis="desktop"; icon="computer-desktop" ;; + 3|4|5|6|7|13|15|16|24|35|36) chassis="desktop"; icon="computer-desktop" ;; 8|9|10|14) chassis="laptop"; icon="computer-laptop" ;; 11) chassis="handset"; icon="phone" ;; - 17|23) chassis="server"; icon="computer-server" ;; + 17|23|25|28|29) chassis="server"; icon="computer-server" ;; 30|32) chassis="tablet"; icon="computer-tablet" ;; 31) chassis="convertible"; icon="computer-laptop" ;; + 33|34) chassis="embedded"; icon="computer" ;; *) chassis=""; icon="computer" ;; esac @@ -118,12 +142,13 @@ detect_chassis() { local ct ct=$(dmi_read chassis_type) case "$ct" in - 3|4|6|7) echo "desktop" ;; + 3|4|5|6|7|13|15|16|24|35|36) echo "desktop" ;; 8|9|10|14) echo "laptop" ;; 11) echo "handset" ;; - 17|23) echo "server" ;; + 17|23|25|28|29) echo "server" ;; 30|32) echo "tablet" ;; 31) echo "convertible" ;; + 33|34) echo "embedded" ;; *) echo "unknown" ;; esac } @@ -188,8 +213,19 @@ cmd_status() { kernel=$(uname -r) arch=$(uname -m) - hw_vendor=$(dmi_read sys_vendor) - hw_model=$(dmi_read product_name) + dt_dir=${DT_DIR:-/sys/firmware/devicetree/base} + dt_model="" + dt_vendor="" + # Only touch the device tree when the properties exist (they don't on x86); + # test first so a missing file never produces a shell redirection error. + if [ -r "$dt_dir/model" ]; then + dt_model=$(tr -d '\0' < "$dt_dir/model") + fi + if [ -r "$dt_dir/compatible" ]; then + dt_vendor=$(tr '\0' '\n' < "$dt_dir/compatible" | head -n1 | cut -d, -f1) + fi + hw_vendor=$(first_usable "$(dmi_read sys_vendor)" "$(dmi_read board_vendor)" "$dt_vendor") + hw_model=$(first_usable "$(dmi_read product_name)" "$(dmi_read board_name)" "$dt_model") fw_version=$(dmi_read bios_version) fw_date=$(dmi_read bios_date) diff --git a/src/rcl-hostname.c b/src/rcl-hostname.c index d38607c..88db2bf 100644 --- a/src/rcl-hostname.c +++ b/src/rcl-hostname.c @@ -164,45 +164,80 @@ static void rcl_daemon_init( RclDaemon *self ) {} -------------------------------------------------------------------------- */ /* - * SMBIOS chassis types → freedesktop icon names. - * Numeric values from SMBIOS spec 3.x, Table 17. + * Chassis handling. One table drives everything: the SMBIOS chassis type + * (SMBIOS spec 3.x, Table 17) maps to a Chassis string, and the Chassis + * string maps to an icon name. Keep this in sync with the case statements + * in slack/hostnamectl. */ const gchar * -rcl_chassis_type_to_icon_name( guint t ) +rcl_chassis_type_to_string( guint t ) { switch( t ) { - case 3: /* Desktop */ return "computer-desktop"; - case 4: /* Low Profile */ return "computer-desktop"; - case 6: /* Mini Tower */ return "computer-desktop"; - case 7: /* Tower */ return "computer-desktop"; - case 8: /* Portable */ return "computer-laptop"; - case 9: /* Laptop */ return "computer-laptop"; - case 10: /* Notebook */ return "computer-laptop"; - case 11: /* Hand-held */ return "phone"; - case 14: /* Sub-Notebook */ return "computer-laptop"; - case 18: /* Expansion Chassis*/ return "computer-desktop"; - case 21: /* Peripheral */ return "computer-desktop"; - case 30: /* Tablet */ return "computer-tablet"; - case 31: /* Convertible */ return "computer-laptop"; - case 32: /* Detachable */ return "computer-tablet"; - default: return "computer"; + case 3: /* Desktop */ + case 4: /* Low Profile Desktop*/ + case 5: /* Pizza Box */ + case 6: /* Mini Tower */ + case 7: /* Tower */ + case 13: /* All In One */ + case 15: /* Space-saving */ + case 16: /* Lunch Box */ + case 24: /* Sealed-case PC */ + case 35: /* Mini PC */ + case 36: /* Stick PC */ return "desktop"; + case 8: /* Portable */ + case 9: /* Laptop */ + case 10: /* Notebook */ + case 14: /* Sub Notebook */ return "laptop"; + case 11: /* Hand Held */ return "handset"; + case 17: /* Main Server */ + case 23: /* Rack Mount Server */ + case 25: /* Multi-system */ + case 28: /* Blade */ + case 29: /* Blade Enclosure */ return "server"; + case 30: /* Tablet */ + case 32: /* Detachable */ return "tablet"; + case 31: /* Convertible */ return "convertible"; + case 33: /* IoT Gateway */ + case 34: /* Embedded PC */ return "embedded"; + default: return ""; } } +gboolean +rcl_chassis_is_known( const gchar *c ) +{ + static const gchar *const known[] = + { "desktop", "laptop", "convertible", "server", "tablet", "handset", + "watch", "embedded", "vm", "container", NULL }; + guint i; + + for( i = 0; c && known[i]; i++ ) + if( g_strcmp0( c, known[i] ) == 0 ) + return TRUE; + return FALSE; +} + const gchar * -rcl_chassis_type_to_string( guint t ) +rcl_chassis_to_icon_name( const gchar *c ) { - switch( t ) - { - case 3: case 4: case 6: case 7: return "desktop"; - case 8: case 9: case 10: case 14: return "laptop"; - case 11: return "handset"; - case 17: return "server"; - case 30: case 32: return "tablet"; - case 31: return "convertible"; - default: return ""; - } + if( !c ) return "computer"; + if( g_strcmp0( c, "desktop" ) == 0 ) return "computer-desktop"; + if( g_strcmp0( c, "laptop" ) == 0 ) return "computer-laptop"; + if( g_strcmp0( c, "convertible" ) == 0 ) return "computer-laptop"; + if( g_strcmp0( c, "server" ) == 0 ) return "computer-server"; + if( g_strcmp0( c, "tablet" ) == 0 ) return "computer-tablet"; + if( g_strcmp0( c, "handset" ) == 0 ) return "phone"; + if( g_strcmp0( c, "watch" ) == 0 ) return "watch"; + if( g_strcmp0( c, "vm" ) == 0 ) return "computer-vm"; + if( g_strcmp0( c, "container" ) == 0 ) return "container"; + return "computer"; +} + +const gchar * +rcl_chassis_type_to_icon_name( guint t ) +{ + return rcl_chassis_to_icon_name( rcl_chassis_type_to_string( t ) ); } /* -------------------------------------------------------------------------- @@ -285,6 +320,70 @@ read_dmi( const gchar *path ) return val ? val : g_strdup( "" ); } +/* Firmware routinely leaves unfilled template strings in the DMI identity + fields (very common on retail desktop boards, e.g. ASUS reports + sys_vendor "System manufacturer" and product_name "System Product Name" + while the real names live in board_vendor / board_name). Treat those, + and empty values, as "not set" so the next source is used. */ +static gboolean +rcl_dmi_is_placeholder( const gchar *v ) +{ + static const gchar *const junk[] = { + "System manufacturer", "System Product Name", "System Version", + "To Be Filled By O.E.M.", "To be filled by O.E.M.", "Default string", + "Not Applicable", "Not Specified", "N/A", "None", "Unknown", "Undefined", + "O.E.M.", "OEM", "Type1ProductConfigId", "All Series", + "System Serial Number", "Chassis Serial Number", "Base Board Serial Number", + "Serial Number", NULL + }; + gchar *t; + gboolean bad = FALSE; + guint i; + + if( !v ) + return TRUE; + t = g_strstrip( g_strdup( v ) ); + if( t[0] == '\0' ) + bad = TRUE; + for( i = 0; !bad && junk[i]; i++ ) + if( g_ascii_strcasecmp( t, junk[i] ) == 0 ) + bad = TRUE; + g_free( t ); + return bad; +} + +/* First usable (non-empty, non-placeholder) of the given strings, as a + newly-allocated string; "" if none is usable. */ +static gchar * +first_usable_dup( const gchar *a, const gchar *b, const gchar *c ) +{ + if( !rcl_dmi_is_placeholder( a ) ) return g_strstrip( g_strdup( a ) ); + if( !rcl_dmi_is_placeholder( b ) ) return g_strstrip( g_strdup( b ) ); + if( !rcl_dmi_is_placeholder( c ) ) return g_strstrip( g_strdup( c ) ); + return g_strdup( "" ); +} + +/* Device-tree string property (NUL-terminated); "" when absent. */ +static gchar * +read_dt_string( const gchar *path ) +{ + gchar *v = read_first_line( path ); + return v ? v : g_strdup( "" ); +} + +/* The device-tree "compatible" property is a NUL-separated list of + "vendor,model" strings, most specific first. Return the vendor prefix of + the first entry (e.g. "raspberrypi" from "raspberrypi,5-model-b"), or "". */ +static gchar * +dt_vendor_from_compatible( const gchar *first_entry ) +{ + const gchar *comma; + + if( !first_entry || !( comma = strchr( first_entry, ',' ) ) || comma == first_entry ) + return g_strdup( "" ); + return g_strndup( first_entry, (gsize)( comma - first_entry ) ); +} + /* -------------------------------------------------------------------------- FirmwareDate parser @@ -317,6 +416,8 @@ parse_firmware_date_usec (const gchar *date_str) return (guint64) t * G_USEC_PER_SEC; } +static gboolean rcl_hostname_is_valid( const gchar *name ); + /* -------------------------------------------------------------------------- Property refresh -------------------------------------------------------------------------- */ @@ -381,46 +482,70 @@ rcl_daemon_sync_dbus_properties( RclHostnameDaemon *daemon ) read_key_value_file( RCL_MACHINE_INFO_FILE, "LOCATION" ), "Location" ); - /* ---- chassis / icon: machine-info wins, DMI is the fallback ---- - The effective value is computed first and then fed through - UPDATE_STR_PROP exactly once, so an unchanged value never produces a - PropertiesChanged entry (previously the machine-info read blanked the - field and the DMI fallback re-set it on every sync, emitting duplicate - Chassis/IconName entries each time). */ + /* ---- chassis / icon ---- + Chassis: machine-info, then DMI chassis type, then the device-tree + chassis-type property. Icon: machine-info, else derived from the + effective chassis string. Each value is computed first and passed + through UPDATE_STR_PROP exactly once, so an unchanged value never + produces a PropertiesChanged entry. */ { gchar *chassis_val = read_key_value_file( RCL_MACHINE_INFO_FILE, "CHASSIS" ); gchar *icon_val = read_key_value_file( RCL_MACHINE_INFO_FILE, "ICON_NAME" ); - gboolean need_chassis = ( chassis_val == NULL || chassis_val[0] == '\0' ); - gboolean need_icon = ( icon_val == NULL || icon_val[0] == '\0' ); - if( need_chassis || need_icon ) + if( chassis_val == NULL || chassis_val[0] == '\0' ) { gchar *dmi_chassis_type = read_dmi( RCL_DMI_CHASSIS_TYPE ); chassis_num = (guint) g_ascii_strtoull( dmi_chassis_type, NULL, 10 ); g_free( dmi_chassis_type ); - } - if( need_chassis ) - { g_free( chassis_val ); chassis_val = g_strdup( rcl_chassis_type_to_string( chassis_num ) ); + + if( chassis_val[0] == '\0' ) + { + gchar *dt = read_dt_string( RCL_DT_CHASSIS_TYPE ); + if( rcl_chassis_is_known( dt ) ) + { + g_free( chassis_val ); + chassis_val = dt; + } + else + g_free( dt ); + } } - if( need_icon ) + + if( icon_val == NULL || icon_val[0] == '\0' ) { g_free( icon_val ); - icon_val = g_strdup( rcl_chassis_type_to_icon_name( chassis_num ) ); + icon_val = g_strdup( rcl_chassis_to_icon_name( chassis_val ) ); } UPDATE_STR_PROP( chassis, chassis_val, "Chassis" ); UPDATE_STR_PROP( icon_name, icon_val, "IconName" ); } - /* ---- HostnameSource ---- */ + /* ---- DefaultHostname: os-release DEFAULT_HOSTNAME, else "localhost" ---- */ + { + gchar *def = read_key_value_file( RCL_OS_RELEASE_FILE, "DEFAULT_HOSTNAME" ); + if( !def || !rcl_hostname_is_valid( def ) ) + { + g_free( def ); + def = g_strdup( "localhost" ); + } + UPDATE_STR_PROP( default_hostname, def, "DefaultHostname" ); + } + + /* ---- HostnameSource ---- + static – a static hostname is configured + transient – none configured, kernel hostname was set to something else + default – none configured, kernel hostname unset or equal to default */ { const gchar *src; if( priv->static_hostname && priv->static_hostname[0] != '\0' ) src = "static"; - else if( priv->hostname && priv->hostname[0] != '\0' ) + else if( priv->hostname && priv->hostname[0] != '\0' && + g_strcmp0( priv->hostname, "(none)" ) != 0 && + g_strcmp0( priv->hostname, priv->default_hostname ) != 0 ) src = "transient"; else src = "default"; @@ -449,8 +574,24 @@ rcl_daemon_sync_dbus_properties( RclHostnameDaemon *daemon ) "OperatingSystemHomeURL" ); /* ---- DMI hardware info ---- */ - UPDATE_STR_PROP( hw_vendor, read_dmi(RCL_DMI_BOARD_VENDOR), "HardwareVendor" ); - UPDATE_STR_PROP( hw_model, read_dmi(RCL_DMI_PRODUCT_NAME), "HardwareModel" ); + /* Vendor: DMI system vendor, board vendor, then the device-tree + compatible prefix. Model: DMI product name, board name, then the + device-tree model. Placeholder strings are skipped at each step. */ + { + gchar *sys_v = read_dmi( RCL_DMI_SYS_VENDOR ); + gchar *board_v = read_dmi( RCL_DMI_BOARD_VENDOR ); + gchar *compat = read_dt_string( RCL_DT_COMPATIBLE ); + gchar *dt_v = dt_vendor_from_compatible( compat ); + gchar *prod = read_dmi( RCL_DMI_PRODUCT_NAME ); + gchar *board_n = read_dmi( RCL_DMI_BOARD_NAME ); + gchar *dt_m = read_dt_string( RCL_DT_MODEL ); + + UPDATE_STR_PROP( hw_vendor, first_usable_dup( sys_v, board_v, dt_v ), "HardwareVendor" ); + UPDATE_STR_PROP( hw_model, first_usable_dup( prod, board_n, dt_m ), "HardwareModel" ); + + g_free( sys_v ); g_free( board_v ); g_free( compat ); g_free( dt_v ); + g_free( prod ); g_free( board_n ); g_free( dt_m ); + } UPDATE_STR_PROP( firmware_version, read_dmi(RCL_DMI_BIOS_VERSION), "FirmwareVersion" ); /* FirmwareDate is type 't' (uint64 µs since epoch) per the spec */ @@ -654,7 +795,7 @@ write_machine_info_key( const gchar *key, static gboolean rcl_hostname_is_valid( const gchar *name ) { - gsize len, i; + gsize len, i, label_len = 0; if( !name ) return FALSE; @@ -663,16 +804,29 @@ rcl_hostname_is_valid( const gchar *name ) if( len == 0 || len > HOST_NAME_MAX ) return FALSE; - if( name[0] == '.' || name[len - 1] == '.' ) - return FALSE; - + /* RFC 1123: labels of 1-63 [A-Za-z0-9-] characters, separated by single + dots, not starting or ending with '-'. No leading/trailing/double dot. */ for( i = 0; i < len; i++ ) { gchar c = name[i]; - if( !( g_ascii_isalnum(c) || c == '-' || c == '.' ) ) + + if( c == '.' ) + { + if( label_len == 0 || name[i - 1] == '-' ) + return FALSE; + label_len = 0; + continue; + } + + if( !( g_ascii_isalnum( c ) || c == '-' ) ) + return FALSE; + if( c == '-' && label_len == 0 ) + return FALSE; + if( ++label_len > 63 ) return FALSE; } - return TRUE; + + return label_len > 0 && name[len - 1] != '-'; } /* -------------------------------------------------------------------------- @@ -698,8 +852,12 @@ rcl_derive_pretty_hostname( const gchar *name ) if( !dot ) return g_strdup( name ); - /* Looks like an FQDN/static hostname rather than a human label – - keep only the leading segment. */ + /* Only treat it as an FQDN/static hostname (and keep the leading label) + when the whole string is a valid hostname. A deliberate human label + such as "Bob's PC v2.0" is left untouched. */ + if( !rcl_hostname_is_valid( name ) ) + return g_strdup( name ); + return g_strndup( name, (gsize)(dot - name) ); } @@ -915,17 +1073,62 @@ do_set_machine_info( RclHostnameDaemon *daemon, const gchar *key, g_dbus_method_invocation_return_value( invocation, NULL ); } +/* Parse "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" into 16 raw bytes. Returns + FALSE for malformed input and for the all-zero / all-0xFF placeholder + UUIDs firmware uses to mean "not set". */ +static gboolean +parse_uuid_bytes( const gchar *str, guchar out[16] ) +{ + gsize n = 0; + gint hi = -1; + guchar or_all = 0, and_all = 0xff; + + for( ; *str; str++ ) + { + gint v; + + if( *str == '-' ) + continue; + v = g_ascii_xdigit_value( *str ); + if( v < 0 ) + return FALSE; + if( hi < 0 ) + hi = v; + else + { + if( n >= 16 ) + return FALSE; + out[n] = (guchar)( ( hi << 4 ) | v ); + or_all |= out[n]; + and_all &= out[n]; + n++; + hi = -1; + } + } + + return n == 16 && hi < 0 && or_all != 0 && and_all != 0xff; +} + static void do_get_product_uuid( GDBusMethodInvocation *invocation ) { gchar *uuid_str = read_dmi( RCL_DMI_PRODUCT_UUID ); + guchar raw[16]; GVariantBuilder bytes; - g_variant_builder_init( &bytes, G_VARIANT_TYPE("ay") ); - for( gsize i = 0; uuid_str[i]; i++ ) - g_variant_builder_add( &bytes, "y", (guchar)uuid_str[i] ); + if( !parse_uuid_bytes( uuid_str, raw ) ) + { + g_free( uuid_str ); + g_dbus_method_invocation_return_error( invocation, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "Product UUID is not available" ); + return; + } g_free( uuid_str ); + g_variant_builder_init( &bytes, G_VARIANT_TYPE("ay") ); + for( gsize i = 0; i < 16; i++ ) + g_variant_builder_add( &bytes, "y", raw[i] ); + g_dbus_method_invocation_return_value( invocation, g_variant_new("(ay)", &bytes) ); } @@ -933,7 +1136,24 @@ do_get_product_uuid( GDBusMethodInvocation *invocation ) static void do_get_hardware_serial( GDBusMethodInvocation *invocation ) { - gchar *serial = read_dmi( RCL_DMI_CHASSIS_SERIAL ); + gchar *prod_s = read_dmi( RCL_DMI_PRODUCT_SERIAL ); + gchar *board_s = read_dmi( RCL_DMI_BOARD_SERIAL ); + gchar *chas_s = read_dmi( RCL_DMI_CHASSIS_SERIAL ); + /* Retail boards commonly leave "Default string" / "System Serial Number" + in some of these; use the first one that holds a real value. */ + gchar *serial = first_usable_dup( prod_s, board_s, chas_s ); + + g_free( prod_s ); g_free( board_s ); g_free( chas_s ); + + /* No usable DMI serial (e.g. ARM boards such as the Raspberry Pi): use the + device-tree serial-number property. */ + if( serial[0] == '\0' ) + { + gchar *dt_s = read_dt_string( RCL_DT_SERIAL ); + g_free( serial ); + serial = first_usable_dup( dt_s, NULL, NULL ); + g_free( dt_s ); + } g_dbus_method_invocation_return_value( invocation, g_variant_new("(s)", serial) ); g_free( serial ); @@ -961,9 +1181,18 @@ on_check_done( GObject *source, GAsyncResult *res, gpointer user_data ) if( !polkit_authorization_result_get_is_authorized( result ) ) { - g_dbus_method_invocation_return_error( ctx->invocation, - G_DBUS_ERROR, G_DBUS_ERROR_AUTH_FAILED, - "Not authorised to perform the requested action" ); + /* is_challenge: authentication is possible but the caller passed + interactive=FALSE. Tell the client to retry with interactive=TRUE + (so a GUI can raise an agent dialog) rather than reporting a hard + denial. */ + if( polkit_authorization_result_get_is_challenge( result ) ) + g_dbus_method_invocation_return_dbus_error( ctx->invocation, + "org.freedesktop.DBus.Error.InteractiveAuthorizationRequired", + "Authentication is required to perform the requested action" ); + else + g_dbus_method_invocation_return_error( ctx->invocation, + G_DBUS_ERROR, G_DBUS_ERROR_AUTH_FAILED, + "Not authorised to perform the requested action" ); g_object_unref( result ); auth_ctx_free( ctx ); return; diff --git a/src/rcl-hostname.h b/src/rcl-hostname.h index 1fc92e1..16e7965 100644 --- a/src/rcl-hostname.h +++ b/src/rcl-hostname.h @@ -90,7 +90,7 @@ void rcl_daemon_set_debug ( RclDaemon *daemon, gboolean debug ); SetDeployment (s name, b interactive) – writes DEPLOYMENT to /etc/machine-info SetLocation (s name, b interactive) – writes LOCATION to /etc/machine-info GetProductUUID (b interactive) → ay – returns DMI product UUID as byte array - GetHardwareSerial (b interactive) → s – returns DMI chassis serial number + GetHardwareSerial (b interactive) → s – returns the first usable DMI serial (product, board, chassis) Describe () → s – returns JSON blob of all properties The 'interactive' boolean follows the systemd convention: when TRUE the @@ -156,14 +156,43 @@ void rcl_daemon_sync_dbus_properties ( RclHostnameDaemon *daemon ); ---------------------------------------------------------------------------*/ /* DMI sysfs paths (Linux-specific) */ +#ifndef RCL_DMI_CHASSIS_TYPE #define RCL_DMI_CHASSIS_TYPE "/sys/class/dmi/id/chassis_type" +#endif #define RCL_DMI_CHASSIS_VENDOR "/sys/class/dmi/id/chassis_vendor" +#ifndef RCL_DMI_PRODUCT_NAME #define RCL_DMI_PRODUCT_NAME "/sys/class/dmi/id/product_name" +#endif #define RCL_DMI_PRODUCT_UUID "/sys/class/dmi/id/product_uuid" #define RCL_DMI_CHASSIS_SERIAL "/sys/class/dmi/id/chassis_serial" +#define RCL_DMI_PRODUCT_SERIAL "/sys/class/dmi/id/product_serial" +#define RCL_DMI_BOARD_SERIAL "/sys/class/dmi/id/board_serial" #define RCL_DMI_BIOS_VERSION "/sys/class/dmi/id/bios_version" #define RCL_DMI_BIOS_DATE "/sys/class/dmi/id/bios_date" +#ifndef RCL_DMI_BOARD_VENDOR #define RCL_DMI_BOARD_VENDOR "/sys/class/dmi/id/board_vendor" +#endif +#ifndef RCL_DMI_BOARD_NAME +#define RCL_DMI_BOARD_NAME "/sys/class/dmi/id/board_name" +#endif +#ifndef RCL_DMI_SYS_VENDOR +#define RCL_DMI_SYS_VENDOR "/sys/class/dmi/id/sys_vendor" +#endif + +/* Device-tree fallbacks for platforms without DMI (ARM, RISC-V, e.g. the + Raspberry Pi). Overridable for tests. */ +#ifndef RCL_DT_MODEL +#define RCL_DT_MODEL "/sys/firmware/devicetree/base/model" +#endif +#ifndef RCL_DT_COMPATIBLE +#define RCL_DT_COMPATIBLE "/sys/firmware/devicetree/base/compatible" +#endif +#ifndef RCL_DT_SERIAL +#define RCL_DT_SERIAL "/sys/firmware/devicetree/base/serial-number" +#endif +#ifndef RCL_DT_CHASSIS_TYPE +#define RCL_DT_CHASSIS_TYPE "/sys/firmware/devicetree/base/chassis-type" +#endif /*--------------------------------------------------------------------------- Chassis type helpers. @@ -177,5 +206,7 @@ void rcl_daemon_sync_dbus_properties ( RclHostnameDaemon *daemon ); const gchar *rcl_chassis_type_to_icon_name ( guint chassis_type ); const gchar *rcl_chassis_type_to_string ( guint chassis_type ); +const gchar *rcl_chassis_to_icon_name ( const gchar *chassis ); +gboolean rcl_chassis_is_known ( const gchar *chassis ); G_END_DECLS