diff --git a/Documentation/ABI/testing/configfs-usb-gadget-uvc b/Documentation/ABI/testing/configfs-usb-gadget-uvc
index 045c57e7e245..5faa049ed759 100644
--- a/Documentation/ABI/testing/configfs-usb-gadget-uvc
+++ b/Documentation/ABI/testing/configfs-usb-gadget-uvc
@@ -10,6 +10,26 @@ Description:	UVC function directory
 		function_name		string [32]
 		===================	=============================
 
+What:		/config/usb-gadget/gadget/functions/uvc.name/strings
+Date:		Nov 2022
+KernelVersion:	6.1
+Description:	String descriptors
+
+What:		/config/usb-gadget/gadget/functions/uvc.name/strings/langid
+Date:		Nov 2022
+KernelVersion:	6.1
+Description:	String descriptors for langid (e.g. 0x409)
+
+What:		/config/usb-gadget/gadget/functions/uvc.name/strings/langid/name
+Date:		Nov 2022
+KernelVersion:	6.1
+Description:	String descriptor
+
+		===================	=============================
+		id			id of the string descriptor
+		s			126 character string
+		===================	=============================
+
 What:		/config/usb-gadget/gadget/functions/uvc.name/control
 Date:		Dec 2014
 KernelVersion:	4.0
diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index e0a308f1355c..1b8871a24be8 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -912,6 +912,7 @@ static struct usb_function_instance *uvc_alloc_inst(void)
 		(const struct uvc_descriptor_header * const *)ctl_cls;
 
 	INIT_LIST_HEAD(&opts->extension_units);
+	INIT_LIST_HEAD(&opts->languages);
 
 	opts->streaming_interval = 1;
 	opts->streaming_maxpacket = 1024;
