NEWID() In Joined Virtual Table Causes Unintended Cross Apply Behavior
This behaviour is by design, as explained in detail on this Connect bug report.  The most pertinent Microsoft reply is reproduced below for convenience (and in case the link dies at some point):


  Posted by Microsoft on 7/7/2008 at 9:27 AM
  
  Closing the loop . . . I've discussed this question with the Dev team.
  And eventually we have decided not to change current behavior, for the
  following reasons:
  
  
  The optimizer does not guarantee timing or number of executions of
  scalar functions. This is a long-estabilished tenet. It's the
  fundamental 'leeway' tha allows the optimizer enough freedom to gain
  significant improvements in query-plan execution.
  This "once-per-row behavior" is not a new issue, although it's not
  widely discussed. We started to tweak its behavior back in the Yukon
  release. But it's quite hard to pin down precisely, in all cases,
  exactly what it means! For example, does it a apply to interim rows
  calculated 'on the way' to the final result? - in which case it
  clearly depends on the plan chosen. Or does it apply only to the rows
  that will eventually appear in the completed result? - there's a nasty
  recursion going on here, as I'm sure you'll agree!
  As I mentioned earlier, we default to "optimize performance" -
  which is good for 99% of cases. The 1% of cases where it might change
  results are fairly easy to spot - side-effecting 'functions' such as
  NEWID - and easy to 'fix' (trading perf, as a consequence). This
  default to "optimize performance" again, is long-established, and
  accepted. (Yes, it's not the stance chosen by compilers for
  conventional programming languages, but so be it).
  
  
  So, our recommendations are:
  
  
  Avoid reliance on non-guaranteed timing and number-of-executions
  semantics.
  Avoid using NEWID() deep in table expressions.
  Use
  OPTION to force a particular behavior (trading perf)
  
  
  Hope this explanation helps clarify our reasons for closing this bug
  as "won't fix".


The GETDATE and SYSDATETIME functions are indeed non-deterministic, but they are treated as runtime constants for a particular query.  Broadly, this means the function's value is cached when query execution starts, and the result re-used for all references within the query.

None of the 'workarounds' in the question are safe; there is no guarantee the behaviour will not change the next time the plan is compiled, when you next apply a service pack or cumulative update...or for other reasons.

The only safe solution is to use a temporary object of some kind - a variable, table, or multi-statement function for example. Using a workaround that appears to work today based on observation is a great way to experience unexpected behaviours in future, typically in the form of a paging alert at 3am on Sunday morning.