Skip to content

fix(sdk): replace env::set_var with direct filter in FFI logging setup#3302

Open
QuantumExplorer wants to merge 4 commits intov3.1-devfrom
fix/ffi-env-set-var-thread-safety
Open

fix(sdk): replace env::set_var with direct filter in FFI logging setup#3302
QuantumExplorer wants to merge 4 commits intov3.1-devfrom
fix/ffi-env-set-var-thread-safety

Conversation

@QuantumExplorer
Copy link
Member

@QuantumExplorer QuantumExplorer commented Mar 15, 2026

Issue being fixed or feature implemented

Security audit found that dash_sdk_enable_logging calls std::env::set_var("RUST_LOG", ...) which is a data race in multi-threaded contexts. Since Rust 1.66+, env::set_var is deprecated as safe because it is unsound on POSIX systems when other threads may concurrently read environment variables. The Tokio runtime may already have worker threads active when this function is called.

What was done?

  • Replaced env::set_var("RUST_LOG", ...) with direct tracing_subscriber::EnvFilter::new(filter_string) construction
  • The filter string is built in-process with the same per-crate directives (dash_sdk, rs_sdk, dapi_grpc, h2, tower, hyper, tonic) that were previously stored in RUST_LOG
  • The tracing_subscriber::fmt subscriber is initialized directly with the filter via try_init(), which is safe to call multiple times (subsequent calls are no-ops)
  • Added tracing-subscriber with env-filter and fmt features as a direct dependency of rs-sdk-ffi
  • No environment variables are read or written by the new implementation

How Has This Been Tested?

  • Added unit test enable_logging_does_not_set_env_var that verifies RUST_LOG is never set by the function
  • Added unit test enable_logging_is_thread_safe that calls the function concurrently from multiple threads to confirm no panics or data races
  • All 232 existing rs-sdk-ffi lib tests pass
  • cargo clippy -p rs-sdk-ffi clean (no warnings)
  • cargo fmt --all clean

Breaking Changes

None. The function signature and behavior are unchanged -- logging output is identical, only the internal mechanism for configuring the tracing subscriber has changed.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Logging initialization is now safe to call repeatedly, thread-safe, and will not panic if a global logger is already set.
    • Logging is configured in-process and will prefer existing external logging directives instead of overwriting environment settings.
  • Tests

    • Added tests to verify non-reliance on environment variable mutation and safe concurrent initialization.
  • Documentation

    • Clarified logging behavior, thread-safety, and precedence of existing logging directives.

Replace `env::set_var("RUST_LOG", ...)` in `dash_sdk_enable_logging`
with direct `tracing_subscriber::EnvFilter` construction and subscriber
initialization. The previous approach was a data race when called after
the Tokio runtime (or any other threads) had started, since
`env::set_var` is unsound in multi-threaded POSIX processes and has been
deprecated as safe since Rust 1.66.

The new implementation constructs the same per-crate filter directives
in-process and passes them directly to `tracing_subscriber::fmt` via
`EnvFilter::new()`, which never reads or writes environment variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f96b161-2407-4cad-a14f-c8b4ad7de5e4

📥 Commits

Reviewing files that changed from the base of the PR and between 0cf75a0 and 7599f5f.

📒 Files selected for processing (1)
  • packages/rs-sdk-ffi/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rs-sdk-ffi/src/lib.rs

📝 Walkthrough

Walkthrough

Replaced environment-variable-based logging setup with an in-process tracing-subscriber configuration. dash_sdk_enable_logging now prefers RUST_LOG if present, otherwise builds a per-crate EnvFilter, installs a global tracing subscriber via try_init (no-op if already set), and includes tests ensuring RUST_LOG isn't modified and the call is thread-safe.

Changes

Cohort / File(s) Summary
Logging dependency
packages/rs-sdk-ffi/Cargo.toml
Added tracing-subscriber dependency with env-filter and fmt features.
Logging implementation & tests
packages/rs-sdk-ffi/src/lib.rs
Refactored dash_sdk_enable_logging to construct an in-process per-crate EnvFilter (prefer existing RUST_LOG via EnvFilter::try_from_default_env()), install a global tracing subscriber with try_init() (no panic if already set), removed std::env::set_var usage; added unit tests verifying RUST_LOG is not set and the initializer is thread-safe.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I tuned the traces with gentle hops,
No env-var digging, no noisy stops.
Threads may race, the setup stays bright,
Logs find their way without a fight,
I hop off grinning into the night. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing env::set_var with a direct filter approach in FFI logging setup, which is the core motivation and solution in this pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ffi-env-set-var-thread-safety
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added this to the v3.1.0 milestone Mar 15, 2026
@codecov
Copy link

codecov bot commented Mar 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.93%. Comparing base (e398cd7) to head (7599f5f).
⚠️ Report is 1 commits behind head on v3.1-dev.

Additional details and impacted files
@@            Coverage Diff            @@
##           v3.1-dev    #3302   +/-   ##
=========================================
  Coverage     79.93%   79.93%           
