Actually the CLR doesn't make any assumptions like that.
Actually it does.
The registers are stored onto the stack
Actually no. While the kernel may indeed store the registers on the stack when the thread is suspended the only way for an application to get those registers is to call GetThreadContext. Sure, the CONTEXT that you pass to that function can be stored on stack but that's irrelevant here.
the stack is then inspected for any value that lies within the bounds of the current heap. Any value that falls within an object is marked as live for this round. The root-set computed is therefore a super-set of the "true" root-set, since an integer which happens to hold a value that would also be a valid pointer into the heap might cause a value to live longer than it strictly should
Nope, this is not what MS's CLR does. What you're describing here sounds like coservative GC, not compacting GC.
but this doesn't matter since this is much faster than using type information to derive the true root set.
Actually it matters a lot because it prevents objects from being moved which was one the main questions in this thread. You can't move objects unless you know exactly where pointers are. Or are you suggesting to modify random values in memory that happen to look like pointers?