I think I might have accidentally stumbled upon the source of the bug, though I haven't tested it myself. The bad code is in CvCity:: DoCreatePuppet(). TL;DR: the bug is caused by code that is not only not working as intended, but would (almost) never work as intended.
When a city is puppeted, all tiles within a 2-tile radius are supposed to have Working City Overrides set to the newly puppet city. Working City Overrides happen when a tile can be worked by multiple cities: the city to which the tile is currently allocated is its Working City Override, whereas the city that acquired the tile first is the Working City.
The problem is that the code doesn't do this at all. Instead, it follows the following strange set of instructions: for every tile within a 3-tile radius around the city, it checks whether that looped tile is within 2 range of the tile that is (City's X coordinate, City's Y coordinate) away from the looped tile. If it is, then that second tile's Working City Override is set to the newly puppeted city. This condition rarely ever fires off, and when it does, I think it will usually get cleaned up by one of the many updateWorkingCity() calls that exist in the game. When it doesn't get cleaned up though, you should experience an issue identical to the one people have been posting in screenshots here: the city to which the tile belongs cannot work it because its Working City Override is set to another city, the player cannot unset the Working City Override because the UI doesn't let them do it to city tiles, and the Working City Override city cannot work the tile either because it's outside of the city's work range (since cities have to be at least 3 tiles apart). If updateWorkingCity() is called in an incorrect order or not at all when a city flips from a peace deal, that might explain why people have only seen the bug as a result of getting a city through a peace deal.