=========================================
  Files          2861     2861           
  Lines        280230   280230           
=========================================
+ Hits         223993   223997    +4     
+ Misses        56237    56233    -4     
Components Coverage Δ
dpp 71.82% <ø> (ø)
drive 82.30% <ø> (ø)
drive-abci 86.67% <ø> (ø)
sdk 36.55% <ø> (ø)
dapi-client 79.06% <ø> (ø)
platform-version ∅ <ø> (∅)
platform-value 83.36% <ø> (ø)
platform-wallet 76.55% <ø> (ø)
drive-proof-verifier 55.26% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 15, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "f4d753efb058e87df7eac90d82d26cac0342a19ee4f62818b96aec2bc5b8d5fd"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

@QuantumExplorer QuantumExplorer requested a review from lklimek March 15, 2026 14:58
@QuantumExplorer
Copy link
Member Author

@lklimek can you review this one?

@QuantumExplorer QuantumExplorer added the help wanted Extra attention is needed label Mar 15, 2026
@PastaPastaPasta
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/rs-sdk-ffi/src/lib.rs (1)

206-214: Log message may be misleading when subscriber is already installed.

When try_init() returns Err (subscriber already exists), the "logging enabled" message is still emitted via the existing subscriber, not the one we tried to create. This can mislead callers into thinking our per-crate filter was applied.

Per the context snippets, rs-drive-abci may initialize its own subscriber via try_init() before this FFI function runs, making this scenario likely in integrated deployments.

Consider either:

  1. Conditionally logging based on try_init() result, or
  2. Adjusting the message to clarify when the filter was actually applied.
♻️ Suggested improvement
-    // Initialize the global tracing subscriber.  `try_init` returns Err if a
-    // subscriber is already installed; we intentionally ignore that so that
-    // calling this function more than once is harmless.
-    let _ = tracing_subscriber::fmt::fmt()
+    // Initialize the global tracing subscriber. `try_init` returns Err if a
+    // subscriber is already installed; we intentionally ignore that so that
+    // calling this function more than once is harmless.
+    let installed = tracing_subscriber::fmt::fmt()
         .with_env_filter(filter)
-        .try_init();
+        .try_init()
+        .is_ok();
 
