When the child process execs, all its current pages are replaced with a brand new set of pages corresponding to the new executable image (plus heap, stack, etc.).
Modern OSes implement CoW is by maintaining a reference count for the physical pages shared between parent and child processes. If a page is shared between parent and child, the reference count will be 2. Once the child process goes through exec, the reference count for the shared pages is decremented (e.g., it's back to 1) thus any write operation by the parent process will succeed without CoW.
For your amusement, create a simple program that does fork followed by the child process sleeping for a few seconds and then doing and exec. Now observe the contents of
/proc/PID/smaps of both processes before fork (only parent of course), after fork but before exec, and after exec. Pay attention to the Shared_XXX pages and the corresponding address ranges.
In terms of code, there are a few simple XV6 extensions to support copy-on-write. A simple google search might be enough. Another place to look at might be https://github.com/torvalds/linux/blob/master/kernel/fork.c. Start tracing it from the fork entry and have fun.
Fork is rather simple, once you get the hang of it, but the memory
management can be a bitch. See 'mm/memory.c': 'copy_page_range()'