Skip to content

Commit

Permalink
Re-enable obtaining local offset on Linux
Browse files Browse the repository at this point in the history
This will succeed if the process is single-threaded. A call to the
relevant methods will fail if other threads are present.

This only affects Linux and not other Unix-based systems.
  • Loading branch information
jhpratt committed Nov 12, 2021
1 parent a2b67af commit 77f8bfa
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ jobs:
matrix:
rust: ["1.51", stable]
os:
- { name: Ubuntu, value: ubuntu-20.04, has_local_offset: false }
- { name: Ubuntu, value: ubuntu-20.04, has_local_offset: true }
- { name: Windows, value: windows-latest, has_local_offset: true }
- { name: MacOS, value: macOS-latest, has_local_offset: false }

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ The format is based on [Keep a Changelog]. This project adheres to [Semantic Ver
- `OffsetDateTime::checked_add`
- `OffsetDateTime::checked_sub`

### Changed

- Attempts to obtain the local UTC offset will now succeed on Linux if the process is
single-threaded. This does not affect other Unix platforms. As a reminder, the relevant methods
are fallible and may return an `Err` value for any reason.

## 0.3.4 [2021-10-26]

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ rand = { version = "0.8.4", optional = true, default-features = false }
serde = { version = "1.0.126", optional = true, default-features = false }
time-macros = { version = "=0.2.3", path = "time-macros", optional = true }

[target.'cfg(unsound_local_offset)'.dependencies]
[target.'cfg(any(target_os = "linux", unsound_local_offset))'.dependencies]
libc = "0.2.98"

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
trivial_numeric_casts,
unreachable_pub,
unsafe_code,
// unsafe_op_in_unsafe_fn, // requires Rust 1.51
unused_extern_crates
)]
#![warn(
Expand Down
61 changes: 43 additions & 18 deletions src/sys/local_offset_at/unix.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
//! Get the system's UTC offset on Unix.

#[cfg(unsound_local_offset)]
#[cfg(any(target_os = "linux", unsound_local_offset))]
use core::convert::TryInto;
#[cfg(unsound_local_offset)]
#[cfg(any(target_os = "linux", unsound_local_offset))]
use core::mem::MaybeUninit;

use crate::{OffsetDateTime, UtcOffset};

/// Obtain the system's UTC offset.
// See #293 for details.
#[cfg(not(unsound_local_offset))]
#[cfg(not(any(target_os = "linux", unsound_local_offset)))]
#[allow(clippy::missing_const_for_fn)]
pub(super) fn local_offset_at(_datetime: OffsetDateTime) -> Option<UtcOffset> {
None
}

/// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on any error.
#[cfg(unsound_local_offset)]
fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
///
/// # 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.
#[cfg(any(target_os = "linux", unsound_local_offset))]
unsafe fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
extern "C" {
#[cfg_attr(target_os = "netbsd", link_name = "__tzset50")]
fn tzset();
Expand All @@ -32,30 +40,23 @@ fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
// Update timezone information from system. `localtime_r` does not do this for us.
//
// Safety: tzset is thread-safe.
#[allow(unsafe_code)]
unsafe {
tzset();
}
tzset();

// Safety: We are calling a system API, which mutates the `tm` variable. If a null
// pointer is returned, an error occurred.
#[allow(unsafe_code)]
let tm_ptr = unsafe { libc::localtime_r(&timestamp, tm.as_mut_ptr()) };
let tm_ptr = 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.
#[allow(unsafe_code)]
{
Some(unsafe { tm.assume_init() })
}
Some(tm.assume_init())
}
}

/// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error.
// `tm_gmtoff` extension
#[cfg(unsound_local_offset)]
#[cfg(any(target_os = "linux", unsound_local_offset))]
#[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
fn tm_to_offset(tm: libc::tm) -> Option<UtcOffset> {
let seconds: i32 = tm.tm_gmtoff.try_into().ok()?;
Expand Down Expand Up @@ -105,8 +106,32 @@ fn tm_to_offset(tm: libc::tm) -> Option<UtcOffset> {
.ok()
}

/// Determine if the current process is single-threaded. Returns `None` if this cannot be
/// determined.
#[cfg(target_os = "linux")]
fn process_is_single_threaded() -> Option<bool> {
std::fs::read_dir("/proc/self/task")
// If we can't read the directory, return `None`.
.ok()
// Check for the presence of multiple files in the directory. If there is exactly one then
// the process is single-threaded. This is indicated by the second element of the iterator
// (index 1) being `None`.
.map(|mut tasks| tasks.nth(1).is_none())
}

/// Obtain the system's UTC offset.
#[cfg(unsound_local_offset)]
#[cfg(any(target_os = "linux", unsound_local_offset))]
pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
tm_to_offset(timestamp_to_tm(datetime.unix_timestamp())?)
// 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 !cfg!(unsound_local_offset) && !matches!(process_is_single_threaded(), Some(true)) {
return None;
}

// 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(datetime.unix_timestamp())? };
tm_to_offset(tm)
}
20 changes: 9 additions & 11 deletions src/sys/local_offset_at/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use core::convert::TryInto;
use core::mem::MaybeUninit;

use crate::{OffsetDateTime, UtcOffset};

// ffi: WINAPI FILETIME struct
#[repr(C)]
#[allow(non_snake_case)]
Expand Down Expand Up @@ -42,16 +44,13 @@ extern "system" {
fn systemtime_to_filetime(systime: &SystemTime) -> Option<FileTime> {
let mut ft = MaybeUninit::uninit();

// Safety: `SystemTimeToFileTime` is thread-safe. We are only assuming initialization if
// the call succeeded.
#[allow(unsafe_code)]
{
if 0 == unsafe { SystemTimeToFileTime(systime, ft.as_mut_ptr()) } {
// failed
None
} else {
Some(unsafe { ft.assume_init() })
}
// Safety: `SystemTimeToFileTime` is thread-safe.
if 0 == unsafe { SystemTimeToFileTime(systime, ft.as_mut_ptr()) } {
// failed
None
} else {
// Safety: The call succeeded.
Some(unsafe { ft.assume_init() })
}
}

Expand Down Expand Up @@ -84,7 +83,6 @@ pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {

// Safety: `local_time` is only read if it is properly initialized, and
// `SystemTimeToTzSpecificLocalTime` is thread-safe.
#[allow(unsafe_code)]
let systime_local = unsafe {
let mut local_time = MaybeUninit::uninit();

Expand Down
2 changes: 2 additions & 0 deletions src/sys/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Functions with a common interface that rely on system calls.

#![allow(unsafe_code)] // We're interfacing with system calls.

#[cfg(feature = "local-offset")]
mod local_offset_at;

Expand Down

0 comments on commit 77f8bfa

Please sign in to comment.