Large negative R2 or accuracy scores for random forest with GridSearchCV but not train_test_split
R2 can be negative if the model is arbitrarily worse according to the sklearn documentation
So the very negative train scores were indicative of an extremely bad performance.
Why was the test performance so colossally bad in GridSearchCV when it was decent in a simple test/train split?
The main problem is that train_test_split chooses observations randomly while GridSearchCV does not!
My problem was that the dataframe was sorted by the target variable!
The GridSearchCV and cross_val_score do not make random folds.  They literally take the first 20% of observations in the dataframe as fold 1, the next 20% as fold 2, etc.
Let's say my target is a range between 1-50.  If I sort my dataframe by target, then all observations are in order from 1 to 50.  The first fold of the cross-validation will take (for example) only observations with a target between 1-10, save this for the test, then train the model only on targets of 20-50.  This is why it was performing so badly!  The model was trained on a certain range, the test set only included a target range the model had never seen before!
The solution is simple. Shuffle the original dataframe before splitting into X, y for cross-validation.
df = df.sample(frac=1, random_state=0)

This solved my problem, now the test and train scores from GridSearchCV are both between 0-1, comparable to a simple train_test_split.
Lesson learned: Always shuffle a dataframe before a cross-validation - otherwise the folds will be subject to any biases in the order of how data was collected.