Skip to content

Commit dbcf4b2

Browse files
: attrs: error informatively on deserializing an unknown key (#1721)
Summary: previously if a parent proc sent attrs the child binary didn’t know about, deserialization would fail deep in bincode with “Bincode does not support Deserializer::deserialize_ignored_any”, which then surfaced as an undeliverable `ProcSpec`. the root cause is that bincode can't safely skip an unknown value without knowing its type/length, and we were trying to treat it like JSON and ignore it. this change makes that failure explicit: we now error as soon as we see an unknown key, and the message names both the unknown key and the binary that rejected it. the new test exercises exactly that path by injecting an unregistered key, round-tripping through bincode, and asserting we get the structured error. this gives us a clear, actionable failure mode and sets us up to filter unsupported attrs before send. Differential Revision: D85913786
1 parent b02b329 commit dbcf4b2

File tree

1 file changed

+79
-2
lines changed

1 file changed

+79
-2
lines changed

hyperactor/src/attrs.rs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,11 +627,40 @@ impl<'de> Visitor<'de> for AttrsVisitor {
627627
});
628628
let keys_by_name = &*KEYS_BY_NAME;
629629

630+
let exe_name = std::env::current_exe()
631+
.ok()
632+
.and_then(|p| p.file_name().map(|os| os.to_string_lossy().to_string()))
633+
.unwrap_or_else(|| "<unknown-exe>".to_string());
634+
630635
let mut attrs = Attrs::new();
631636
while let Some(key_name) = access.next_key::<String>()? {
632637
let Some(&key) = keys_by_name.get(key_name.as_str()) else {
633-
// Silently ignore unknown keys
634-
access.next_value::<serde::de::IgnoredAny>()?;
638+
// We hit an attribute key that this binary doesn't
639+
// know about.
640+
//
641+
// In JSON we'd just deserialize that value into
642+
// `IgnoredAny` and move on. With bincode we *can't*
643+
// do that safely:
644+
//
645+
// - We don't know this key's value type.
646+
// - That means we don't know how many bytes to
647+
// consume for the value.
648+
// - If we guess wrong or try `IgnoredAny`, bincode
649+
// would need `Deserializer::deserialize_any()` to
650+
// skip it, but bincode refuses because it can't
651+
// know how many bytes to advance.
652+
//
653+
// Result: we cannot safely "skip" the unknown value
654+
// without risking desync of the remaining stream. So
655+
// we abort here and surface which key caused it, and
656+
// the caller must strip it before sending.
657+
access.next_value::<serde::de::IgnoredAny>().map_err(|_| {
658+
serde::de::Error::custom(format!(
659+
"unknown attr key '{}' on binary '{}'; \
660+
this binary doesn't know this key and cannot skip its value safely under bincode",
661+
key_name, exe_name,
662+
))
663+
})?;
635664
continue;
636665
};
637666

@@ -1190,4 +1219,52 @@ mod tests {
11901219
attrs.set(CRATE_LOCAL_ATTR, "test".to_string());
11911220
assert_eq!(attrs[CRATE_LOCAL_ATTR], "test".to_string());
11921221
}
1222+
1223+
#[test]
1224+
fn attrs_deserialize_unknown_key_is_error() {
1225+
// Build a real Attrs, but inject a key that this binary does
1226+
// NOT know about. We do that with
1227+
// insert_value_by_name_unchecked(), which bypasses the
1228+
// declare_attrs!/inventory registration path.
1229+
//
1230+
// Then:
1231+
// 1. Serialize that Attrs with bincode (the real wire
1232+
// format).
1233+
// 2. Attempt to bincode-deserialize those bytes back into
1234+
// Attrs.
1235+
//
1236+
// During deserialization, AttrsVisitor::visit_map() will:
1237+
// - read the key string
1238+
// - fail to find it in KEYS_BY_NAME (the compiled-in
1239+
// registry)
1240+
// - immediately error instead of trying to skip the value,
1241+
// because with bincode we can't safely consume an unknown
1242+
// typed value without risking stream desync.
1243+
//
1244+
// This reproduces exactly what happens when a parent proc
1245+
// sends an Attrs containing a key the child binary wasn't
1246+
// built with.
1247+
1248+
// Definitely not declared in this crate's inventory:
1249+
let bad_key: &'static str = "monarch_hyperactor::pytokio::unawaited_pytokio_traceback";
1250+
1251+
// Make an Attrs that pretends to have that key. u32 already
1252+
// implements AttrValue -> SerializableValue, so we can just
1253+
// box a 0u32.
1254+
let mut attrs = Attrs::new();
1255+
attrs.insert_value_by_name_unchecked(bad_key, Box::new(0u32));
1256+
1257+
// Serialize this Attrs using bincode (same codec we use on
1258+
// the wire).
1259+
let wire_bytes = bincode::serialize(&attrs).unwrap();
1260+
1261+
// Now try to decode those bytes back into Attrs. This should
1262+
// hit the unknown-key branch and return Err.
1263+
let err = bincode::deserialize::<Attrs>(&wire_bytes)
1264+
.expect_err("should error on unknown attr key");
1265+
1266+
let msg = format!("{err}");
1267+
assert!(msg.contains("unknown attr key"), "got: {msg}");
1268+
assert!(msg.contains(bad_key), "got: {msg}");
1269+
}
11931270
}

0 commit comments

Comments
 (0)