Before C++20, you would need to substitute Foo* foo = reinterpret_cast<Foo*>(malloc(sz + sizeof(Foo))); for Foo* foo = ::new(malloc(sz + sizeof(Foo)) Foo;. In C++20 and newer your class is an implicit-lifetime class, and as such it doesn't need to be created manually in malloced memory. I wouldn't rely on this C++20 feature, as it's rather obscure.
Don't forget to call the destructor before freeing the memory. Read about std::launder and consider if you need to use it or not.
This is technically still UB, but only because the standard is defective in its description of reinterpret_cast. No compiler is going to enforce this.
Also the pointer reachibility rules for std::launder somewhat imply that it could be UB, but it's not stated explicitly, and again, no contemporary compiler enforces this.
Consider overloading operator new for your class to have a semblance of a good class allocation interface:
#include <new>
#include <iostream>
struct Foo
{
    size_t len = 0;
    size_t cap = 0;
    char *buf()
    {
        return reinterpret_cast<char*>(this + 1);
    }
    void *operator new(std::size_t size, std::size_t cap)
    {
        return ::operator new(size + cap);
    }
    // Only if you need custom cleanup in `delete`:
    // void operator delete(Foo *ptr, std::destroying_delete_t)
    // {
    //     // Destroy array elements here.
    //     ptr->~Foo();
    //     ::operator delete(ptr);
    // }
};
Foo *new_foo(size_t sz)
{
    Foo* foo = new(sz) Foo;
    foo->len = 0;
    foo->cap = sz;
    return foo;
}
Now new Foo causes a compilation error, while new(cap) Foo works and forces you to allocate enough memory for the element. Though ::new Foo still works, there is no way to disable it.
If you uncomment the destroying operator delete, you can hook into delete foo to insert the cleanup for the elements.