3.6 KiB
id, name, status, depends_on, scope, risk, impact, level, review_findings, adr
| id | name | status | depends_on | scope | risk | impact | level | review_findings | adr | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| fix/config-reload-toctou | Add mtime check to config reload to detect mid-write file changes (ADR-029) | pending | narrow | low | component | implementation |
|
|
Description
Both the SIGHUP reload path (src/shutdown.rs:handle_sighup_reload) and the
admin HTTP reload path (src/admin/socket.rs:handle_reload, soon
src/admin/handler.rs) read the config file from disk with
tokio::fs::read_to_string(), then parse and apply it. If another process is
writing to the config file at the same time, the proxy could read a partially
written config.
ADR-029 specifies a simple mitigation: compare file modification timestamps before and after reading. If mtime changed, reject the reload and return a "please retry" message.
Changes Required
Shared reload function — Extract the common file-read-and-validate logic
from src/shutdown.rs:handle_sighup_reload() and
src/admin/socket.rs:handle_reload() into a shared function (e.g.,
src/config/dynamic_config.rs or a new src/config/reload.rs):
pub async fn read_and_validate_config(
config_path: &str,
cli_allow_wildcard_bind: bool,
) -> Result<(StaticConfig, DynamicConfig), ReloadError> {
let metadata_before = tokio::fs::metadata(config_path).await
.map_err(ReloadError::Io)?;
let config_content = tokio::fs::read_to_string(config_path).await
.map_err(ReloadError::Io)?;
let metadata_after = tokio::fs::metadata(config_path).await
.map_err(ReloadError::Io)?;
if metadata_before.modified().ok() != metadata_after.modified().ok() {
return Err(ReloadError::FileChangedDuringRead);
}
let full_config = FullConfig::parse(&config_content)?;
let (new_static, new_dynamic) = full_config.into_static_and_dynamic();
validate(&new_static, &new_dynamic, cli_allow_wildcard_bind)?;
Ok((new_static, new_dynamic))
}
src/shutdown.rs — Replace inline file read + parse + validate with a
call to read_and_validate_config(). On ReloadError::FileChangedDuringRead,
log a warning: "config file changed during read, please retry SIGHUP".
src/admin/handler.rs (after admin-http-api task) — Same call. On
ReloadError::FileChangedDuringRead, return
{"status": "error", "message": "config file changed during read, please retry"}.
Error type — Define ReloadError enum with variants:
Io(std::io::Error)Parse(toml::de::Error)Validation(String)FileChangedDuringRead
Acceptance Criteria
- Both SIGHUP and admin HTTP reload paths use the same file-reading logic
- mtime is checked before and after reading the config file
- If mtime changed, reload is rejected with a clear error message
- Error message in admin HTTP response is generic ("config file changed during read, please retry") — no filesystem paths leaked
- Full error details are logged server-side (path, mtime values)
- SIGHUP path logs the same error at warn level
cargo testpassescargo clippypasses with no warnings
References
- docs/architecture/decisions/029-config-reload-toctou.md — ADR-029
- docs/reviews/005-admin-socket-security-review.md — W2 finding
- src/shutdown.rs — handle_sighup_reload
- src/admin/socket.rs — handle_reload (to be replaced by admin/handler.rs)
Notes
This fix is independent of the admin socket → HTTP migration. It applies to both reload paths (SIGHUP and admin). The implementation should be done after or alongside the admin-http-api task since that task replaces socket.rs with handler.rs.
Summary
To be filled on completion