-    tracing::info!(level = log_level, "logging enabled");
+    if installed {
+        tracing::info!(level = log_level, "logging enabled with per-crate filter");
+    } else {
+        tracing::debug!(level = log_level, "subscriber already installed; per-crate filter not applied");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk-ffi/src/lib.rs` around lines 206 - 214, The current code
unconditionally emits tracing::info! after calling
tracing_subscriber::fmt().with_env_filter(filter).try_init(), which can mislead
when try_init() returns Err (subscriber already installed); update the init
sequence to capture the Result from
tracing_subscriber::fmt().fmt().with_env_filter(filter).try_init() and
conditionally log: on Ok(...) emit the existing "logging enabled" message
(including the applied filter or log_level if desired), and on Err(...) emit a
clear message that the subscriber was already installed and the per-crate filter
was not applied (or that existing settings remain); use the try_init() result to
drive which message is logged so callers of the FFI see accurate information.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/rs-sdk-ffi/src/lib.rs`:
- Around line 206-214: The current code unconditionally emits tracing::info!
after calling tracing_subscriber::fmt().with_env_filter(filter).try_init(),
which can mislead when try_init() returns Err (subscriber already installed);
update the init sequence to capture the Result from
tracing_subscriber::fmt().fmt().with_env_filter(filter).try_init() and
conditionally log: on Ok(...) emit the existing "logging enabled" message
(including the applied filter or log_level if desired), and on Err(...) emit a
clear message that the subscriber was already installed and the per-crate filter
was not applied (or that existing settings remain); use the try_init() result to
drive which message is logged so callers of the FFI see accurate information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a90f1cad-073c-4a0e-8ea7-eb4b6f683a7c

📥 Commits

Reviewing files that changed from the base of the PR and between dc17af2 and a8b5e7e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • packages/rs-sdk-ffi/Cargo.toml
  • packages/rs-sdk-ffi/src/lib.rs

Copy link
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Solid security fix replacing unsafe env::set_var with direct EnvFilter construction. The core change is correct and thread-safe. Two real issues: the filter omits the FFI crate's own tracing target (rs_sdk_ffi), and the regression test reintroduces the same class of unsafe env mutation this PR eliminates.

🟡 2 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-sdk-ffi/src/lib.rs`:
- [SUGGESTION] lines 197-200: FFI crate's own tracing events are silently filtered out
  The filter string includes `rs_sdk` but not `rs_sdk_ffi`. In tracing's EnvFilter, `rs_sdk` matches targets like `rs_sdk::foo` (child modules delimited by `::`) but does NOT match `rs_sdk_ffi` — that's a distinct crate target. This means `tracing::info!(...)` at line 213 (implicit target `rs_sdk_ffi`) is silently dropped, and any future diagnostics emitted from this crate are invisible. The old code had the same omission (RUST_LOG also lacked rs_sdk_ffi), so this isn't a regression, but since we're rebuilding the filter from scratch it's a good opportunity to fix it.
- [SUGGESTION] lines 231-239: Regression test reintroduces unsafe env mutation that the PR is eliminating
  The test calls `unsafe { std::env::remove_var("RUST_LOG") }` with a safety comment claiming the binary is single-threaded. This is incorrect: `cargo test` runs `#[test]` functions on parallel threads by default, so `enable_logging_does_not_set_env_var` may execute concurrently with `enable_logging_is_thread_safe` (which itself spawns additional threads). This is the same class of data race the PR is fixing.

Since the new implementation never touches RUST_LOG, the test can simply assert that `std::env::var("RUST_LOG").is_err()` without the remove_var preamble — or guard with `env::var_os` before asserting and skip if already set.

- Add rs_sdk_ffi to the tracing filter string so the FFI crate's own
  events are not silently dropped
- Only emit "logging enabled" info log on successful try_init (not on
  idempotent re-calls where the level is silently discarded)
- Remove unsafe env::remove_var from test — skip instead if RUST_LOG
  is already set, avoiding the same data race class the PR is fixing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean follow-up addressing all three prior review findings. The filter string now includes rs_sdk_ffi, the tracing::info! call is correctly gated on successful subscriber initialization, and the test no longer uses unsafe { env::remove_var } — replaced with a safe early-return guard. No new issues.

Reviewed commit: 1ea0a91

@lklimek lklimek self-assigned this Mar 20, 2026
lklimek and others added 2 commits March 20, 2026 09:17
Use try_from_default_env() to honor RUST_LOG when set, falling back to
the hardcoded per-crate filter. This reads but never writes env vars,
preserving thread safety. Also clarify that tracing-subscriber's
built-in tracing-log bridge handles log crate compatibility without
needing a separate env_logger init.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the rs-sdk-ffi logging initialization to avoid std::env::set_var("RUST_LOG", ...) (which is unsound in multi-threaded contexts) by configuring a tracing_subscriber filter directly inside the FFI entrypoint.

Changes:

  • Replaced RUST_LOG mutation with an in-process tracing_subscriber::EnvFilter + fmt subscriber initialization.
  • Added tracing-subscriber as a direct dependency of rs-sdk-ffi with env-filter and fmt features.
  • Added unit tests asserting dash_sdk_enable_logging doesn’t set RUST_LOG and can be called concurrently.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.

File Description
packages/rs-sdk-ffi/src/lib.rs Builds an explicit filter string and initializes a global tracing subscriber via try_init(), plus adds unit tests.
packages/rs-sdk-ffi/Cargo.toml Adds tracing-subscriber dependency needed by the new logging setup.
Cargo.lock Locks the new tracing-subscriber dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +181 to +183
/// The subscriber's built-in `tracing-log` bridge captures output from
/// crates that use the `log` facade, so a separate `env_logger::init()`
/// is not required.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment claims the fmt subscriber has a built-in tracing-log bridge and therefore captures crates using the log facade. In this crate there is no tracing-log dependency nor any tracing_log::LogTracer::init()/LogTracer setup, so log records will not be forwarded to tracing as described. Either install the log-to-tracing bridge explicitly (and add the needed dependency/feature) or adjust the documentation to avoid stating this behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +200 to +206
// Build the filter string with per-crate directives -- identical to what
// was previously stored in RUST_LOG, but constructed in-process so there
// is no data-race with concurrent `env::var` reads on other threads.
let filter_string = format!(
"dash_sdk={log_level},rs_sdk={log_level},rs_sdk_ffi={log_level},\
dapi_grpc={log_level},h2={log_level},tower={log_level},\
hyper={log_level},tonic={log_level}"
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the per-crate filter string is "identical" to what was previously stored in RUST_LOG, but this new string adds a rs_sdk_ffi=... directive that wasn't present before. Update the comment to reflect that this is a derived/default filter (or confirm and align the directive list with the previous behavior).

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +212
// Honour RUST_LOG when present (read-only, no data-race); fall back
// to the programmatic filter string otherwise.
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter_string));
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation reads RUST_LOG via EnvFilter::try_from_default_env(), but the PR description says "No environment variables are read or written by the new implementation." Either update the PR description to match the current behavior, or remove the env-based filter fallback if the intent is to avoid all env var access in this FFI entrypoint.

Copilot uses AI. Check for mistakes.
}

// Call the function under test with each supported level.
for level in 0..=5 {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test comment says it calls the function with each supported level, but the loop includes level = 5 (the public API docs list levels 0–4). Consider iterating 0..=4 to align with the documented range, or update the comment to clarify that this also exercises the out-of-range fallback behavior.

Suggested change
for level in 0..=5 {
for level in 0..=4 {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants