In the code you have written the task object will be destroyed (with a call to the destructor) at the end of pass_back_ownership.
That is completely fine and no UB in your code. However, main must be careful not to dereference ptr after the call to pass_back_ownership.
Also be careful that there are potential order-of-evaluation issues if pass_back_ownership were to take the parameter by-value instead of by-reference. That would make the code a bit simpler, since you wouldn't need to crate the local tmp variable, but only since C++17 is it guaranteed that ptr->pass_back_ownership will be evaluated before the function parameter is constructed. If the order was the other way around, then you would have UB for dereferencing an empty ptr.
Regarding best practice:
What your call ptr->pass_back_ownership(std::move(ptr)); is saying is that main relinquishes ownership of the task object referenced by ptr to itself. The lifetime of the task object is not intended to be bound to the scope of main anymore.
The question would then be what possible lifetimes the task object could then have. There are basically only two choices: 1. its lifetime ends at latest at the end of the function call, which is your case, or 2. pass_back_ownership stores the passed unique_ptr somewhere outside the function to extend the lifetime.
If you are interested only in 1., then I would question why it is necessary to pass the std::unique_ptr to some random function to achieve the effect. If you intent ptr->pass_back_ownership(std::move(ptr)); to definitively destroy the task object, then why aren't the operations that pass_back_ownership does simply done in the destructor of task? Then you could simply let ptr in main run out-of-scope to have the same effect.
It makes a bit more sense if pass_back_ownership only conditionally actually takes ownership by moving the unique_ptr reference. Then main could test with if(ptr) after the call whether or not the task object was destroyed, but then why is it important for the task object to be destroyed prematurely instead of simply waiting for ptr in main to go out-of-scope? That no instructions are left can also be communicated by either having the function return a bool state or have the task object have some way of checking presence of instructions (e.g. an operator bool).