Intuition
Mutating as you scan would corrupt later checks, so first record which rows and columns must be zeroed, then apply the changes in a second pass.
Approach
Zeroing as you scan corrupts the scan
The tempting move — when you find a 0, immediately zero out its whole row and column — is a trap. Those freshly written zeros are indistinguishable from original zeros, so the rest of the scan mistakes them for triggers and cascades, zeroing far more than it should. The fix is to never let detection and mutation happen at the same time.
Separate detection from mutation
Make a first pass that only records which rows and which columns contain at least one original zero, storing their indices in two sets. This pass changes nothing, so there's no cascade — every zero it sees is a real one from the input.
Apply the flags in a second pass
Now sweep the grid again and set a cell to 0 if its row index or its column index was flagged. Because the flags were all computed from the original matrix, the result is exactly right regardless of overlap (set membership is idempotent, so a cell at a flagged row and column is just zeroed once). Two passes: O(m·n) time, O(m + n) space for the two sets.
Solution & live demo
Edge cases
Both sets stay empty, so the second pass changes nothing.
Every row and column is flagged; the whole matrix stays zero after the second pass.
Set membership is idempotent, so overlapping flags simply zero the cell once.