Files
buzz/crates/buzz-feature-flags
Logan Johnson 9e8c9fc04d fix(deps): bump ctutils to 0.4.5 for RUSTSEC-2026-0385 (#8560)
🤖
## Summary
On 2026-10-23, RustSec published RUSTSEC-2026-0385 against `ctutils`, a
constant-time helper crate used by the RustCrypto hashing and HMAC
crates. Some of its functions were meant to run in constant time, so
their timing would reveal nothing about secret values, but in versions
before 0.4.4 they could compile to code that branches on those values.
Since the advisory came out, `cargo-deny check` reports advisories as
failed. That breaks `Security Domain / Security` on `main` and on every
PR that runs the Rust security job.

This PR changes `ctutils` from 0.4.2 to 0.4.5 in the three lockfiles
that pin it: `Cargo.lock`, `desktop/src-tauri/Cargo.lock`, and the
`buzz-feature-flags` relay-feature-selection test fixture. No other
packages change.

| Advisory | Crate | Change |
|---|---|---|
| [RUSTSEC-2026-0385](https://rustsec.org/advisories/RUSTSEC-2026-0385)
| `ctutils` (non-constant-time mask generation and `u128` comparisons) |
0.4.2 → 0.4.5 |

### Related issue
N/A. I searched open and closed PRs for `ctutils` and
`RUSTSEC-2026-0385` and found none.

### Testing
- `cargo deny check advisories`: `advisories ok`. On `origin/main` the
Security job fails on this advisory.
- `cargo check --workspace` passes.
- `cargo test -p buzz-feature-flags --test
relay_feature_selection_contract` passes. This test compiles the fixture
with `--locked`.
- The pre-push hook's Rust and Tauri lanes pass.

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:28:39 -04:00
..

buzz-feature-flags

Provider-neutral, typed feature-flag contracts for Buzz server crates.

This crate is infrastructure-only. It defines a small API for flag evaluation, plus a static evaluator and an optional LaunchDarkly adapter behind a compile feature.

Purpose

buzz-feature-flags centralizes typed flag evaluation so server crates can use flags without importing vendor SDK types.

Provider-neutral types:

  • BooleanFlag
  • IntegerFlag (i64, including negative values)
  • EvaluationContext (required CommunityId, optional actor PublicKey)
  • FlagEvaluator (typed evaluate_bool and evaluate_int)
  • StaticEvaluator (always returns declared defaults)
  • EnvironmentEvaluator (process-wide immutable environment snapshot)
  • EnvironmentDiagnostic (sanitized malformed-value report)

LaunchDarkly support is optional and compile-gated behind buzz-feature-flags/launchdarkly.

EnvironmentEvaluator is provider-neutral and always available (no Cargo feature and no external dependency).

Environment Variable Naming

EnvironmentEvaluator reads only the fixed namespace BUZZ_FEATURE_FLAG_.

For each flag key:

  • uppercase ASCII letters,
  • replace each run of non-ASCII-alphanumeric characters with _,
  • trim leading/trailing _,
  • prepend BUZZ_FEATURE_FLAG_.

Example: relay.feature.query-v2 → BUZZ_FEATURE_FLAG_RELAY_FEATURE_QUERY_V2.

Normalized names must be unique across declared flags. Collisions (for example a-b and a_b) intentionally map to the same environment variable. Enforcing uniqueness belongs in a future flag registry; this crate does not add a registry yet.

Relay Artifact Contract (not yet wired into buzz-relay)

The intended composition root is the outer relay artifact. It must select exactly one provider at compile time:

  • static-feature-flags
  • environment-feature-flags
  • launchdarkly-feature-flags

launchdarkly-feature-flags is the only relay feature that forwards to buzz-feature-flags/launchdarkly. static-feature-flags and environment-feature-flags keep the reusable crate vendor-neutral and still allow adapter code to compile in tests.

The relay artifact boundary, not this reusable crate, owns exact-one enforcement. Consumer integration PRs must carry exact-one compile_error guards verbatim/equivalently. That keeps buzz-feature-flags free to compile multiple adapters for tests while ensuring a staging or production relay artifact cannot choose a different evaluator at runtime.

This repository currently adds the crate and adapter, but does not yet wire relay AppState or handlers to consume it. crates/buzz-relay/Cargo.toml still has no dependency on buzz-feature-flags, so current workspace/default builds remain intentionally unchanged. The executable contract for the future relay integration lives in tests/fixtures/relay-feature-selection.

Composition Root Shape (artifact boundary)

Construct once at process startup, then inject shared Arc<dyn FlagEvaluator> into server components.

use std::sync::Arc;
use std::time::Duration;

use buzz_feature_flags::{EnvironmentEvaluator, FlagEvaluator, StaticEvaluator};
#[cfg(feature = "launchdarkly-feature-flags")]
use buzz_feature_flags::launchdarkly::{
    LaunchDarklyEvaluator, LaunchDarklyInitError, LaunchDarklyRuntimeConfig,
    LaunchDarklyStartError,
};

#[cfg(not(any(
    feature = "static-feature-flags",
    feature = "environment-feature-flags",
    feature = "launchdarkly-feature-flags",
)))]
compile_error!(
    "select exactly one relay feature flag provider feature: static-feature-flags, environment-feature-flags, or launchdarkly-feature-flags"
);

