1. The assumptions that it makes. Everything in and out are files (phony files notwithstanding). It is hard and painful if you want outputs to rely on and rebuild from configuration in the makefile itself. It's not impossible to implement, but it's difficult to precisely implement it: often times I've seen systems that just rebuild everything after configuration changes.
2. The mix of declarative and imperative styles, while useful for quickly throwing a build together, gets difficult to deal with as things scale up. The make language itself is pretty restricted, too (without using $(eval)).
I know that recent versions support guile extensions and even C(/C++?) extensions, but at that point it's not giving you all that much. I have often wished the make functionality was exposed in some "libmake" for me to extend.
For this reason (and others), I've recently refactored a huge build system to use shake[1] instead. Now builds are precise and correct, and properly depend on configuration and build environment.