blob: fea182885a4743eb7e6745d95ef5e8843d464ab9 [file] [edit]
/*
* Copyright (C) 2026 Igalia S.L.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include <wtf/TimeZone.h>
#include <gio/gio.h>
#include <mutex>
#include <wtf/NeverDestroyed.h>
#include <wtf/glib/FilePathWatcher.h>
#include <wtf/glib/GRefPtr.h>
#include <wtf/glib/GUniquePtr.h>
namespace WTF {
// /etc/localtime is the canonical location of the host time-zone binary on
// glibc/musl-based systems (localtime(5):
// https://man7.org/linux/man-pages/man5/localtime.5.html). The time-conversion
// functions implicitly call tzset(), which re-reads this file, so any change
// that takes effect for libc consumers necessarily mutates the path we watch
// here. See tzset(3):
// https://man7.org/linux/man-pages/man3/tzset.3.html
// FilePathWatcher catches symlink swap, replacement, and in-place rewrite —
// see the CHANGES_DONE_HINT / CREATED filter in FilePathWatcher.cpp.
static std::unique_ptr<FilePathWatcher>& localtimeWatcher()
{
static NeverDestroyed<std::unique_ptr<FilePathWatcher>> watcher;
return watcher.get();
}
// Listener for the "Timezone" property of org.freedesktop.timedate1, which
// systemd-timedated (and a few API-compatible reimplementations) emit on
// PropertiesChanged whenever timedatectl set-timezone (or any client of the
// same D-Bus method) succeeds. Spec:
// https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.timedate1.html
// https://github.com/systemd/systemd/blob/main/src/timedate/timedated.c
//
// Coverage:
// * Caught by the file watcher AND this listener: any tool that changes the
// TZ via timedatectl / GNOME / KDE / SetTimezone() — these always go
// through timedated, which rewrites /etc/localtime.
// * Caught by the file watcher only: a direct symlink swap of /etc/localtime
// by a privileged process that bypasses timedated.
// * Caught by this listener only: a TZ change inside a container/sandbox
// where /etc/localtime is bind-mounted from the host (so inotify on the
// guest path never fires) but the system bus is forwarded.
// * Not caught by either: a process-local TZ override applied to the
// calling process via setenv("TZ", ...) — there is no kernel notifier
// for environment changes, and tzset(3) defines TZ as a per-process
// override (https://man7.org/linux/man-pages/man3/tzset.3.html). Callers
// that need that path should call timeZoneDidChange() themselves (this is
// what setHostTimeZoneForTesting() does).
static GRefPtr<GDBusProxy>& timedate1Proxy()
{
static NeverDestroyed<GRefPtr<GDBusProxy>> proxy;
return proxy.get();
}
static void timedate1PropertiesChanged(GDBusProxy*, GVariant* changedProperties, char** invalidatedProperties, gpointer)
{
// "&s" points into changedProperties without allocating; we only need to
// know whether Timezone is present, not its value.
const char* timeZone = nullptr;
if (g_variant_lookup(changedProperties, "Timezone", "&s", &timeZone))
timeZoneDidChange();
else if (invalidatedProperties && g_strv_contains(invalidatedProperties, "Timezone"))
timeZoneDidChange();
}
static void onTimedate1ProxyReady(GObject*, GAsyncResult* result, gpointer)
{
// g_dbus_proxy_new_for_bus_finish returns null if the system bus is
// unreachable (e.g. sandboxed without bus forwarding) or proxy setup
// failed; in that case we fall back to the file watcher.
GUniqueOutPtr<GError> error;
GRefPtr<GDBusProxy> proxy = adoptGRef(g_dbus_proxy_new_for_bus_finish(result, &error.outPtr()));
if (!proxy) {
g_debug("TimeZone: timedate1 proxy unavailable, relying on the /etc/localtime watcher: %s", error.get() ? error->message : "unknown error");
return;
}
timedate1Proxy() = proxy;
g_signal_connect(proxy.get(), "g-properties-changed", G_CALLBACK(timedate1PropertiesChanged), nullptr);
}
void listenForTimeZoneChangeNotifications()
{
static std::once_flag onceKey;
std::call_once(onceKey, [] {
// Both the file monitor and the D-Bus proxy attach to the GMainContext
// that is thread-default when this first runs (the first DateCache
// construction drives it), so this must be reached on the main thread
// for its run loop to dispatch the notifications.
localtimeWatcher() = makeUnique<FilePathWatcher>("/etc/localtime"_s, [] {
timeZoneDidChange();
});
// Async proxy construction so we don't block on timedated being
// present; DO_NOT_AUTO_START avoids spuriously activating it just to
// observe its absence. If the service has no owner now, the proxy's
// NameOwnerChanged subscription will pick it up later.
g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
nullptr,
"org.freedesktop.timedate1",
"/org/freedesktop/timedate1",
"org.freedesktop.timedate1",
nullptr, onTimedate1ProxyReady, nullptr);
});
}
} // namespace WTF