I don't think it's particularly important that they match to that degree. First, users generally don't see the trace logs. Second, it's good enough that they match when the product property is non-null. There's no strong reason to make them match for the null case. It's fine to have this function return something more useful than that, especially since it's currently used as a sort key and "(null)" is useless for that.
I have no objection if you want to make debugstr_device() also include the vendor/product ID as a fallback, but I don't think it's especially important. The result from copy_device_name() is not currently logged, so there's no way for a reader of the logs to benefit if the two functions match in that way, nor lose anything if they don't match.
I don't know how important it is for get_osx_device_name() to return a non-empty name. It could be changed to call copy_device_name() to get the name and then extract that to the buffer. Mind you, it's currently buggy in how it treats the CFString length vs. the buffer length. The former is in number of UTF-16 code units, the latter in terms of ASCII characters, so they're not directly comparable. (Also, nothing ever checks the function's return value, so returning the supposed required length doesn't achieve anything.)
Testing isn't appropriate. It could work in some versions of the frameworks and not in others. The authority on the design contract is the documentation (including header comments), with the understanding that if it doesn't explicitly say that NULL is acceptable, then it's not.
Returning the empty string is probably simplest. If you go the pointer testing route, then surely (!name1 && !name2) implies equal, not less-than.