diff --git a/include/libcamera/controls.h b/include/libcamera/controls.h
index c970e4b7b..59b666f16 100644
--- a/include/libcamera/controls.h
+++ b/include/libcamera/controls.h
@@ -201,7 +201,8 @@ public:
 	T get() const
 	{
 		assert(type_ == details::control_type<std::remove_cv_t<T>>::value);
-		assert(isArray_);
+		if (type_ != ControlTypeString)
+			assert(isArray_);
 
 		using V = typename T::value_type;
 		const V *value = reinterpret_cast<const V *>(data().data());
diff --git a/src/libcamera/controls.cpp b/src/libcamera/controls.cpp
index 1e1b49e6b..f82c4f777 100644
--- a/src/libcamera/controls.cpp
+++ b/src/libcamera/controls.cpp
@@ -276,8 +276,12 @@ std::string ControlValue::toString() const
 			str += value->toString();
 			break;
 		}
+		case ControlTypeString: {
+			const std::string *value = reinterpret_cast<const std::string *>(data);
+			str += *value;
+			break;
+		}
 		case ControlTypeNone:
-		case ControlTypeString:
 			break;
 		}
 
@@ -353,7 +357,8 @@ bool ControlValue::operator==(const ControlValue &other) const
 void ControlValue::set(ControlType type, bool isArray, const void *data,
 		       std::size_t numElements, std::size_t elementSize)
 {
-	ASSERT(elementSize == ControlValueSize[type]);
+	if (type != ControlTypeString)
+		ASSERT(elementSize == ControlValueSize[type]);
 
 	reserve(type, isArray, numElements);
 
@@ -375,7 +380,7 @@ void ControlValue::set(ControlType type, bool isArray, const void *data,
  */
 void ControlValue::reserve(ControlType type, bool isArray, std::size_t numElements)
 {
-	if (!isArray)
+	if (!isArray && type != ControlTypeString)
 		numElements = 1;
 
 	std::size_t oldSize = numElements_ * ControlValueSize[type_];
diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
index 8c78b8c42..641ea4981 100644
--- a/src/libcamera/v4l2_device.cpp
+++ b/src/libcamera/v4l2_device.cpp
@@ -12,6 +12,7 @@
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
+#include <string_view>
 #include <sys/ioctl.h>
 #include <sys/syscall.h>
 #include <unistd.h>
@@ -25,6 +26,8 @@
 
 #include "libcamera/internal/formats.h"
 #include "libcamera/internal/sysfs.h"
+#include "libcamera/controls.h"
+#include "linux/videodev2.h"
 
 /**
  * \file v4l2_device.h
@@ -230,6 +233,13 @@ ControlList V4L2Device::getControls(Span<const uint32_t> ids)
 				v4l2Ctrl.p_u32 = reinterpret_cast<uint32_t *>(data.data());
 				break;
 
+			case V4L2_CTRL_TYPE_STRING:
+				type = ControlTypeString;
+				value.reserve(type, false, info.maximum + 1);
+				data = value.data();
+				v4l2Ctrl.string = reinterpret_cast<char *>(data.data());
+				break;
+
 			default:
 				LOG(V4L2, Error)
 					<< "Unsupported payload control type "
@@ -570,6 +580,8 @@ ControlType V4L2Device::v4l2CtrlType(uint32_t ctrlType)
 		 * integer type.
 		 */
 		return ControlTypeInteger32;
+	case V4L2_CTRL_TYPE_STRING:
+		return ControlTypeString;
 
 	default:
 		return ControlTypeNone;
@@ -709,6 +721,7 @@ void V4L2Device::listControls()
 		case V4L2_CTRL_TYPE_U8:
 		case V4L2_CTRL_TYPE_U16:
 		case V4L2_CTRL_TYPE_U32:
+		case V4L2_CTRL_TYPE_STRING:
 			break;
 		/* \todo Support other control types. */
 		default:
@@ -808,6 +821,21 @@ void V4L2Device::updateControls(ControlList *ctrls,
 			value.set<int64_t>(v4l2Ctrl.value64);
 			break;
 
+		case ControlTypeString:
+			/*
+			 * The ControlValue contains storage for the  maximum
+			 * length of the string, and its size matches that. After
+			 * the data is retrieved, it must be resized so ControlValue::numElements()
+			 * is correct.
+			*
+			 * VIDIOC_G_EXT_CTRLS does not return the length of the string,
+			 * so we must reassign and let the std::string_view constructor
+			 * calculate the true length. Because value is a copy, there will be
+			 * no use-after-free issues.
+			 */
+			value.set<std::string_view>(v4l2Ctrl.string);
+			break;
+
 		default:
 			/*
 			 * Note: this catches the ControlTypeInteger32 case.