#[cfg(any(
    all(feature = "static-feature-flags", feature = "environment-feature-flags"),
    all(feature = "static-feature-flags", feature = "launchdarkly-feature-flags"),
    all(feature = "environment-feature-flags", feature = "launchdarkly-feature-flags"),
))]
compile_error!(
    "select exactly one relay feature flag provider feature: static-feature-flags, environment-feature-flags, or launchdarkly-feature-flags"
);

#[cfg(any(feature = "static-feature-flags", feature = "environment-feature-flags"))]
#[derive(Default)]
struct FeatureFlagRuntimeConfig;

#[cfg(feature = "launchdarkly-feature-flags")]
struct FeatureFlagRuntimeConfig {
    sdk_key: String,
    relay_proxy_endpoint: Option<String>,
}

struct RelayCompositionRoot {
    feature_flags: Arc<dyn FlagEvaluator>,
    environment_diagnostics: Option<Arc<EnvironmentEvaluator>>,
    #[cfg(feature = "launchdarkly-feature-flags")]
    launchdarkly_lifecycle: Option<Arc<LaunchDarklyEvaluator>>,
}

#[cfg(feature = "launchdarkly-feature-flags")]
enum RelayStartupError {
    LaunchDarklyInit(LaunchDarklyInitError),
    LaunchDarklyStart(LaunchDarklyStartError),
}

#[cfg(feature = "static-feature-flags")]
fn build_feature_flag_evaluator(_config: FeatureFlagRuntimeConfig) -> RelayCompositionRoot {
    RelayCompositionRoot {
        feature_flags: Arc::new(StaticEvaluator),
        environment_diagnostics: None,
    }
}

#[cfg(feature = "environment-feature-flags")]
fn build_feature_flag_evaluator(config: FeatureFlagRuntimeConfig) -> RelayCompositionRoot {
    let _ = config;
    let owner = Arc::new(EnvironmentEvaluator::from_process_environment());
    let feature_flags: Arc<dyn FlagEvaluator> = owner.clone();
    RelayCompositionRoot {
        feature_flags,
        environment_diagnostics: Some(owner),
    }
}

#[cfg(feature = "launchdarkly-feature-flags")]
async fn build_feature_flag_evaluator(
    config: FeatureFlagRuntimeConfig,
) -> Result<RelayCompositionRoot, RelayStartupError> {
    let mut runtime_config = LaunchDarklyRuntimeConfig::new(config.sdk_key);
    if let Some(relay_proxy_endpoint) = config.relay_proxy_endpoint {
        runtime_config = runtime_config.with_relay_proxy_endpoint(relay_proxy_endpoint);
    }

    let owner = Arc::new(
        LaunchDarklyEvaluator::from_runtime_config(runtime_config)
            .map_err(RelayStartupError::LaunchDarklyInit)?,
    );
    owner
        .start_with_default_executor_and_wait(Duration::from_secs(5))
        .await
        .map_err(RelayStartupError::LaunchDarklyStart)?;

    let feature_flags: Arc<dyn FlagEvaluator> = owner.clone();
    Ok(RelayCompositionRoot {
        feature_flags,
        environment_diagnostics: None,
        launchdarkly_lifecycle: Some(owner),
    })
}

Each relay feature produces exactly one build_feature_flag_evaluator path, and runtime config carries only the selected provider's settings. There is no runtime provider enum, no provider-name environment variable, and no fallback selector.

Startup is fail-closed: if the selected provider fails initialization (including start_with_default_executor_and_wait for LaunchDarkly), relay startup must return an error and stop rather than falling back to another evaluator.

The extra concrete Arc is an owner/lifecycle handle to that same evaluator, not a layered provider. Consumers receive only the cloned Arc<dyn FlagEvaluator> view. Retain the environment owner to drain diagnostics after evaluations, and retain the LaunchDarkly owner so shutdown can call close().

EnvironmentEvaluator snapshots environment values once at construction, retains only BUZZ_FEATURE_FLAG_ entries, ignores EvaluationContext, and never rereads process environment during evaluation.

For deterministic tests or embedding, EnvironmentEvaluator::from_pairs(...) constructs the same immutable snapshot from explicit key/value pairs.

