Template-related compiler bug or... ?
The answer to that question is basically always the OR option.
Compiler bugs do happen. Unless you're really pushing the compiler with cutting-edge Boost-developer style magic, though, you're not hitting a bug. You're just doing it wrong. :)
The solution for your case would be to change up the template definition to not rely on type deduction. You're making assumptions based on the rules for overloading, but overloading isn't want has failed. Type deduction is what has failed. The operator*= is being excluded because of substitution failure.
Basically, the problem is that the compiler has to guess what T is for your operator*=. The parameters it has to match against are Foo<T> and T, and you've passed it a Foo<float> and an int. T cannot be both a float and an int. Substitution of types for T has failed and the template is not added to the overload set. There is then an empty overload set. Promotion only comes into play when _converting_ values, not when substituting types, but since there's an empty overload set there's nothing to promote anyway.
One option would be to put the operator*= inline in the defintion of Foo. That way, T no longer needs to be deduced because it is explicit in the definition of Foo<float>, the operator*= of type T is found and added to the overload set, and the int is then promoted to float during overload resolution.
A second option, if the definition must be external, is to allow more flexibility for substitution. There are several ways to do this, but I think the "simplest" one may be this:
template <typename T, typename U, typename = decltype(std::declval<T>() *= std::declval<U>())>
Foo<T>& operator*=(Foo<T>& a, U const& b)
{
l.val *= b;
return l;
}
The third typename parameter will trigger substitution failure when a value of T (such as l.val) does not support operator*= with a value of U (such as the b parameter). Concepts would make this code a lot cleaner, of course. There's a few other ways to write that function, but that's probably the least crazy. The cleanest option in general for this sort of problem relies on treiling return types or C++17 void_t, but trailing return types doesn't work in this specific circumstance and you odn't have void_t yet. :)