Skip to content

Race condition between has_manual_override and manual_override #229

@meng-xu-cs

Description

@meng-xu-cs

NOTE: Possible duplicate of #101 / #201 (but hopefully this issue is closer to the root cause of the symptoms shown in previous issues).

Summary

ShouldColorize stores the manual override as two separate atomics:

has_manual_override: AtomicBool,
manual_override: AtomicBool,

and currently publishes them in this order:

pub fn set_override(&self, override_colorize: bool) {
    self.has_manual_override.store(true, Ordering::Relaxed);
    self.manual_override.store(override_colorize, Ordering::Relaxed);
}

while should_colorize() consumes them in this order:

pub fn should_colorize(&self) -> bool {
    if self.has_manual_override.load(Ordering::Relaxed) {
        return self.manual_override.load(Ordering::Relaxed);
    }
    // ...
}

A legal interleaving is:

  1. Initial state: has_manual_override = false, manual_override = false
  2. Thread A calls set_override(true) and executes has_manual_override.store(true, ...)
  3. Thread B calls should_colorize(), sees has_manual_override == true, then reads manual_override == false
  4. Thread A then executes manual_override.store(true, ...)

So should_colorize() can return the stale value even with one writer and one reader, without any competing set_override() calls.

ColoredString::has_colors() calls control::SHOULD_COLORIZE.should_colorize() on the normal formatting path, so the stale result directly affects whether ANSI escapes are emitted.

Expected behavior

Once a thread observes has_manual_override == true, it should also observe the override value that was meant to be published with that flag.

Actual behavior

A formatting thread can observe has_manual_override == true together with the previous manual_override value and make the opposite colorization decision.

Impact

This can transiently:

  • suppress ANSI escapes even though set_override(true) was requested, or
  • emit ANSI escapes even though set_override(false) was requested.

This is never meant to be a serious bug, just causing some confusions though.

Suggested fix

Publish the value first and then publish the flag with release semantics:

pub fn set_override(&self, override_colorize: bool) {
    self.manual_override.store(override_colorize, Ordering::Relaxed);
    self.has_manual_override.store(true, Ordering::Release);
}

pub fn should_colorize(&self) -> bool {
    if self.has_manual_override.load(Ordering::Acquire) {
        return self.manual_override.load(Ordering::Relaxed);
    }

    if let Some(forced_value) = self.clicolor_force {
        return forced_value;
    }

    self.clicolor
}

Another option would be to replace the pair with a single atomic enum / bitfield representing the full state (Auto, ForcedTrue, ForcedFalse), which avoids cross-atomic coherence issues entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions