Skip to content

Commit b5a0574

Browse files
committed
Finalize Fluxheim 1.7.8 release
1 parent 4b65921 commit b5a0574

10 files changed

Lines changed: 84 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Fluxheim follows semantic versioning once `1.0.0` is released. Before `1.0.0`,
77
minor versions may still change configuration shape, feature names, and runtime
88
behavior when the change improves security or project direction.
99

10-
## 1.7.8 - Unreleased
10+
## 1.7.8 - 2026-07-11
1111

1212
### Added
1313

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Fluxheim is licensed under the European Union Public Licence 1.2.
116116

117117
| Capability | Status | Notes |
118118
| --- | --- | --- |
119-
| Admin API || Bearer-token auth, loopback defaults, brute-force throttling, authenticated health by default, snapshots, rollback, cache operations. |
119+
| Admin API || Bearer-token auth, loopback defaults, brute-force throttling, authenticated health by default, authenticated transactional snapshots, rollback, doctor/prune operations, and cache operations. |
120120
| Read-only ops socket || `1.4.1`; Unix-domain local status/cache/health endpoint with owner/group-only permissions; snapshot listing requires bearer auth. |
121121
| Prometheus metrics || Native metrics profile and bounded labels for edge/cache/LB/PHP events. |
122122
| OpenTelemetry || OTLP metrics and tracing export profiles. |

crates/fluxheim-server/src/native_http1_cache_purge.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@ use super::{NativeDiskCache, NativeDiskCacheLocation};
1111
static NATIVE_DISK_CACHE_PURGE_REGISTRY: OnceLock<Mutex<Vec<NativeDiskCachePurgeHandle>>> =
1212
OnceLock::new();
1313

14+
#[cfg(test)]
15+
std::thread_local! {
16+
static PURGE_REGISTRY_LOCK_HELD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
17+
}
18+
19+
#[cfg(test)]
20+
struct PurgeRegistryLockObservation;
21+
22+
#[cfg(test)]
23+
impl PurgeRegistryLockObservation {
24+
fn enter() -> Self {
25+
PURGE_REGISTRY_LOCK_HELD.with(|held| {
26+
assert!(
27+
!held.replace(true),
28+
"nested purge registry lock observation"
29+
);
30+
});
31+
Self
32+
}
33+
}
34+
35+
#[cfg(test)]
36+
impl Drop for PurgeRegistryLockObservation {
37+
fn drop(&mut self) {
38+
PURGE_REGISTRY_LOCK_HELD.with(|held| held.set(false));
39+
}
40+
}
41+
1442
#[derive(Clone, Debug)]
1543
struct NativeDiskCachePurgeHandle {
1644
vhost: Arc<str>,
@@ -214,10 +242,7 @@ pub(crate) fn register_native_disk_cache_purge_handle(
214242

215243
#[cfg(test)]
216244
pub(super) fn native_disk_cache_purge_registry_is_unlocked_for_test() -> bool {
217-
NATIVE_DISK_CACHE_PURGE_REGISTRY
218-
.get()
219-
.and_then(|registry| registry.try_lock().ok())
220-
.is_some()
245+
PURGE_REGISTRY_LOCK_HELD.with(|held| !held.get())
221246
}
222247

223248
pub fn purge_native_disk_cache_primary(
@@ -419,6 +444,8 @@ fn native_disk_cache_purge_targets() -> Vec<NativeDiskCachePurgeTarget> {
419444
std::process::abort();
420445
}
421446
};
447+
#[cfg(test)]
448+
let _lock_observation = PurgeRegistryLockObservation::enter();
422449
registry.retain(|handle| handle.cache.upgrade().is_some());
423450
registry
424451
.iter()

crates/fluxheim-server/src/native_http1_proxy_tests.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,14 @@ fn response_header(response: &str, name: &str) -> Option<String> {
241241
})
242242
}
243243

244-
async fn unused_local_address() -> std::net::SocketAddr {
244+
async fn rejecting_upstream() -> std::net::SocketAddr {
245245
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
246246
let address = listener.local_addr().unwrap();
247-
drop(listener);
247+
tokio::spawn(async move {
248+
while let Ok((stream, _)) = listener.accept().await {
249+
drop(stream);
250+
}
251+
});
248252
address
249253
}
250254

crates/fluxheim-server/src/native_http1_proxy_tests/h2.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use crate::{
1111
};
1212

1313
use super::{
14-
downstream_get, native_proxy_test_request_for, proxy_listener_for,
15-
static_load_balance_without_health_check, unused_local_address,
14+
downstream_get, native_proxy_test_request_for, proxy_listener_for, rejecting_upstream,
15+
static_load_balance_without_health_check,
1616
};
1717

1818
#[path = "h2_support.rs"]
@@ -278,7 +278,7 @@ async fn native_proxy_weighted_round_robins_successful_http2_static_upstreams()
278278

279279
#[tokio::test]
280280
async fn native_proxy_http2_safe_method_fails_over_to_second_static_upstream() {
281-
let first = unused_local_address().await;
281+
let first = rejecting_upstream().await;
282282
let (second, second_connections) = h2_upstream_with_body("h2 failover\n", 1).await;
283283
let proxy_config = fluxheim_config::ProxyConfig {
284284
upstreams: vec![first.to_string(), second.to_string()],
@@ -309,7 +309,7 @@ async fn native_proxy_http2_safe_method_fails_over_to_second_static_upstream() {
309309

310310
#[tokio::test]
311311
async fn native_proxy_http2_does_not_fail_over_unsafe_method() {
312-
let first = unused_local_address().await;
312+
let first = rejecting_upstream().await;
313313
let (second, second_connections) = h2_upstream_with_body("h2 unsafe replay\n", 1).await;
314314
let proxy_config = fluxheim_config::ProxyConfig {
315315
upstreams: vec![first.to_string(), second.to_string()],

crates/fluxheim-server/src/native_http1_proxy_tests/response_features.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ use crate::{DownstreamHttp1Policy, NativeHttp1Proxy};
1414

1515
#[cfg(feature = "compression-gzip")]
1616
use super::upstream;
17-
use super::{
18-
downstream_get, proxy_config_with_error_page, proxy_listener_for, unused_local_address,
19-
};
17+
use super::{downstream_get, proxy_config_with_error_page, proxy_listener_for, rejecting_upstream};
2018

2119
#[cfg(feature = "compression-gzip")]
2220
async fn downstream_request_bytes(proxy: std::net::SocketAddr, request: &str) -> Vec<u8> {
@@ -81,7 +79,7 @@ async fn native_proxy_serves_configured_error_page_on_bad_gateway() {
8179
let errors = tempfile::tempdir().unwrap();
8280
std::fs::write(errors.path().join("502.html"), "native custom 502\n").unwrap();
8381
let mut config = proxy_config_with_error_page(errors.path().to_path_buf());
84-
config.upstream = Some(unused_local_address().await.to_string());
82+
config.upstream = Some(rejecting_upstream().await.to_string());
8583
let proxy = NativeHttp1Proxy::from_proxy_config(&config, DownstreamHttp1Policy::default())
8684
.unwrap()
8785
.unwrap();
@@ -104,7 +102,7 @@ async fn native_proxy_falls_back_when_configured_error_page_is_too_large() {
104102
let oversized = std::fs::File::create(errors.path().join("502.html")).unwrap();
105103
oversized.set_len(64 * 1024 * 1024 + 1).unwrap();
106104
let mut config = proxy_config_with_error_page(errors.path().to_path_buf());
107-
config.upstream = Some(unused_local_address().await.to_string());
105+
config.upstream = Some(rejecting_upstream().await.to_string());
108106
let proxy = NativeHttp1Proxy::from_proxy_config(&config, DownstreamHttp1Policy::default())
109107
.unwrap()
110108
.unwrap();

crates/fluxheim-server/src/native_http1_proxy_tests/static_upstream.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ use crate::{NativeHttp1Proxy, NativeHttp1Upstream};
88

99
use super::{
1010
counting_upstream, downstream_get, failover_proxy_listener, proxy_listener_for,
11-
unused_local_address, upstream,
11+
rejecting_upstream, upstream,
1212
};
1313

1414
#[tokio::test]
1515
async fn native_proxy_fails_over_get_to_second_static_upstream() {
16-
let first = unused_local_address().await;
16+
let first = rejecting_upstream().await;
1717
let second = upstream(|request, mut stream| async move {
1818
let request = String::from_utf8(request).unwrap();
1919
assert!(request.starts_with("GET /failover HTTP/1.1\r\n"));
@@ -95,7 +95,7 @@ async fn native_proxy_weighted_round_robins_static_upstreams() {
9595

9696
#[tokio::test]
9797
async fn native_proxy_weighted_failover_skips_duplicate_slots() {
98-
let first = unused_local_address().await;
98+
let first = rejecting_upstream().await;
9999
let (second, second_count) = counting_upstream("second", 1).await;
100100
let proxy = NativeHttp1Proxy::from_weighted_upstreams(
101101
vec![
@@ -121,7 +121,7 @@ async fn native_proxy_weighted_failover_skips_duplicate_slots() {
121121

122122
#[tokio::test]
123123
async fn native_proxy_does_not_fail_over_unsafe_method() {
124-
let first = unused_local_address().await;
124+
let first = rejecting_upstream().await;
125125
let second = upstream(|_, mut stream| async move {
126126
stream
127127
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 6\r\n\r\nsecond")

docs/modularity-exceptions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,14 @@ line adds broader Wasm phases.
3232
| `crates/fluxheim-server/src/native_http1_route_wasm.rs` | 2043 | The native hook bridge now owns registry lookup, pre-submission security/cache admission, access decisions, bounded header host calls, bounded route/cache ABIs, and cache metadata aggregation. | Split header host-call state/mutation helpers, route-decision helpers, and cache-policy helpers into focused `native_http1_route_wasm_headers`, `native_http1_route_wasm_route_decision`, and `native_http1_route_wasm_cache_policy` modules. |
3333
| `crates/fluxheim-wasm/src/runtime.rs` | 1061 | Runtime construction now includes host bindings, compile-slot coordination, bounded async admission, shared epoch interruption, and execution policy checks. | Split admission, engine/ticker, compile-slot, execution, and host-call helpers into separate runtime submodules. |
3434
| `src/metrics.rs` | 505 | Root metrics compatibility wrappers remain just over the target after native/Wasm additions. | Move remaining Wasm metric wiring into the observability crate or a focused root adapter. |
35+
36+
The following files crossed the target during the broader `1.7.8` security and
37+
WASI integration pass. They remain temporary release exceptions with explicit
38+
split destinations; new functionality must not use them as precedent.
39+
40+
| File | Baseline lines | Reason | Split target |
41+
| --- | ---: | --- | --- |
42+
| `crates/fluxheim-geoip/src/lib.rs` | 567 | Runtime policy integration and CIRCL combined-schema support currently share the public model module. | Move runtime loading and provider-specific decoding into focused modules while preserving the public GeoIP types. |
43+
| `crates/fluxheim-server/src/native_http1_host_router.rs` | 545 | Trusted-client GeoIP context and Wasm policy construction expanded the central host-router assembly boundary. | Move GeoIP/Wasm policy assembly into a dedicated router policy builder. |
44+
| `crates/fluxheim-server/src/native_http1_host_router_tests.rs` | 516 | Host-router tests now cover trusted-proxy GeoIP and Wasm policy wiring alongside base routing. | Split GeoIP and Wasm construction regressions into focused test modules. |
45+
| `src/cli.rs` | 531 | Snapshot command dispatch and validated runtime loading remain together in the root CLI adapter. | Move snapshot subcommand execution into a dedicated CLI snapshot module. |

packaging/rpm/fluxheim.spec

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ fi
159159
- Require separate clock and randomness grants and reject all other WASI
160160
imports before module instantiation.
161161
- Add real WASI smoke and native listener coverage.
162+
- Add authenticated transactional snapshots with generation witnesses,
163+
provider-backed integrity, resilient doctor/prune operations, and safe legacy
164+
manifest bootstrap and migration.
165+
- Harden stream forwarding, PHP-FPM lifecycle and body handling, TLS input
166+
parsing, GeoIP database loading, and static-path invariants.
162167

163168
* Fri Jul 10 2026 Fluxheim Maintainers <1921261+eldryoth@users.noreply.github.com> - 1.7.7-1
164169
- Add opt-in wasm-proxy-abi compatibility preview namespace validation.

release-notes/RELEASE_NOTES_1.7.8.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ general-purpose WASI application hosting.
4848
- Add optional HMAC-SHA-256 manifests backed by an external bounded key file.
4949
Config and metadata are verified before rollback parsing; legacy stores are
5050
reported as unverified rather than silently authenticated.
51+
- Persist an authenticated generation high-water mark and per-manifest
52+
generation witnesses so pruning cannot reuse audit generations and freshness
53+
scans remain bounded without rereading complete snapshot configurations.
54+
- Preserve authenticated manifests created before generation witnesses. A
55+
fully verified all-legacy store with no generation counter bootstraps from
56+
its highest generation, persists authenticated state first, migrates its
57+
manifests, and publishes the next snapshot at `max + 1`. Missing state still
58+
fails closed for V2 and mixed stores.
59+
- Route snapshot SHA-256 and HMAC-SHA-256 through the selected Ring,
60+
OpenSSL-FIPS, or AWS-LC-FIPS provider, returning provider failures to the
61+
administrative caller instead of aborting the data plane.
62+
- Require owner-only snapshot state and integrity-key files, keep integrity
63+
keys outside the snapshot store, and authenticate intentional pruning
64+
boundaries.
5165
- Add resilient listing plus snapshot `show`, `diff`, `verify`, `doctor`, and
5266
protected `prune` operations. Snapshot TOML remains plaintext and needs
5367
encrypted storage or backups when confidentiality is required.
@@ -167,6 +181,10 @@ scripts/smoke_admin_listener.sh
167181
- Document the rootless Podman ownership mapping required for trusted read-only
168182
config mounts, including explicit `podman unshare chown`, an opt-in `:U`
169183
alternative, and an in-container verification command.
184+
- Existing authenticated snapshot stores created before generation witnesses
185+
upgrade automatically on their next locked snapshot creation only when every
186+
retained legacy manifest verifies. See `docs/config-snapshots.md` for the
187+
fail-closed mixed-store and external anti-rollback requirements.
170188
- CIRCL Geo Open users should follow `docs/geoip.md` for dataset attribution,
171189
trusted installation, pinned checksums, schema details, and the opt-in live
172190
database proof. The large network download remains outside normal CI gates.

0 commit comments

Comments
 (0)