Ok, I figured out how to use updateData properly. Leaving this here for posterity. For my use case, I needed to upload a grayscale 3D texture but this should be the same for any texture type:
using namespace Qt3DRender;
QTexture * tex = new QTexture3D;
tex->setSize(vol.width(), vol.height(), vol.depth());
tex->setFormat(QTexture3D::TextureFormat::R8_UNorm);
tex->setLayers(1);
tex->setGenerateMipMaps(false);
QTextureWrapMode wm(QTextureWrapMode::ClampToBorder);
tex->setWrapMode(wm);
data->setFormat(QOpenGLTexture::TextureFormat::R8_UNorm);
data->setWidth(vol.width());
data->setHeight(vol.height());
data->setDepth(vol.depth());
data->setMipLevels(1);
data->setLayers(1);
data->setPixelFormat(QOpenGLTexture::PixelFormat::Red);
data->setPixelType(QOpenGLTexture::PixelType::UInt8);
QByteArray qba;
qba.setRawData((const char *)vol.dataUint8(), static_cast<int>(vol.byteSize()));
data->setData(qba, 1);
QTextureDataUpdate update;
update.setX(0);
update.setY(0);
update.setData(data);
tex->updateData(update);
Qt3DRender::QParameter p();
p.setName("volume"); // the uniform name in the shader
p.setValue(QVariant::fromValue(tex));
QMaterial * mat = ...; // queried from the qml
mat->addParameter(p);
The Qt3D test for texture update was pretty helpful in figuring this out.