ship one record per function: merge the release set, gen reads it, descriptions as doc comments, gates for what was only claimed; v3.0
Some checks failed
CI / fuzz (push) Successful in 2m2s
CI / lint (push) Successful in 15s
CI / test (push) Failing after 18s

This commit is contained in:
Kamal Tufekcic 2026-08-02 22:01:36 +03:00
commit 3410a79b6a
28 changed files with 30596 additions and 955 deletions

View file

@ -60,6 +60,10 @@ pub const CURRENT_LAYOUT: SchemaLayout = SchemaLayout {
ci_base_count: 41,
ci_fields: 48,
ci_bases: 56,
// Every displacement below is added to a FILE-CONTROLLED pointer, so each use wraps rather than
// panicking under the overflow-checked fuzz build. That includes the ones that are 0 today: they are
// layout values, revised when Valve reshapes the struct, and "safe because this constant happens to
// be zero" is a trap that springs on the revision rather than on the code that introduced it.
f_name: 0,
f_offset: 16,
f_stride: 32,
@ -157,7 +161,10 @@ pub fn enumerate_schema(img: &CodeImage) -> Vec<SchemaClass> {
out.push(cls);
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
// By NAME, then by the record's own address. The walk iterates `reloc_slots()`, which is a HashMap,
// so the collection order is hash order; a sort on name alone is stable and therefore leaves ties —
// two libraries registering one class — resolved by that hash order, in a byte-reproducible artifact.
out.sort_by(|a, b| (&a.name, a.class_info).cmp(&(&b.name, b.class_info)));
out
}
@ -185,11 +192,49 @@ const EV_VALUE: u64 = 8;
/// field that isn't a count at all before it drives an allocation.
const EB_MAX_VALUES: u32 = 4096;
/// Enumerate every registered enum in `img`, alongside [`enumerate_schema`]'s classes. Same reloc-driven
/// discovery: an enum binding is found by the slot holding its type-name pointer, then accepted only if
/// the width/count word and the enumerator array both read as what they claim to be — so a layout change
/// yields fewer enums, never wrong ones. Sorted by name.
pub fn enumerate_enums(img: &CodeImage) -> Vec<SchemaEnum> {
/// Enumerate every registered enum in `img`, given the classes [`enumerate_schema`] already recovered.
/// Same reloc-driven discovery: an enum binding is found by the slot holding its type-name pointer, then
/// accepted only if the width/count word and the enumerator array both read as what they claim to be —
/// so a layout change yields fewer enums, never wrong ones. Sorted by name.
///
/// **A class's FIELD descriptor is byte-compatible with an enum binding**, which is why `classes` is a
/// parameter rather than a convenience. `SchemaClassFieldData_t` is `{ name, type, offset, metadataCount,
/// metadata }`: read as an enum binding, the name reads as a type name, the low bytes of the offset read
/// as a plausible size/alignment, the metadata count reads as an enumerator count, and the metadata array
/// — `{ name, data }` pairs — reads as enumerators. Every field carrying exactly one metadata tag at a
/// field offset whose low two bytes are both powers of two therefore fits, and the result would be an
/// enum that does not exist, named after a member, whose one "value" is the ADDRESS of a documentation
/// string and therefore differs between runs of the same build.
///
/// Two independent structural facts reject them, and both are needed — measured over 1,016 CS2 and 1,490
/// Dota candidates, they catch 40 apiece with zero real enums lost, and neither catches all 40 alone:
///
/// 1. **The record sits inside a class's field array**, at a `F_STRIDE` boundary. That is not a heuristic
/// — the SchemaSystem states that this address is that class's Nth field.
/// 2. **An enumerator's value is a relocation.** An enum value is a compile-time literal, so it is never
/// relocated; a metadata entry's second word is a pointer, so it always is. This is what catches a
/// field whose owning class the class walk itself rejected, leaving no array to fall inside.
pub fn enumerate_enums(img: &CodeImage, classes: &[SchemaClass]) -> Vec<SchemaEnum> {
// The address ranges class field descriptors occupy, sorted so membership is a binary search.
let mut spans: Vec<(u64, u64)> = classes
.iter()
.filter_map(|c| {
let fp = img.read_ptr(c.class_info.wrapping_add(CI_FIELDS))?;
(fp != 0).then(|| (fp, fp.wrapping_add(F_STRIDE * c.fields.len() as u64)))
})
.collect();
spans.sort_unstable();
// Each class owns its own array, so the ranges are disjoint and the last one starting at or before
// `a` is the only one that can contain it. If that ever stopped holding, the miss would be a fake
// enum surviving rather than a real one dropped — the same direction every other guard here errs in.
let in_field_array = |a: u64| {
let i = spans.partition_point(|&(s, _)| s <= a);
i > 0 && {
let (s, e) = spans[i - 1];
a < e && (a - s).is_multiple_of(F_STRIDE)
}
};
let mut out = Vec::new();
for (slot, val) in img.reloc_slots() {
let Some(name) = img.read_c_string(val) else {
@ -201,6 +246,10 @@ pub fn enumerate_enums(img: &CodeImage) -> Vec<SchemaEnum> {
continue;
}
let base = slot.wrapping_sub(EB_TYPE_NAME);
// Clause 1: the SchemaSystem states this address is a class's field descriptor, so it is one.
if in_field_array(base) {
continue;
}
let Some(w) = img.read_ptr(base.wrapping_add(EB_WIDTH)) else {
continue;
};
@ -228,7 +277,10 @@ pub fn enumerate_enums(img: &CodeImage) -> Vec<SchemaEnum> {
) else {
break;
};
if n.is_empty() {
// Clause 2: a relocated "value" is a pointer, so these are `{ name, data }` metadata
// entries and not enumerators. Rejects the whole record — one pointer among the values
// means the array is the wrong kind, not that one enumerator is odd.
if n.is_empty() || img.is_reloc_slot(rec.wrapping_add(EV_VALUE)) {
break;
}
values.push((n, v));
@ -242,7 +294,13 @@ pub fn enumerate_enums(img: &CodeImage) -> Vec<SchemaEnum> {
});
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
// Same reason as `enumerate_schema`, and it matters MORE here because the `dedup_by` below then keeps
// whichever row sorted first: with a name-only sort that survivor was picked by hash order. Enums have
// no record address on the struct, so the tiebreak is the content that distinguishes two accounts of
// one name — width, alignment, then the enumerator list.
out.sort_by(|a, b| {
(&a.name, a.size, a.align, &a.values).cmp(&(&b.name, b.size, b.align, &b.values))
});
out.dedup_by(|a, b| a.name == b.name); // one binding per name; libs re-register shared enums
out
}
@ -272,10 +330,13 @@ fn parse_class(img: &CodeImage, base: u64, name: &str, name_ptr: u64) -> Option<
let mut fields = Vec::with_capacity(field_count as usize);
for i in 0..field_count as u64 {
let fe = fields_ptr.wrapping_add(i.wrapping_mul(F_STRIDE));
let Some(fname) = img.read_ptr(fe + F_NAME).and_then(|p| img.read_c_string(p)) else {
let Some(fname) = img
.read_ptr(fe.wrapping_add(F_NAME))
.and_then(|p| img.read_c_string(p))
else {
break;
};
let offset = img.read_i32(fe + F_OFFSET).unwrap_or(0);
let offset = img.read_i32(fe.wrapping_add(F_OFFSET)).unwrap_or(0);
fields.push(SchemaField {
name: fname,
offset,
@ -288,13 +349,13 @@ fn parse_class(img: &CodeImage, base: u64, name: &str, name_ptr: u64) -> Option<
if bases_ptr != 0 {
for i in 0..base_count as u64 {
let be = bases_ptr.wrapping_add(i.wrapping_mul(B_STRIDE));
let offset = img.read_u32(be + B_OFFSET).unwrap_or(0);
let bcls = img.read_ptr(be + B_CLASS).unwrap_or(0);
let offset = img.read_u32(be.wrapping_add(B_OFFSET)).unwrap_or(0);
let bcls = img.read_ptr(be.wrapping_add(B_CLASS)).unwrap_or(0);
if bcls == 0 {
continue;
}
if let Some(bn) = img
.read_ptr(bcls + CI_NAME)
.read_ptr(bcls.wrapping_add(CI_NAME))
.and_then(|p| img.read_c_string(p))
{
bases.push(SchemaBase { name: bn, offset });
@ -316,7 +377,7 @@ fn parse_class(img: &CodeImage, base: u64, name: &str, name_ptr: u64) -> Option<
// The offline reader above recovers class layouts (names + field offsets) from the static reflection
// tables. Field *types* are runtime-resolved (each record's `m_pType` is a null pointer on disk), so
// `live_schema` attaches to a running process, reads the types back, and builds the typed
// `netvars-<game>.json` (`model::Schema`) directly — no `sdk.json` intermediate.
// the artifact's `schema` section (`model::Schema`) directly — no `sdk.json` intermediate.
/// FNV-1a (32-bit). The Source-2 schema field/class name hash: a field's runtime lookup key is
/// `(fnv1a32(class_name) << 32) | fnv1a32(field_name)` (field name keeps its `m_` prefix). Confirmed
@ -344,7 +405,7 @@ fn builtin_size(t: &str) -> i32 {
/// Walk a running process's schema across every server-mapped library (`profile.libs`) and build the typed
/// netvars (`model::Schema`) DIRECTLY — no `sdk.json` round-trip: field layout is read offline from each
/// image, the runtime `m_pType` from the live process. Shared classes (compiled into many libs) de-dupe
/// precedence-first (the earlier lib in `libs` wins). This is `netvars-<game>.json` — the shipped SDK
/// precedence-first (the earlier lib in `libs` wins). This is the artifact's `schema` section — the shipped SDK
/// material (`source2rosetta-gen` renders it on demand).
pub(crate) fn live_schema(
prof: &GameProfile,
@ -370,9 +431,13 @@ pub(crate) fn live_schema(
};
let Some(base) = live.base(lib) else { continue }; // lib not mapped in the process -> skip
nlibs += 1;
// ONE class walk per library, shared by both consumers below — the enum walk needs it to tell a
// field descriptor from an enum binding, and walking the reflection tables twice per image is
// what a large game's memory ceiling notices first.
let schema_classes = enumerate_schema(&img);
// Enum bindings are static, so they come from the IMAGE — no process read, unlike field types.
// First library wins, matching the class precedence: a shared enum has one definition.
for e in enumerate_enums(&img) {
for e in enumerate_enums(&img, &schema_classes) {
enums.entry(e.name).or_insert_with(|| model::EnumDef {
size: e.size,
values: e
@ -382,7 +447,7 @@ pub(crate) fn live_schema(
.collect(),
});
}
for c in &enumerate_schema(&img) {
for c in &schema_classes {
// a shared class already taken from an earlier (higher-precedence) lib — identical layout, skip
if !seen.insert(c.name.clone()) {
continue;