From f76ea24ab25dca252ad09edf1b9e8888948241ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20dos=20Santos=20Lopes?= Date: Tue, 12 May 2026 12:12:36 +0100 Subject: [PATCH] fix(helper-rust): treat cleared shmem as no-config rather than error When the sidecar's last runtime for a target disconnects, expired() clears the shmem file (size=1, NUL byte). A freshly-started sidecar's Service::new() calls poll_and_apply_rc(), which reads this cleared state and gets an empty data slice. ConfigDirectory::runtime_id() and iter() were erroring with "No LF in remote config" instead of treating empty data as "no config available". Libdatadog's own RemoteConfigManager::fetch_update() already handles this correctly with an explicit if !data.is_empty() guard. Mirror that here: add is_empty() early-return guards in both methods so cleared shmem is treated identically to a missing shmem file. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- appsec/helper-rust/src/rc.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/appsec/helper-rust/src/rc.rs b/appsec/helper-rust/src/rc.rs index 7cd3db0efb..049e45f4a4 100644 --- a/appsec/helper-rust/src/rc.rs +++ b/appsec/helper-rust/src/rc.rs @@ -175,6 +175,10 @@ impl ConfigDirectory { } pub fn runtime_id(&self) -> anyhow::Result<&str> { + if self.data.is_empty() { + // Cleared shmem state (expired()): no config available, same as unwritten shmem. + return Ok(""); + } self.data.iter().position(|&b| b == b'\n').map_or_else( || { Err(anyhow::anyhow!( @@ -194,6 +198,13 @@ impl ConfigDirectory { } pub fn iter(&self) -> anyhow::Result>> + '_> { + if self.data.is_empty() { + // Cleared shmem state (expired()): no config available, same as unwritten shmem. + return Ok(ConfigIter { + data: &self.data[..], + pos: 0, + }); + } self.data.iter().position(|&b| b == b'\n').map_or_else( || { Err(anyhow::anyhow!( @@ -639,6 +650,28 @@ mod tests { Ok(payload_size) } + #[test] + fn config_directory_handles_cleared_shmem() -> anyhow::Result<()> { + // expired() calls writer.write(&[]), which stores size=1 with just the trailing NUL byte. + // This cleared state means "no config available" and must not be treated as an error. + let name = "/helper_rust_cfg_cleared_state"; + shm_create_config_dir_with_raw_payload(name, b"")?; + + let mut poller = ConfigPoller::new(Path::new(OsStr::from_bytes(name.as_bytes()))); + let cfg_dir = poller + .poll()? + .context("expected config snapshot when seq advanced")?; + + assert_eq!(cfg_dir.runtime_id()?, ""); + let configs: Vec<_> = cfg_dir.iter()?.collect::>>()?; + assert!(configs.is_empty()); + + unsafe { + let _ = libc::shm_unlink(CString::new(name.as_bytes()).unwrap().as_ptr()); + } + Ok(()) + } + #[test] fn config_directory_iter_errors_when_payload_has_no_lf() -> anyhow::Result<()> { let outer = "/helper_rust_cfg_no_lf_iter";