Skip to content

Commit

Permalink
Use thread safe libtz instead of libc to get local timezone offset.
Browse files Browse the repository at this point in the history
Fixes #293 without needing to disable the feature.
  • Loading branch information
caldwell committed Jan 26, 2023
1 parent c45264c commit 3b93cc3
Show file tree
Hide file tree
Showing 3 changed files with 7 additions and 140 deletions.
10 changes: 0 additions & 10 deletions tests/utc_offset.rs
Expand Up @@ -161,13 +161,3 @@ fn current_local_offset() {
#[cfg(target_family = "unix")]
let _ = UtcOffset::current_local_offset();
}

#[test]
#[cfg(target_family = "unix")]
fn local_offset_error_when_multithreaded() {
std::thread::spawn(|| {
assert!(UtcOffset::current_local_offset().is_err());
})
.join()
.expect("failed to join thread");
}
5 changes: 2 additions & 3 deletions time/Cargo.toml
Expand Up @@ -26,7 +26,7 @@ default = ["std"]
alloc = ["serde?/alloc"]
formatting = ["dep:itoa", "std", "time-macros?/formatting"]
large-dates = ["time-macros?/large-dates"]
local-offset = ["std", "dep:libc", "dep:num_threads"]
local-offset = ["std", "dep:libtz"]
macros = ["dep:time-macros"]
parsing = ["time-macros?/parsing"]
quickcheck = ["dep:quickcheck", "alloc"]
Expand All @@ -49,8 +49,7 @@ time-core = { version = "=0.1.0", path = "../time-core" }
time-macros = { version = "=0.2.6", path = "../time-macros", optional = true }

[target.'cfg(target_family = "unix")'.dependencies]
libc = { version = "0.2.98", optional = true }
num_threads = { version = "0.1.2", optional = true }
libtz = { version = "0.1.0", optional = true }

[target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies]
js-sys = { version = "0.3.58", optional = true }
Expand Down
132 changes: 5 additions & 127 deletions time/src/sys/local_offset_at/unix.rs
@@ -1,135 +1,13 @@
//! Get the system's UTC offset on Unix.

use core::mem::MaybeUninit;
use libtz::Timezone;

use crate::util::local_offset::{self, Soundness};
use crate::{OffsetDateTime, UtcOffset};

/// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on any error.
///
/// # Safety
///
/// This method must only be called when the process is single-threaded.
///
/// This method will remain `unsafe` until `std::env::set_var` is deprecated or has its behavior
/// altered. This method is, on its own, safe. It is the presence of a safe, unsound way to set
/// environment variables that makes it unsafe.
unsafe fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
extern "C" {
#[cfg_attr(target_os = "netbsd", link_name = "__tzset50")]
fn tzset();
}

// The exact type of `timestamp` beforehand can vary, so this conversion is necessary.
#[allow(clippy::useless_conversion)]
let timestamp = timestamp.try_into().ok()?;

let mut tm = MaybeUninit::uninit();

// Update timezone information from system. `localtime_r` does not do this for us.
//
// Safety: tzset is thread-safe.
unsafe { tzset() };

// Safety: We are calling a system API, which mutates the `tm` variable. If a null
// pointer is returned, an error occurred.
let tm_ptr = unsafe { libc::localtime_r(&timestamp, tm.as_mut_ptr()) };

if tm_ptr.is_null() {
None
} else {
// Safety: The value was initialized, as we no longer have a null pointer.
Some(unsafe { tm.assume_init() })
}
}

/// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error.
// This is available to any target known to have the `tm_gmtoff` extension.
#[cfg(any(
target_os = "redox",
target_os = "linux",
target_os = "l4re",
target_os = "android",
target_os = "emscripten",
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd",
target_os = "haiku",
))]
fn tm_to_offset(_unix_timestamp: i64, tm: libc::tm) -> Option<UtcOffset> {
let seconds = tm.tm_gmtoff.try_into().ok()?;
UtcOffset::from_whole_seconds(seconds).ok()
}

/// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error.
///
/// This method can return an incorrect value, as it only approximates the `tm_gmtoff` field. The
/// reason for this is that daylight saving time does not start on the same date every year, nor are
/// the rules for daylight saving time the same for every year. This implementation assumes 1970 is
/// equivalent to every other year, which is not always the case.
#[cfg(not(any(
target_os = "redox",
target_os = "linux",
target_os = "l4re",
target_os = "android",
target_os = "emscripten",
target_os = "macos",
target_os = "ios",
target_os = "watchos",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd",
target_os = "haiku",
)))]
fn tm_to_offset(unix_timestamp: i64, tm: libc::tm) -> Option<UtcOffset> {
use crate::Date;

let mut tm = tm;
if tm.tm_sec == 60 {
// Leap seconds are not currently supported.
tm.tm_sec = 59;
}

let local_timestamp =
Date::from_ordinal_date(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1)
.ok()?
.with_hms(
tm.tm_hour.try_into().ok()?,
tm.tm_min.try_into().ok()?,
tm.tm_sec.try_into().ok()?,
)
.ok()?
.assume_utc()
.unix_timestamp();

let diff_secs = (local_timestamp - unix_timestamp).try_into().ok()?;

UtcOffset::from_whole_seconds(diff_secs).ok()
}

/// Obtain the system's UTC offset.
pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
// Ensure that the process is single-threaded unless the user has explicitly opted out of this
// check. This is to prevent issues with the environment being mutated by a different thread in
// the process while execution of this function is taking place, which can cause a segmentation
// fault by dereferencing a dangling pointer.
// If the `num_threads` crate is incapable of determining the number of running threads, then
// we conservatively return `None` to avoid a soundness bug.

if local_offset::get_soundness() == Soundness::Unsound
|| num_threads::is_single_threaded() == Some(true)
{
let unix_timestamp = datetime.unix_timestamp();
// Safety: We have just confirmed that the process is single-threaded or the user has
// explicitly opted out of soundness.
let tm = unsafe { timestamp_to_tm(unix_timestamp) }?;
tm_to_offset(unix_timestamp, tm)
} else {
None
}
let tz = Timezone::default().ok()?;
let tm = tz.localtime(datetime.unix_timestamp()).ok()?;
let seconds = tm.tm_gmtoff.try_into().ok()?;
UtcOffset::from_whole_seconds(seconds).ok()
}

0 comments on commit 3b93cc3

Please sign in to comment.