Yes, but there's a legal way to do this.
reference the other two areas with offsets relative to the first area
If you cast the pointers to uintptr_t, and perform your arithmetic on uintptr_t and cast your final pointer back (void * ) before using it, what you've done is perfectly legal and safe (albeit weird) since uintptr_t is an unsigned integer type with all the flexibility of unsigned integers, and if x = (uintptr_t)p then it's guaranteed that p == (void * )x.
In other words:
char * x = malloc(100);
char * y = malloc(100);
ptrdiff_t yminusx = y - x;
x[yminusx] = '\0'
is undefined behaviour, but char * x = malloc(100);
char * y = malloc(100);
uintptr_t yminusx = (uintptr_t)(y) - (uintptr_t)(x);
*(char *)(void *)((uintptr_t)(x) + yminusx) = '\0'
is valid C (and has the same meaning on a DWIM compiler).Quite correct, subject to the requirement that converting the same uintptr_t value back to (void ) will return the original pointer. So the conversion could be implemented as a hash table if the compiler author wanted.
But if you look carefully at what I wrote, I'm never converting a value which was not previously returned by a (void )->(uintptr_t) conversion.
Some of the things that break GC aren't even technically valid C code. For example, say I allocate three memory areas with malloc() then hold on only to the area with the lowest address, and reference the other two areas with offsets relative to the first area. This is illegal in C, though it will work on most existing implementations, because it's illegal to dereference a pointer past the end of an allocation.
Specific GC algorithms might not be able to handle say pointers into the interiors of objects, but that's not a general limitation for GC.