chore: add ASAN/MSAN/TSAN/SAFESTACK to CI (#740)

* feature: Add ASAN/TSAN to CI

* feature: Add SAFESTACK sanitizer

* feature: Add MEMORY sanitizer

* fix: Try to fix thread unsafety by leaking libraries

* fix: Suppress MSAN errors and clarify their future fix

* fix: Instead of supressing fuser errors, use patched version

* fix: Properly lazily initialize controllers

* fix: Add comment explaining why there is allowlist for bindings

* fix: Upgrade used nightly

* fix: Try to fix sanitizer compilation error

* Revert "fix: Try to fix sanitizer compilation error"

This reverts commit 47514158d5.

* Revert "fix: Upgrade used nightly"

This reverts commit c3dd005c7e.

* chore: Instead of personal fork revision, use official repo

* chore: Refactor to not pass arguments for statics

* chore: Add note for loading of libs

* chore: Try to fix codecov action

* chore: Fix compilation problem
This commit is contained in:
Alik Aslanyan
2025-09-10 21:22:30 +03:00
committed by GitHub
parent ef3dfe9ea6
commit cba0786004
10 changed files with 122 additions and 63 deletions
+29 -2
View File
@@ -73,7 +73,8 @@ jobs:
run: cargo llvm-cov --no-fail-fast --include-ffi --workspace --lcov --output-path lcov.info
- name: Upload to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5.5.1
continue-on-error: true
with:
files: lcov.info
fail_ci_if_error: true
@@ -100,6 +101,32 @@ jobs:
- name: Run tests inside Miri
run: cargo +nightly-2025-01-03 miri test
test-sanitizers:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
sanitizer: [address, thread, safestack, memory]
env:
LSAN_OPTIONS: fast_unwind_on_malloc=0:max_leak_stack_depth=50:print_suppressions=1
ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1
steps:
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- uses: ./.github/actions/setup-environment
- name: Install Nightly Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2025-01-03
override: true
components: rust-src
- name: Run tests with ${{ matrix.sanitizer }}
run: |
export RUSTFLAGS="-Zsanitizer=${{ matrix.sanitizer }}"
cargo +nightly-2025-01-03 -Zbuild-std test --target x86_64-unknown-linux-gnu --verbose
check-features-powerset:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }}
@@ -113,4 +140,4 @@ jobs:
- uses: ./.github/actions/setup-environment
- uses: taiki-e/install-action@cargo-hack
- name: Run cargo hack ${{ matrix.command }}
run: cargo hack ${{ matrix.command }}
run: cargo hack ${{ matrix.command }}
Generated
+1 -3
View File
@@ -818,15 +818,13 @@ checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
[[package]]
name = "fuser"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369"
source = "git+https://github.com/cberner/fuser.git?rev=47113e10ea4ab4be5b562cdc0d8cc8d41ce50311#47113e10ea4ab4be5b562cdc0d8cc8d41ce50311"
dependencies = [
"libc",
"log",
"memchr",
"nix 0.29.0",
"page_size",
"pkg-config",
"smallvec",
"zerocopy",
]
+5
View File
@@ -55,3 +55,8 @@ strip = false
debug = 1
lto = "thin"
codegen-units = 256
# fuser contains memory safety issues which trigger leaks and UB inside libfuse.so
# It's used only as dev dependency in the test code, so this is okay.
[patch.crates-io]
fuser = { git = "https://github.com/cberner/fuser.git", rev = "47113e10ea4ab4be5b562cdc0d8cc8d41ce50311" }
+19 -8
View File
@@ -5,6 +5,12 @@
"commit": "7e742c96146890f33fc9d1f9e0251f814d3b87aa",
"dest": "flatpak-cargo/git/cl3-7e742c9"
},
{
"type": "git",
"url": "https://github.com/cberner/fuser",
"commit": "47113e10ea4ab4be5b562cdc0d8cc8d41ce50311",
"dest": "flatpak-cargo/git/fuser-47113e1"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -1142,16 +1148,21 @@
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/fuser/fuser-0.15.1.crate",
"sha256": "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369",
"dest": "cargo/vendor/fuser-0.15.1"
"type": "shell",
"commands": [
"cp -r --reflink=auto \"flatpak-cargo/git/fuser-47113e1/.\" \"cargo/vendor/fuser\""
]
},
{
"type": "inline",
"contents": "{\"package\": \"53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369\", \"files\": {}}",
"dest": "cargo/vendor/fuser-0.15.1",
"contents": "[package]\nname = \"fuser\"\ndescription = \"Filesystem in Userspace (FUSE) for Rust\"\nlicense = \"MIT\"\nrepository = \"https://github.com/cberner/fuser\"\ndocumentation = \"https://docs.rs/fuser\"\nhomepage = \"https://github.com/cberner/fuser\"\nversion = \"0.15.1\"\nedition = \"2024\"\nrust-version = \"1.85\"\nreadme = \"README.md\"\nauthors = [\"Christopher Berner <me@cberner.com>\"]\nkeywords = [\"fuse\", \"filesystem\", \"system\", \"bindings\"]\ncategories = [\"external-ffi-bindings\", \"api-bindings\", \"filesystem\", \"os::unix-apis\"]\nbuild = \"build.rs\"\n\n[dependencies]\nlibc = \"0.2.51\"\nlog = \"0.4.6\"\nmemchr = \"2.7.2\"\npage_size = \"0.6.0\"\nsmallvec = \"1.6.1\"\n\n[dependencies.serde]\nversion = \"1.0.102\"\nfeatures = [\"std\", \"derive\"]\noptional = true\n\n[dependencies.zerocopy]\nversion = \"0.8\"\nfeatures = [\"derive\"]\n\n[dependencies.nix]\nversion = \"0.29.0\"\nfeatures = [\"fs\", \"user\"]\n\n[dev-dependencies]\nenv_logger = \"0.11.7\"\nbincode = \"1.3.1\"\ntempfile = \"3.10.1\"\n\n[dev-dependencies.clap]\nversion = \"4.4\"\nfeatures = [\"cargo\", \"derive\"]\n\n[dev-dependencies.serde]\nversion = \"1.0.102\"\nfeatures = [\"std\", \"derive\"]\n\n[dev-dependencies.nix]\nversion = \"0.29.0\"\nfeatures = [\"poll\", \"fs\", \"ioctl\"]\n\n[build-dependencies.pkg-config]\nversion = \"0.3.14\"\noptional = true\n\n[features]\ndefault = []\nlibfuse = [\"pkg-config\"]\nserializable = [\"serde\"]\nmacfuse-4-compat = []\nabi-7-19 = []\nabi-7-20 = [\"abi-7-19\"]\nabi-7-21 = [\"abi-7-20\"]\nabi-7-22 = [\"abi-7-21\"]\nabi-7-23 = [\"abi-7-22\"]\nabi-7-24 = [\"abi-7-23\"]\nabi-7-25 = [\"abi-7-24\"]\nabi-7-26 = [\"abi-7-25\"]\nabi-7-27 = [\"abi-7-26\"]\nabi-7-28 = [\"abi-7-27\"]\nabi-7-29 = [\"abi-7-28\"]\nabi-7-30 = [\"abi-7-29\"]\nabi-7-31 = [\"abi-7-30\"]\nabi-7-36 = [\"abi-7-31\"]\nabi-7-40 = [\"abi-7-36\", \"nix/ioctl\"]\n\n[[example]]\nname = \"poll\"\n\n[[example]]\nname = \"poll_client\"\n\n[[example]]\nname = \"notify_inval_entry\"\n\n[[example]]\nname = \"notify_inval_inode\"\n\n[[example]]\nname = \"ioctl\"\n\n[[example]]\nname = \"passthrough\"\nrequired-features = [\"abi-7-40\"]\n",
"dest": "cargo/vendor/fuser",
"dest-filename": "Cargo.toml"
},
{
"type": "inline",
"contents": "{\"package\": null, \"files\": {}}",
"dest": "cargo/vendor/fuser",
"dest-filename": ".cargo-checksum.json"
},
{
@@ -4510,7 +4521,7 @@
},
{
"type": "inline",
"contents": "[source.vendored-sources]\ndirectory = \"cargo/vendor\"\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.\"https://github.com/kenba/cl3\"]\ngit = \"https://github.com/kenba/cl3\"\nreplace-with = \"vendored-sources\"\nbranch = \"develop\"\n",
"contents": "[source.vendored-sources]\ndirectory = \"cargo/vendor\"\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.\"https://github.com/kenba/cl3\"]\ngit = \"https://github.com/kenba/cl3\"\nreplace-with = \"vendored-sources\"\nbranch = \"develop\"\n\n[source.\"https://github.com/cberner/fuser\"]\ngit = \"https://github.com/cberner/fuser\"\nreplace-with = \"vendored-sources\"\nrev = \"47113e10ea4ab4be5b562cdc0d8cc8d41ce50311\"\n",
"dest": "cargo",
"dest-filename": "config"
}
+4
View File
@@ -22,6 +22,10 @@ fn gen_intel_bindings() {
.header("include/intel.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.dynamic_library_name("IntelDrm")
// Don't generate bindings for functions and types related to file access
// This is needed to avoid IntelDrm structure being marked as !Send
.allowlist_item("(DRM|drm).*")
.blocklist_item(".*(FILE|file).*")
.generate_comments(false)
.generate()
.expect("Unable to generate intel bindings")
+10 -12
View File
@@ -15,7 +15,7 @@ use nvidia::NvidiaGpuController;
pub const VENDOR_AMD: &str = "1002";
pub const VENDOR_NVIDIA: &str = "10DE";
use crate::bindings::intel::IntelDrm;
use crate::server::handler::{AMD_DRM, INTEL_DRM, NVML};
use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable;
use anyhow::anyhow;
use anyhow::Context;
@@ -23,9 +23,10 @@ use futures::{future::LocalBoxFuture, FutureExt};
use lact_schema::{
config::GpuConfig, ClocksInfo, DeviceInfo, DeviceStats, GpuPciInfo, PciInfo, PowerStates,
};
use libdrm_amdgpu_sys::LibDrmAmdgpu;
use std::io;
use std::{cell::LazyCell, collections::HashMap, fs, path::PathBuf, rc::Rc};
#[cfg(feature = "nvidia")]
use std::sync::Arc;
use std::{collections::HashMap, fs, path::PathBuf, rc::Rc};
use tokio::{sync::Notify, task::JoinHandle};
use tracing::{error, warn};
@@ -131,19 +132,16 @@ pub struct PciSlotInfo {
}
#[cfg(feature = "nvidia")]
pub type NvidiaLibs = (Rc<Nvml>, Rc<Option<NvApi>>);
pub type NvidiaLibs = (Arc<Nvml>, Arc<Option<NvApi>>);
#[cfg(not(feature = "nvidia"))]
pub type NvidiaLibs = ();
pub(crate) fn init_controller(
path: PathBuf,
pci_db: &pciid_parser::Database,
nvml: &LazyCell<Option<NvidiaLibs>>,
amd_drm: &LazyCell<Option<LibDrmAmdgpu>>,
intel_drm: &LazyCell<Option<Rc<IntelDrm>>>,
) -> anyhow::Result<Box<dyn GpuController>> {
#[cfg(not(feature = "nvidia"))]
let _ = nvml;
let _ = NVML;
let uevent_path = path.join("uevent");
let uevent = fs::read_to_string(uevent_path).context("Could not read 'uevent'")?;
@@ -215,13 +213,13 @@ pub(crate) fn init_controller(
match common.driver.as_str() {
"amdgpu" | "radeon" => {
match AmdGpuController::new_from_path(common.clone(), amd_drm.as_ref()) {
match AmdGpuController::new_from_path(common.clone(), AMD_DRM.as_ref()) {
Ok(controller) => return Ok(Box::new(controller)),
Err(err) => error!("could not initialize AMD controller: {err:#}"),
}
}
"i915" | "xe" => {
if let Some(drm) = intel_drm.as_ref().cloned() {
if let Some(drm) = INTEL_DRM.as_ref() {
match IntelGpuController::new(common.clone(), drm) {
Ok(controller) => return Ok(Box::new(controller)),
Err(err) => error!("could not initialize Intel controller: {err:#}"),
@@ -232,8 +230,8 @@ pub(crate) fn init_controller(
}
#[cfg(feature = "nvidia")]
"nvidia" => {
if let Some((nvml, nvapi)) = nvml.as_ref().cloned() {
match NvidiaGpuController::new(common.clone(), nvml, nvapi) {
if let Some((nvml, nvapi)) = NVML.as_ref() {
match NvidiaGpuController::new(common.clone(), nvml, nvapi.as_ref().as_ref()) {
Ok(controller) => {
return Ok(Box::new(controller));
}
+1 -1
View File
@@ -75,7 +75,7 @@ impl AmdGpuController {
#[allow(unused_variables)]
pub fn new_from_path(
common: CommonControllerInfo,
libdrm_amdgpu: Option<&LibDrmAmdgpu>,
libdrm_amdgpu: Option<&'static LibDrmAmdgpu>,
) -> anyhow::Result<Self> {
let handle = GpuHandle::new_from_path(common.sysfs_path.clone())
.map_err(|error| anyhow!("failed to initialize gpu handle: {error}"))?;
@@ -30,7 +30,6 @@ use std::{
io::{BufRead, BufReader},
os::{fd::AsRawFd, raw::c_int},
path::{Path, PathBuf},
rc::Rc,
str::FromStr,
time::Instant,
};
@@ -63,7 +62,7 @@ pub struct IntelGpuController {
tile_gts: Vec<PathBuf>,
hwmon_path: Option<PathBuf>,
drm_file: fs::File,
drm: Rc<IntelDrm>,
drm: &'static IntelDrm,
last_drm_util: RefCell<Option<DrmUtilMap>>,
last_gpu_busy: Cell<Option<(Instant, u64)>>,
#[allow(dead_code)]
@@ -72,7 +71,7 @@ pub struct IntelGpuController {
}
impl IntelGpuController {
pub fn new(common: CommonControllerInfo, drm: Rc<IntelDrm>) -> anyhow::Result<Self> {
pub fn new(common: CommonControllerInfo, drm: &'static IntelDrm) -> anyhow::Result<Self> {
let driver_type = match common.driver.as_str() {
"xe" => DriverType::Xe,
"i915" => DriverType::I915,
@@ -297,7 +296,7 @@ impl IntelGpuController {
unsafe {
let mut out = T::default();
let result = f(&self.drm, self.drm_file.as_raw_fd(), &raw mut out);
let result = f(self.drm, self.drm_file.as_raw_fd(), &raw mut out);
if result == 0 {
Some(out)
} else {
@@ -48,8 +48,8 @@ const SUPPORTED_UTIL_TYPES: &[ProcessUtilizationType] = &[
];
pub struct NvidiaGpuController {
nvml: Rc<Nvml>,
nvapi: Rc<Option<NvApi>>,
nvml: &'static Nvml,
nvapi: Option<&'static NvApi>,
common: CommonControllerInfo,
fan_control_handle: RefCell<Option<FanControlHandle>>,
@@ -67,8 +67,8 @@ pub struct NvidiaGpuController {
impl NvidiaGpuController {
pub fn new(
common: CommonControllerInfo,
nvml: Rc<Nvml>,
nvapi: Rc<Option<NvApi>>,
nvml: &'static Nvml,
nvapi: Option<&'static NvApi>,
) -> anyhow::Result<Self> {
let device = nvml
.device_by_pci_bus_id(common.pci_slot_name.as_str())
@@ -166,7 +166,7 @@ impl NvidiaGpuController {
let notify = Rc::new(Notify::new());
let task_notify = notify.clone();
let nvml = self.nvml.clone();
let nvml = self.nvml;
let pci_slot_id = self.common.pci_slot_name.clone();
debug!("spawning new fan control task");
+45 -28
View File
@@ -31,8 +31,10 @@ use pciid_parser::Database;
use serde_json::json;
#[cfg(not(test))]
use std::collections::HashMap;
#[cfg(all(not(test), feature = "nvidia"))]
use std::sync::Arc;
use std::{
cell::{Cell, LazyCell, RefCell},
cell::{Cell, RefCell},
collections::BTreeMap,
env,
fs::{self, File, Permissions},
@@ -40,6 +42,7 @@ use std::{
os::unix::fs::{MetadataExt, PermissionsExt},
path::{Path, PathBuf},
rc::Rc,
sync::LazyLock,
time::{Duration, Instant},
};
use tokio::{
@@ -1055,19 +1058,20 @@ pub(crate) fn read_pci_db() -> Database {
})
}
/// `sysfs_only` disables initialization of any external data sources, such as libdrm and nvml
fn load_controllers(
base_path: &Path,
pci_db: &Database,
) -> anyhow::Result<BTreeMap<String, DynGpuController>> {
let mut controllers = BTreeMap::new();
#[cfg(any(test, not(feature = "nvidia")))]
pub(crate) static NVML: LazyLock<Option<NvidiaLibs>> = LazyLock::new(|| None);
#[cfg(all(not(test), feature = "nvidia"))]
let nvml: LazyCell<Option<NvidiaLibs>> = LazyCell::new(|| match Nvml::init() {
#[cfg(all(not(test), feature = "nvidia"))]
// SAFETY: We use global LazyLock to make sure it's safe.
// Loading of shared libaries is unsafe
// https://docs.rs/libloading/0.8.8/libloading/struct.Library.html#method.new
#[allow(unused_unsafe)]
pub(crate) static NVML: LazyLock<Option<NvidiaLibs>> =
LazyLock::new(|| match unsafe { Nvml::init() } {
Ok(nvml) => {
use crate::server::gpu_controller::NvApi;
// The config has to be re-read here, because a LazyCell cannot capture external variables into the init closure
// The config has to be re-read here, because a LazyLock cannot capture external variables into the init closure
let disable_nvapi = Config::load()
.ok()
.flatten()
@@ -1088,17 +1092,21 @@ fn load_controllers(
.ok()
};
Some((Rc::new(nvml), Rc::new(nvapi)))
Some((Arc::new(nvml), Arc::new(nvapi)))
}
Err(err) => {
error!("could not load Nvidia management library: {err}");
None
}
});
#[cfg(any(test, not(feature = "nvidia")))]
let nvml: LazyCell<Option<NvidiaLibs>> = LazyCell::new(|| None);
let amd_drm: LazyCell<Option<LibDrmAmdgpu>> = LazyCell::new(|| match LibDrmAmdgpu::new() {
pub(crate) static AMD_DRM: LazyLock<Option<LibDrmAmdgpu>> = LazyLock::new(|| {
// SAFETY: We use global LazyLock to make sure it's safe.
#[allow(unused_unsafe)]
// Loading of shared libaries is unsafe
// https://github.com/Umio-Yasuno/libdrm-amdgpu-sys-rs/issues/12
// https://docs.rs/libloading/0.8.8/libloading/struct.Library.html#method.new
match unsafe { LibDrmAmdgpu::new() } {
Ok(drm) => {
info!("AMDGPU DRM initialized");
Some(drm)
@@ -1107,20 +1115,29 @@ fn load_controllers(
error!("failed to initialize AMDGPU DRM: {err}, some functionality will be missing");
None
}
});
}
});
let intel_drm: LazyCell<Option<Rc<IntelDrm>>> = unsafe {
LazyCell::new(|| match IntelDrm::new("libdrm_intel.so.1") {
Ok(drm) => {
info!("Intel DRM initialized");
Some(Rc::new(drm))
}
Err(err) => {
error!("failed to initialize Intel DRM: {err}");
None
}
})
};
pub(crate) static INTEL_DRM: LazyLock<Option<IntelDrm>> = LazyLock::new(|| {
// SAFETY: We use global LazyLock to make sure it's safe.
match unsafe { IntelDrm::new("libdrm_intel.so.1") } {
Ok(drm) => {
info!("Intel DRM initialized");
Some(drm)
}
Err(err) => {
error!("failed to initialize Intel DRM: {err}");
None
}
}
});
/// `sysfs_only` disables initialization of any external data sources, such as libdrm and nvml
fn load_controllers(
base_path: &Path,
pci_db: &Database,
) -> anyhow::Result<BTreeMap<String, DynGpuController>> {
let mut controllers = BTreeMap::new();
for entry in base_path
.read_dir()
@@ -1136,7 +1153,7 @@ fn load_controllers(
trace!("trying gpu controller at {:?}", entry.path());
let device_path = entry.path().join("device");
match init_controller(device_path.clone(), pci_db, &nvml, &amd_drm, &intel_drm) {
match init_controller(device_path.clone(), pci_db) {
Ok(controller) => {
let info = controller.controller_info();
let id = info.build_id();