Skip to content

Commit f1c2497

Browse files
committed
Harden Fluxheim 1.7.7 release validation
1 parent 01d346c commit f1c2497

38 files changed

Lines changed: 297 additions & 82 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ temp/
5353
*.zst
5454
*.img
5555
*.qcow2
56+
/*.rpm
5657

5758
# Editor and OS noise
5859
.DS_Store

CHANGELOG.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ behavior when the change improves security or project direction.
2323

2424
### Changed
2525

26+
- Select an installed GCC 13/12/11 compiler pair automatically for
27+
release-mode rustls/AWS-LC FIPS validation when the system default compiler
28+
is outside the supported range.
2629
- Update direct dependency baselines to `base64-ng 1.3.7`, `bytes 1.12.1`,
2730
`regex 1.13.0`, `sanitization 1.2.4`, and test-only `wat 1.253.0`.
2831
- Update the workspace MSRV, pinned toolchain, and container builders to Rust
@@ -43,6 +46,8 @@ behavior when the change improves security or project direction.
4346
- Restore `fuzz/` as an intentionally standalone cargo-fuzz workspace, remove
4447
its obsolete Pingora patch, refresh its dependency lockfile, and make the
4548
fuzz validation gate compile every target automatically.
49+
- Run filesystem-sensitive smoke fixtures below private repository-owned roots
50+
and use compact paths for Unix-socket integration tests.
4651

4752
### Security
4853

@@ -69,8 +74,13 @@ behavior when the change improves security or project direction.
6974
encryption keys in `sanitization::SecretBytes<32>`.
7075
- Reject duplicate canonical storage-bin roots, verify persisted cache object
7176
keys before serving, and record strict Host-routing rejections in metrics.
72-
- Route cache inspection through registered live allocators and hold an
73-
exclusive filesystem lease for every active storage-bin root.
77+
- Route storage-bin cache inspection through registered live allocators and
78+
hold an exclusive filesystem lease for every active storage-bin root, while
79+
retaining standalone filesystem-backend CLI inspection.
80+
- Shorten generated managed PHP-FPM socket names and validate the complete Unix
81+
socket address before spawn instead of permitting PHP-FPM path truncation.
82+
- Map native HTTP/1 request-head limit and syntax failures to explicit `431`,
83+
`414`, and `400` responses before closing the connection.
7484
- Bound aggregate request-driven blocking work across Wasm, auth, mirrors,
7585
disk cache, and ACME at 256 beneath an explicit 384-thread Tokio blocking
7686
pool, preserving operational headroom.

crates/fluxheim-php-fpm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ fluxheim-protocol = { path = "../fluxheim-protocol" }
1414
getrandom = "0.4.3"
1515
log = "0.4.33"
1616
percent-encoding = "2.3.2"
17-
rustix = { version = "1.1.4", features = ["fs", "process"] }
17+
rustix = { version = "1.1.4", features = ["fs", "net", "process"] }
1818
tokio = { version = "1.52.3", default-features = false, features = ["fs", "io-util", "net", "rt", "time"] }
1919
zeroize = "1.9.0"
2020

crates/fluxheim-php-fpm/src/managed_config.rs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,12 @@ fn managed_php_fpm_instance_random() -> io::Result<u64> {
4141
}
4242

4343
pub(crate) fn managed_php_fpm_instance_name_from_parts(
44-
metric_pool: &str,
44+
_metric_pool: &str,
4545
pid: u32,
4646
counter: usize,
4747
random: u64,
4848
) -> io::Result<String> {
49-
let sanitized = metric_pool
50-
.bytes()
51-
.map(|byte| match byte {
52-
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' => byte as char,
53-
_ => '-',
54-
})
55-
.take(48)
56-
.collect::<String>();
57-
let sanitized = if sanitized.is_empty() {
58-
"php".to_owned()
59-
} else {
60-
sanitized
61-
};
62-
Ok(format!(
63-
"fluxheim-php-fpm-{sanitized}-{pid}-{counter}-{random:016x}"
64-
))
49+
Ok(format!("fh-fpm-{pid:x}-{counter:x}-{random:016x}"))
6550
}
6651

6752
pub fn managed_php_fpm_config(

crates/fluxheim-php-fpm/src/managed_process.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ impl ManagedPhpFpmProcess {
156156
})?;
157157
let name = managed_php_fpm_instance_name(metric_pool)?;
158158
let socket = socket_dir.join(format!("{name}.sock"));
159+
rustix::net::SocketAddrUnix::new(&socket).map_err(|error| {
160+
io::Error::new(
161+
io::ErrorKind::InvalidInput,
162+
format!(
163+
"{scope}: managed php-fpm socket path {} is not supported: {error}",
164+
socket.display()
165+
),
166+
)
167+
})?;
159168
let config_path = socket_dir.join(format!("{name}.conf"));
160169
let pid_path = socket_dir.join(format!("{name}.pid"));
161170
let error_log = socket_dir.join(format!("{name}.log"));

crates/fluxheim-php-fpm/src/tests_io_policy.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,19 @@ fn managed_php_fpm_restart_backoff_is_bounded() {
169169
}
170170

171171
#[test]
172-
fn managed_php_fpm_instance_names_are_sanitized_and_bounded() {
172+
fn managed_php_fpm_instance_names_are_compact_and_bounded() {
173173
assert_eq!(
174174
managed_php_fpm_instance_name_from_parts("pool/main:php", 42, 7, 0xfeed).unwrap(),
175-
"fluxheim-php-fpm-pool-main-php-42-7-000000000000feed"
175+
"fh-fpm-2a-7-000000000000feed"
176176
);
177177
assert_eq!(
178178
managed_php_fpm_instance_name_from_parts("", 42, 7, 0xfeed).unwrap(),
179-
"fluxheim-php-fpm-php-42-7-000000000000feed"
179+
"fh-fpm-2a-7-000000000000feed"
180180
);
181181

182182
let long_name =
183183
managed_php_fpm_instance_name_from_parts(&"a".repeat(96), 42, 7, 0xfeed).unwrap();
184-
assert!(long_name.contains(&"a".repeat(48)));
185-
assert!(!long_name.contains(&"a".repeat(49)));
184+
assert!(long_name.len() <= 48);
186185
}
187186

188187
#[test]

crates/fluxheim-server/src/native_http1.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ where
177177
let mut buffer = Vec::with_capacity(READ_CHUNK_BYTES);
178178

179179
loop {
180-
let Some(head_len) = timeout(
180+
let head_result = timeout(
181181
policy.request_head_timeout(),
182182
read_until_head(&mut stream, &mut buffer, limits),
183183
)
@@ -187,8 +187,15 @@ where
187187
std::io::ErrorKind::TimedOut,
188188
"request head timeout",
189189
))
190-
})??
191-
else {
190+
})?;
191+
let Some(head_len) = (match head_result {
192+
Ok(head_len) => head_len,
193+
Err(NativeHttp1Error::Parse(error)) => {
194+
write_request_head_error(&mut stream, &error).await?;
195+
return Ok(());
196+
}
197+
Err(error) => return Err(error),
198+
}) else {
192199
return Ok(());
193200
};
194201
let (close_after_response, body_framing, request) = {
@@ -338,6 +345,30 @@ where
338345
.await
339346
}
340347

348+
async fn write_request_head_error<S>(
349+
stream: &mut S,
350+
error: &Http1ParseError,
351+
) -> Result<(), NativeHttp1Error>
352+
where
353+
S: AsyncWrite + Unpin,
354+
{
355+
let response = match error {
356+
Http1ParseError::HeaderCountExceeded
357+
| Http1ParseError::HeaderLineTooLong
358+
| Http1ParseError::HeadTooLarge => NativeHttp1Response::new(
359+
431,
360+
"Request Header Fields Too Large",
361+
b"request header fields too large\n",
362+
),
363+
Http1ParseError::StartLineTooLong => {
364+
NativeHttp1Response::new(414, "URI Too Long", b"uri too long\n")
365+
}
366+
_ => NativeHttp1Response::new(400, "Bad Request", b"bad request\n"),
367+
}
368+
.close_connection();
369+
write_response(stream, response, true).await
370+
}
371+
341372
async fn read_until_head<S>(
342373
stream: &mut S,
343374
buffer: &mut Vec<u8>,

crates/fluxheim-server/src/native_http1_body_policy_tests.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,12 @@ async fn rejects_header_bytes_over_global_limit() {
135135
)
136136
.await
137137
.unwrap();
138-
let mut response = [0u8; 1];
139-
let read = stream.read(&mut response).await.unwrap();
138+
let response = read_response(&mut stream).await;
140139
let result = result_rx.await.unwrap();
141140

142-
assert_eq!(read, 0);
143-
assert!(matches!(
144-
result,
145-
Err(NativeHttp1Error::Parse(
146-
fluxheim_protocol::Http1ParseError::HeadTooLarge
147-
))
148-
));
141+
assert!(response.starts_with("HTTP/1.1 431 Request Header Fields Too Large\r\n"));
142+
assert!(response.ends_with("request header fields too large\n"));
143+
assert!(result.is_ok());
149144
}
150145

151146
#[tokio::test]

crates/fluxheim-server/src/native_http1_cache_inspect.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ use std::time::Instant;
33
use fluxheim_cache::{
44
CacheObjectFreshnessState, VaryRequestHashField, collect_cache_tags, vary_request_hash_material,
55
};
6-
use fluxheim_config::CacheConfig;
6+
use fluxheim_config::{CacheConfig, CacheDiskBackend};
77

88
use super::native_http1_cache_purge::registered_native_disk_cache;
9-
use super::{NativeDiskCacheObjectMetadata, native_instant_to_unix_secs};
9+
use super::{NativeDiskCache, NativeDiskCacheObjectMetadata, native_instant_to_unix_secs};
1010

1111
pub fn inspect_native_disk_cache_object(
1212
vhost: &str,
@@ -15,7 +15,12 @@ pub fn inspect_native_disk_cache_object(
1515
primary_key: &str,
1616
request_headers: &[(String, String)],
1717
) -> Option<NativeDiskCacheObjectMetadata> {
18-
let cache = registered_native_disk_cache(vhost, route)?;
18+
let cache = registered_native_disk_cache(vhost, route).or_else(|| {
19+
matches!(config.disk.backend, CacheDiskBackend::Filesystem)
20+
.then(|| NativeDiskCache::from_config(config))
21+
.flatten()
22+
.map(std::sync::Arc::new)
23+
})?;
1924
let entry = cache.get(primary_key, |fields| {
2025
native_inspection_vary_cache_key(primary_key, fields, request_headers)
2126
})?;

crates/fluxheim-server/src/native_http1_cache_tests.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,60 @@ fn live_cache_inspection_uses_registered_allocator_during_inserts() {
321321
assert_eq!(cache.stats().entries, 2);
322322
}
323323

324+
#[test]
325+
fn filesystem_cache_inspection_rebuilds_index_without_live_allocator() {
326+
let directory = tempfile::tempdir().unwrap();
327+
let config = CacheConfig {
328+
enabled: true,
329+
max_object_bytes: ByteSize::from_bytes(1024),
330+
disk: fluxheim_config::CacheDiskConfig {
331+
enabled: true,
332+
path: Some(directory.path().join("cache")),
333+
max_size_bytes: ByteSize::from_bytes(4096),
334+
..Default::default()
335+
},
336+
..Default::default()
337+
};
338+
let cache = NativeDiskCache::from_config(&config).unwrap();
339+
cache
340+
.store(
341+
disk_cache_store_key("standalone"),
342+
&disk_cache_entry(b"body"),
343+
)
344+
.unwrap();
345+
drop(cache);
346+
347+
let metadata = super::inspect_native_disk_cache_object(
348+
"standalone-inspection.test",
349+
None,
350+
&config,
351+
"standalone",
352+
&[],
353+
)
354+
.unwrap();
355+
356+
assert_eq!(metadata.body_bytes, 4);
357+
}
358+
359+
#[test]
360+
fn storage_bin_inspection_does_not_construct_unregistered_allocator() {
361+
let directory = tempfile::tempdir().unwrap();
362+
let root = directory.path().join("cache");
363+
let config = storage_bin_config(&root);
364+
365+
assert!(
366+
super::inspect_native_disk_cache_object(
367+
"unregistered-storage-bin.test",
368+
None,
369+
&config,
370+
"missing",
371+
&[],
372+
)
373+
.is_none()
374+
);
375+
assert!(!root.exists());
376+
}
377+
324378
const STORAGE_BIN_LEASE_CHILD_ROOT: &str = "FLUXHEIM_STORAGE_BIN_LEASE_CHILD_ROOT";
325379
const STORAGE_BIN_LEASE_CHILD_MAX_BYTES: &str = "FLUXHEIM_STORAGE_BIN_LEASE_CHILD_MAX_BYTES";
326380
const STORAGE_BIN_LEASE_CHILD_CONFIRMED: &str = "fluxheim-storage-bin-lease-child-confirmed";

0 commit comments

Comments
 (0)