Environment entries are untyped at construction, so malformed present values are detected when a typed flag is first evaluated. take_diagnostics() drains each sanitized diagnostic at most once per flag key and expected type. The composition root should retain the concrete environment evaluator and emit each drained diagnostic as a warning through the application's logging stack. Diagnostics contain no raw value or environment-variable name; the value snapshot itself remains immutable.

buzz-db Boundary (approved correction)

buzz-db may evaluate flags internally when choosing between equivalent query implementations. It should depend on buzz-feature-flags without enabling the LaunchDarkly feature, and it should never construct or import provider types.

Concise example (query path selection only):

use buzz_core::CommunityId;
use std::sync::Arc;

use buzz_feature_flags::{EvaluationContext, FlagEvaluator, IntegerFlag};

pub struct Db {
    feature_flags: Arc<dyn FlagEvaluator>,
}

impl Db {
    pub async fn list_events(
        &self,
        community: CommunityId,
    ) -> anyhow::Result<Vec<EventRow>> {
        let context = EvaluationContext::for_community(community);

        let query_version = self.feature_flags.evaluate_int(
            IntegerFlag::new("db.events.query-version", 1),
            &context,
        );

        if query_version >= 2 {
            self.list_events_v2_sql(community).await
        } else {
            self.list_events_v1_sql(community).await
        }
    }
}

SQL details are intentionally omitted in this example; the key point is that public Db methods accept only domain inputs, while feature-flag evaluator wiring remains internal to Db construction.

EvaluationContext::for_actor is appropriate only when the existing DB operation already receives an authoritative authenticated actor for domain behavior. Feature targeting must not add actor/pubkey parameters to otherwise actor-free DB APIs.

For LaunchDarkly, the adapter context always includes kind community plus optional global kind pubkey. Percentage rollouts must explicitly choose a kind present in evaluation: rollouts must set contextKind to a kind present in evaluation (community or pubkey) when they are intended to bucket Buzz traffic. LaunchDarkly rollouts where configuration omits contextKind defaults to user; Buzz does not synthesize a user alias, so those rollouts bucket as zero and pick the first positive-weight variation.

Guardrails

  • Evaluation context inputs are authoritative server-resolved values (community, optional actor), not client-supplied targeting attributes.
  • Flags may choose between equivalent implementations only.
  • Flags must not weaken authorization, tenant isolation, ordering guarantees, transaction/consistency behavior, or schema invariants.
  • Declared defaults choose the established safe path.
  • Integer values must be validated at the consumer boundary before affecting behavior.

Fallback, Startup, and Lifecycle

  • StaticEvaluator always returns each flag's declared default.
  • EnvironmentEvaluator returns declared defaults when values are missing, empty, invalid, non-Unicode, or the normalized flag key is unusable. Present malformed values also produce a sanitized diagnostic; missing values do not.
  • LaunchDarkly adapter returns declared defaults when a flag is missing, wrong type, or evaluation fails.
  • LaunchDarkly numbers are SDK f64 values. Integer evaluation accepts only mathematically integral values that convert to i64 exactly from that f64 representation; otherwise it returns each flag's declared default.
  • Startup chooses one evaluator at compile time (StaticEvaluator, EnvironmentEvaluator, or LaunchDarkly); provider precedence/stacking is out of scope.
  • If LaunchDarkly is used, call evaluator close() during process shutdown. close() blocks the calling thread while it flushes analytics, so async shutdown code should offload it to a blocking shutdown path.
  • Safety invariants must never rely on remote-flag availability.

Build and Test Expectations

Build modes:

# Provider-neutral (default): no LaunchDarkly dependency activated
cargo build -p buzz-feature-flags

# Provider-neutral environment evaluator is included in the default build
cargo test -p buzz-feature-flags --test environment_evaluator

# LaunchDarkly adapter enabled
cargo build -p buzz-feature-flags --features launchdarkly

Dependency-graph expectation checks:

# Default graph should exclude launchdarkly-server-sdk
cargo tree -p buzz-feature-flags

# Feature graph should include launchdarkly-server-sdk
cargo tree -p buzz-feature-flags --features launchdarkly

Testing expectations:

  • Run provider-neutral tests and LaunchDarkly-feature tests for this crate.
  • Run the relay-artifact compile contract in tests/relay_feature_selection_contract.rs, including zero-feature and multiple-feature rejection.
  • Keep tests/fixtures/relay-feature-selection/Cargo.lock checked in for the nested --locked compile contract. Regenerate it from repo root with:
cargo generate-lockfile --manifest-path crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.toml
  • When buzz-db adopts flag-gated query selection, add parity tests proving old/new query paths return equivalent rows, ordering, and transactional behavior for the same inputs.

References