state: preserve private reflected field access during migration - #28
MeteorsLiu wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Review: serialize reflect.Value read-only (private) fields
This extends reflect.Value serialization to preserve private-field access restrictions by capturing the reflect-internal flag read-only bits (flagStickyRO bit 5, flagEmbedRO bit 6) into a new ReadOnly uint64, clearing them on encode so the graph can be traversed, and restoring them on decode.
Correctness looks solid. The mask 1<<5 | 1<<6 is exactly flagRO and excludes the dangerous adjacent bits (flagIndir, flagAddr, flagMethod, flagKindMask). Untrusted input is bounded: reflectedValue.load rejects any bits outside the mask (object.go) before they reach the unsafe SetUint in decodeObject, and decode only adds read-only restrictions — it cannot escalate access. Test coverage is good, including the negative flag-validation test and end-to-end round-trips through a child process.
No blocking issues. The inline notes below are maintainability/consistency nits — take or leave them.
Summary of suggestions
- Extract the thrice-repeated
FieldByName("flag")+SetUintidiom into one helper, and prefer named bit constants over bare1<<5/1<<6/1<<8. This confines the reflect-internals dependency to a single place (andField(2)avoidsFieldByName's string scan, though that's negligible here). - A one-line comment on the
x.ReadOnly != 0guard and on theReadOnlyfield would help future readers.
|
|
||
| // Go 1.26 reflect/value.go: flagStickyRO and flagEmbedRO have distinct Field | ||
| // propagation rules. Keep both bits instead of collapsing them to CanInterface. | ||
| const reflectValueReadOnlyMask = uint64(1<<5 | 1<<6) |
There was a problem hiding this comment.
The two read-only bits are documented as "distinct" but the mask uses bare magic numbers. Consider named constants composed into the mask, e.g.:
const (
flagStickyRO = uint64(1 << 5)
flagEmbedRO = uint64(1 << 6)
)
const reflectValueReadOnlyMask = flagStickyRO | flagEmbedROThis makes the "keep both bits" intent self-evident and pairs naturally with the bare 1 << 8 (flagAddr) already used in reflectValueUnaddressable. Nit — the whole change rests on undocumented reflect internals, so naming the bits reduces the risk of a future "simplification".
| Failf("reflect.Value has restricted access") | ||
| // Clear access restrictions on this Value copy only. The restored | ||
| // view must retain them even though state can traverse private data. | ||
| flag := reflect.ValueOf(&value).Elem().FieldByName("flag") |
There was a problem hiding this comment.
This reflect.ValueOf(&value).Elem().FieldByName("flag") + reflectValueRWAddr(...).SetUint(...) idiom now appears here, in decodeObject (decode.go), and in reflectValueUnaddressable (decode_unsafe.go). Consider a single helper (e.g. setValueFlagBits(v, set, clear uint64) uint64 returning the prior masked bits) so the reflect-internals dependency — field name, uintptr width, addressability — lives in exactly one place. If the runtime's flag field ever changes, there'd be one site to update instead of three. Nit.
| } | ||
| if _, invalid := x.Type.(nilType); invalid { | ||
| if _, empty := x.Value.(nilValue); !empty || x.Addressable { | ||
| if _, empty := x.Value.(nilValue); !empty || x.Addressable || x.ReadOnly != 0 { |
There was a problem hiding this comment.
The added || x.ReadOnly != 0 clause is correct — a zero reflect.Value can't carry read-only bits, so a nonzero ReadOnly here signals a corrupt/malicious stream — but bundled with x.Addressable it reads as unexplained. A short inline comment noting why would help. Nit.
| "unsafe" | ||
| ) | ||
|
|
||
| // Go 1.26 reflect/value.go: flagStickyRO and flagEmbedRO have distinct Field |
There was a problem hiding this comment.
Minor consistency: the rest of the codebase pins ABI-dependent comments to the exact toolchain ("Go 1.26.6", matching go.mod and state.go). This new comment says "Go 1.26". The referenced bit values are stable across releases so it's not wrong, just less precise than the established convention. Nit.
No description provided.