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