diff --git a/drivers/usb/gadget/function/u_uvc.h b/drivers/usb/gadget/function/u_uvc.h
index 5119cfe5ee4e..c1c9ea5931d3 100644
--- a/drivers/usb/gadget/function/u_uvc.h
+++ b/drivers/usb/gadget/function/u_uvc.h
@@ -81,6 +81,13 @@ struct f_uvc_opts {
 	struct uvc_descriptor_header			**uvc_hs_streaming_cls;
 	struct uvc_descriptor_header			**uvc_ss_streaming_cls;
 
+	/*
+	 * A list of languages, associated with which may be string descriptors
+	 * for various parts of the gadget, including the IAD and XUs.
+	 */
+	struct list_head				languages;
+	unsigned int					nlangs;
+
 	/*
 	 * Read/write access to configfs attributes is handled by configfs.
 	 *
diff --git a/drivers/usb/gadget/function/uvc_configfs.c b/drivers/usb/gadget/function/uvc_configfs.c
index 0a69eb6cf221..da2f70036993 100644
--- a/drivers/usb/gadget/function/uvc_configfs.c
+++ b/drivers/usb/gadget/function/uvc_configfs.c
@@ -13,6 +13,8 @@
 #include "uvc_configfs.h"
 
 #include <linux/sort.h>
+#include <linux/usb/gadget.h>
+
 
 /* -----------------------------------------------------------------------------
  * Global Utility Structures and Macros
@@ -2824,6 +2826,305 @@ static const struct uvcg_config_group_type uvcg_streaming_grp_type = {
 	},
 };
 
+/* -----------------------------------------------------------------------------
+ * strings/<langid>
+ */
+
+static ssize_t uvcg_string_id_show(struct config_item *item, char *page)
+{
+	struct config_group *group = to_config_group(item->ci_parent);
+	struct mutex *su_mutex = &group->cg_subsys->su_mutex;
+	struct uvcg_string *string = to_uvcg_string(item);
+	struct config_item *opts_item;
+	struct f_uvc_opts *opts;
+	int ret;
+
+	mutex_lock(su_mutex);
+
+	opts_item = item->ci_parent->ci_parent->ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+	ret = sprintf(page, "%u\n", string->usb_string.id);
+	mutex_unlock(&opts->lock);
+
+	mutex_unlock(su_mutex);
+
+	return ret;
+}
+UVC_ATTR_RO(uvcg_string_, id, id);
+
+static ssize_t uvcg_string_s_show(struct config_item *item, char *page)
+{
+	struct config_group *group = to_config_group(item->ci_parent);
+	struct mutex *su_mutex = &group->cg_subsys->su_mutex;
+	struct uvcg_string *string = to_uvcg_string(item);
+	struct config_item *opts_item;
+	struct f_uvc_opts *opts;
+	int ret;
+
+	mutex_lock(su_mutex);
+
+	opts_item = item->ci_parent->ci_parent->ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+	ret = snprintf(page, sizeof(string->string), "%s\n", string->string);
+	mutex_unlock(&opts->lock);
+
+	mutex_unlock(su_mutex);
+
+	return ret;
+}
+
+static ssize_t uvcg_string_s_store(struct config_item *item, const char *page,
+				   size_t len)
+{
+	struct config_group *group = to_config_group(item->ci_parent);
+	struct mutex *su_mutex = &group->cg_subsys->su_mutex;
+	struct uvcg_string *string = to_uvcg_string(item);
+	int size = min(sizeof(string->string), len + 1);
+	struct config_item *opts_item;
+	struct f_uvc_opts *opts;
+	int ret;
+
+	if (len > USB_MAX_STRING_LEN)
+		return -EINVAL;
+
+	mutex_lock(su_mutex);
+
+	opts_item = item->ci_parent->ci_parent->ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+	ret = strscpy(string->string, page, size);
+	mutex_unlock(&opts->lock);
+
+	mutex_unlock(su_mutex);
+
+	return len;
+}
+UVC_ATTR(uvcg_string_, s, s);
+
+static struct configfs_attribute *uvcg_string_attrs[] = {
+	&uvcg_string_attr_id,
+	&uvcg_string_attr_s,
+	NULL,
+};
+
+static void uvcg_string_release(struct config_item *item)
+{
+	struct uvcg_string *string = to_uvcg_string(item);
+
+	kfree(string);
+}
+
+static struct configfs_item_operations uvcg_string_item_ops = {
+	.release	= uvcg_string_release,
+};
+
+static const struct config_item_type uvcg_string_type = {
+	.ct_item_ops	= &uvcg_string_item_ops,
+	.ct_attrs	= uvcg_string_attrs,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct config_item *uvcg_string_make(struct config_group *group,
+					    const char *name)
+{
+	struct uvcg_language *language;
+	struct config_item *opts_item;
+	struct uvcg_string *string;
+	struct f_uvc_opts *opts;
+
+	language = to_uvcg_language(group);
+
+	string = kzalloc(sizeof(*string), GFP_KERNEL);
+	if (!string)
+		return ERR_PTR(-ENOMEM);
+
+	opts_item = group->cg_item.ci_parent->ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+
+	string->usb_string.id = language->nstrings++;
+	string->usb_string.s = string->string;
+	list_add_tail(&string->list, &language->strings);
+
+	config_item_init_type_name(&string->item, name, &uvcg_string_type);
+
+	mutex_unlock(&opts->lock);
+
+	return &string->item;
+}
+
+static void uvcg_string_drop(struct config_group *group, struct config_item *item)
+{
+	struct uvcg_language *language;
+	struct config_item *opts_item;
+	struct uvcg_string *string;
+	struct f_uvc_opts *opts;
+	unsigned int i = 1;
+
+	language = to_uvcg_language(group);
+	string = to_uvcg_string(item);
+
+	opts_item = group->cg_item.ci_parent->ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+
+	list_del(&string->list);
+	language->nstrings--;
+
+	/* Reset the ids for the language's strings to guarantee a continuous set */
+	list_for_each_entry(string, &language->strings, list)
+		string->usb_string.id = i++;
+
+	mutex_unlock(&opts->lock);
+}
+
+/* -----------------------------------------------------------------------------
+ * strings/
+ */
+
+static struct configfs_group_operations uvcg_language_group_ops = {
+	.make_item	= uvcg_string_make,
+	.drop_item	= uvcg_string_drop,
+};
+
+static void uvcg_language_release(struct config_item *item)
+{
+	struct uvcg_language *language = to_uvcg_language(to_config_group(item));
+
+	kfree(language);
+}
+
+static struct configfs_item_operations uvcg_language_item_ops = {
+	.release	= uvcg_language_release,
+};
+
+/*
+ * The strings attribute is really a helper for users - having defined some string
+ * descriptors for this language, actually checking what's set when they're all
+ * in separate directories would be a bit...suboptimal. This read-only attribute
+ * gives a summary at language-level.
+ */
+static ssize_t uvcg_language_strings_show(struct config_item *item, char *page)
+{
+	struct config_group *group = to_config_group(item);
+	struct uvcg_language *language = to_uvcg_language(group);
+	struct mutex *su_mutex = &group->cg_subsys->su_mutex;
+	struct config_item *opts_item;
+	struct uvcg_string *string;
+	struct f_uvc_opts *opts;
+	char *pg = page;
+	int ret = 0;
+
+	mutex_lock(su_mutex);
+
+	opts_item = item->ci_parent->ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+
+	list_for_each_entry(string, &language->strings, list) {
+		ret += sprintf(pg, "%s: %s [%u]\n", string->item.ci_name,
+			       string->usb_string.s, string->usb_string.id);
+		pg = page + ret;
+	}
+
+	mutex_unlock(&opts->lock);
+
+	mutex_unlock(su_mutex);
+
+	return ret;
+}
+UVC_ATTR_RO(uvcg_language_, strings, strings);
+
+static struct configfs_attribute *uvcg_language_attrs[] = {
+	&uvcg_language_attr_strings,
+	NULL
+};
+
+static const struct config_item_type uvcg_language_type = {
+	.ct_item_ops	= &uvcg_language_item_ops,
+	.ct_group_ops	= &uvcg_language_group_ops,
+	.ct_attrs	= uvcg_language_attrs,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct config_group *uvcg_language_make(struct config_group *group,
+					       const char *name)
+{
+	struct uvcg_language *language;
+	struct config_item *opts_item;
+	struct f_uvc_opts *opts;
+	int ret;
+	u16 num;
+
+	ret = kstrtou16(name, 0, &num);
+	if (ret)
+		return ERR_PTR(ret);
+
+	if (!usb_validate_langid(num))
+		return ERR_PTR(-EINVAL);
+
+	language = kzalloc(sizeof(*language), GFP_KERNEL);
+	if (!language)
+		return ERR_PTR(-ENOMEM);
+
+	opts_item = group->cg_item.ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+
+	language->stringtab.language = num;
+	list_add_tail(&language->list, &opts->languages);
+	opts->nlangs++;
+	INIT_LIST_HEAD(&language->strings);
+
+	config_group_init_type_name(&language->group, name, &uvcg_language_type);
+
+	mutex_unlock(&opts->lock);
+
+	return &language->group;
+}
+
+static void uvcg_language_drop(struct config_group *group, struct config_item *item)
+{
+	struct uvcg_language *language;
+	struct config_item *opts_item;
+	struct f_uvc_opts *opts;
+
+	language = to_uvcg_language(to_config_group(item));
+
+	opts_item = group->cg_item.ci_parent;
+	opts = to_f_uvc_opts(opts_item);
+
+	mutex_lock(&opts->lock);
+
+	list_del(&language->list);
+	config_item_put(item);
+
+	mutex_unlock(&opts->lock);
+}
+
+static struct configfs_group_operations uvcg_strings_grp_ops = {
+	.make_group	= uvcg_language_make,
+	.drop_item	= uvcg_language_drop,
+};
+
+static const struct uvcg_config_group_type uvcg_strings_grp_type = {
+	.type = {
+		.ct_item_ops	= &uvcg_config_item_ops,
+		.ct_group_ops	= &uvcg_strings_grp_ops,
+		.ct_owner	= THIS_MODULE,
+	},
+	.name = "strings",
+};
+
 /* -----------------------------------------------------------------------------
  * UVC function
  */
@@ -2951,6 +3252,7 @@ static const struct uvcg_config_group_type uvc_func_type = {
 	.children = (const struct uvcg_config_group_type*[]) {
 		&uvcg_control_grp_type,
 		&uvcg_streaming_grp_type,
+		&uvcg_strings_grp_type,
 		NULL,
 	},
 };
diff --git a/drivers/usb/gadget/function/uvc_configfs.h b/drivers/usb/gadget/function/uvc_configfs.h
index c9a4182fb26f..a714426a174a 100644
--- a/drivers/usb/gadget/function/uvc_configfs.h
+++ b/drivers/usb/gadget/function/uvc_configfs.h
@@ -13,6 +13,7 @@
 #define UVC_CONFIGFS_H
 
 #include <linux/configfs.h>
+#include <linux/usb/gadget.h>
 
 #include "u_uvc.h"
 
@@ -132,6 +133,35 @@ static inline struct uvcg_mjpeg *to_uvcg_mjpeg(struct config_item *item)
 	return container_of(to_uvcg_format(item), struct uvcg_mjpeg, fmt);
 }
 
+/* -----------------------------------------------------------------------------
+ * strings/
+ */
+
+struct uvcg_language {
+	struct config_group group;
+	unsigned int nstrings;
+	struct list_head list;
+	struct list_head strings;
+	struct usb_gadget_strings stringtab;
+};
+
+#define to_uvcg_language(language) \
+container_of(language, struct uvcg_language, group)
+
+/* -----------------------------------------------------------------------------
+ * strings/<LANGID>
+ */
+
+struct uvcg_string {
+	struct config_item item;
+	struct list_head list;
+	char string[USB_MAX_STRING_LEN];
+	struct usb_string usb_string;
+};
+
+#define to_uvcg_string(str_item)\
+container_of(str_item, struct uvcg_string, item)
+
 /* -----------------------------------------------------------------------------
  * control/extensions/<NAME>
  */
