Does a R=301 act like an [END] directive in this situation somehow?
No END is not in effect but L (last) is. Please understand END behaves differently from L.
Hence Apache sends a 301 to browser and browser does a full redirect, then it sends /foo2 again to web server and then 2nd and 3rd rules are executed.
Update:
Thanks to comment from @MrWhite below, I have run this test while keeping rewrite logs in trace4.
Config to enable rewrite logging:
LogLevel info rewrite:trace4
And here are additional logs that came in my Apache error logs. Please note that my local host name is localhost and my DocumentRoot is /www/root.
Step: Applying rule #1, URI is /foo3:
strip per-dir prefix: /www/root/foo3 -> foo3
applying pattern '^foo3$' to uri 'foo3'
rewrite 'foo3' -> 'foo2'
add per-dir prefix: foo2 -> /www/root/foo2
explicitly forcing redirect with http://localhost/www/root/foo2
Step: Applying rule #2, URI is http://localhost/www/root/foo2 (Note that your RewriteBase / hasn't come into effect yet):
applying pattern '^foo2$' to uri 'http://localhost/www/root/foo2'
Step: Applying rule #3, URI is http://localhost/www/root/foo2:
applying pattern '^foo1$' to uri 'http://localhost/www/root/foo2'
Step: Applying external redirect 301 by stripping DocumentRoot with your RewriteBase value:
trying to replace prefix /www/root/ with /
add subst prefix: foo2 -> foo2
escaping http://localhost/foo2 for redirect
redirect to http://localhost/foo2 [REDIRECT/301]
Step: Applying rule #1, URI is /foo2:
strip per-dir prefix: /www/root/foo2 -> foo2
applying pattern '^foo3$' to uri 'foo2'
Step: Applying rule #2, URI is /foo2:
strip per-dir prefix: /www/root/foo2 -> foo2
applying pattern '^foo2$' to uri 'foo2'
rewrite 'foo2' -> 'foo1'
Step: Applying rule #3, URI is /foo1:
add per-dir prefix: foo1 -> /www/root/foo1
strip per-dir prefix: /www/root/foo1 -> foo1
applying pattern '^foo1$' to uri 'foo1'
rewrite 'foo1' -> 'index.php'
Step: Internal rewrite to /index.php:
add per-dir prefix: index.php -> /www/root/index.php
trying to replace prefix /www/root/ with /
add subst prefix: index.php -> /index.php
internal redirect with /index.php [INTERNAL REDIRECT]
Step: Applying rule #1, URI is /index.php:
strip per-dir prefix: /www/root/index.php -> index.php
applying pattern '^foo3$' to uri 'index.php'
Step: Applying rule #2, URI is /index.php:
strip per-dir prefix: /www/root/index.php -> index.php
applying pattern '^foo2$' to uri 'index.php'
Step: Applying rule #3, URI is /index.php:
strip per-dir prefix: /www/root/index.php -> index.php
applying pattern '^foo1$' to uri 'index.php'
Step: Stop rule processing, URI is /index.php:
pass through /www/root/index.php
I have added detailed trace logging and my step comment above to show how L is a side effect where rule processing is not completely stopped. However the fact mod_rewrite engines attempts to apply remaining rules on a URI that is invalid i.e. http://localhost/www/root/foo2 those rules will not execute and full redirect will be the outcome.