diff --git a/Makefile b/Makefile
index 3ce391062f..141e194bf5 100644
--- a/Makefile
+++ b/Makefile
@@ -2382,6 +2382,7 @@ FUZZ_OBJS += oss-fuzz/fuzz-credential-from-url-gently.o
 FUZZ_OBJS += oss-fuzz/fuzz-date.o
 FUZZ_OBJS += oss-fuzz/fuzz-pack-headers.o
 FUZZ_OBJS += oss-fuzz/fuzz-pack-idx.o
+FUZZ_OBJS += oss-fuzz/fuzz-parse-attr-line.o
 .PHONY: fuzz-objs
 fuzz-objs: $(FUZZ_OBJS)
 
diff --git a/attr.c b/attr.c
index 06b5b5e55e..b0073c5277 100644
--- a/attr.c
+++ b/attr.c
@@ -259,42 +259,6 @@ const struct git_attr *git_attr(const char *name)
 	return git_attr_internal(name, strlen(name));
 }
 
-/* What does a matched pattern decide? */
-struct attr_state {
-	const struct git_attr *attr;
-	const char *setto;
-};
-
-struct pattern {
-	const char *pattern;
-	int patternlen;
-	int nowildcardlen;
-	unsigned flags;		/* PATTERN_FLAG_* */
-};
-
-/*
- * One rule, as from a .gitattributes file.
- *
- * If is_macro is true, then u.attr is a pointer to the git_attr being
- * defined.
- *
- * If is_macro is false, then u.pat is the filename pattern to which the
- * rule applies.
- *
- * In either case, num_attr is the number of attributes affected by
- * this rule, and state is an array listing them.  The attributes are
- * listed as they appear in the file (macros unexpanded).
- */
-struct match_attr {
-	union {
-		struct pattern pat;
-		const struct git_attr *attr;
-	} u;
-	char is_macro;
-	size_t num_attr;
-	struct attr_state state[FLEX_ARRAY];
-};
-
 static const char blank[] = " \t\r\n";
 
 /* Flags usable in read_attr() and parse_attr_line() family of functions. */
@@ -353,8 +317,8 @@ static const char *parse_attr(const char *src, int lineno, const char *cp,
 	return ep + strspn(ep, blank);
 }
 
-static struct match_attr *parse_attr_line(const char *line, const char *src,
-					  int lineno, unsigned flags)
+struct match_attr *parse_attr_line(const char *line, const char *src,
+				   int lineno, unsigned flags)
 {
 	size_t namelen, num_attr, i;
 	const char *cp, *name, *states;
diff --git a/attr.h b/attr.h
index bb33b60880..a04a521092 100644
--- a/attr.h
+++ b/attr.h
@@ -240,4 +240,47 @@ int git_attr_system_is_enabled(void);
 
 extern char *git_attr_tree;
 
+/*
+ * Exposed for fuzz-testing only.
+ */
+
+/* What does a matched pattern decide? */
+struct attr_state {
+	const struct git_attr *attr;
+	const char *setto;
+};
+
+struct pattern {
+	const char *pattern;
+	int patternlen;
+	int nowildcardlen;
+	unsigned flags;		/* PATTERN_FLAG_* */
+};
+
+/*
+ * One rule, as from a .gitattributes file.
+ *
+ * If is_macro is true, then u.attr is a pointer to the git_attr being
+ * defined.
+ *
+ * If is_macro is false, then u.pat is the filename pattern to which the
+ * rule applies.
+ *
+ * In either case, num_attr is the number of attributes affected by
+ * this rule, and state is an array listing them.  The attributes are
+ * listed as they appear in the file (macros unexpanded).
+ */
+struct match_attr {
+	union {
+		struct pattern pat;
+		const struct git_attr *attr;
+	} u;
+	char is_macro;
+	size_t num_attr;
+	struct attr_state state[FLEX_ARRAY];
+};
+
+struct match_attr *parse_attr_line(const char *line, const char *src,
+				   int lineno, unsigned flags);
+
 #endif /* ATTR_H */
diff --git a/ci/run-build-and-minimal-fuzzers.sh b/ci/run-build-and-minimal-fuzzers.sh
index 631796ab8b..13c85320d6 100755
--- a/ci/run-build-and-minimal-fuzzers.sh
+++ b/ci/run-build-and-minimal-fuzzers.sh
@@ -20,6 +20,7 @@ credential-from-url-gently
 date
 pack-headers
 pack-idx
+parse-attr-line
 "
 
 for fuzzer in $fuzzers; do
diff --git a/oss-fuzz/.gitignore b/oss-fuzz/.gitignore
index 2cfc845b20..ec185f061c 100644
--- a/oss-fuzz/.gitignore
+++ b/oss-fuzz/.gitignore
@@ -4,3 +4,4 @@ fuzz-credential-from-url-gently
 fuzz-date
 fuzz-pack-headers
 fuzz-pack-idx
+fuzz-parse-attr-line
diff --git a/oss-fuzz/fuzz-parse-attr-line.c b/oss-fuzz/fuzz-parse-attr-line.c
new file mode 100644
index 0000000000..45a4c4e53c
--- /dev/null
+++ b/oss-fuzz/fuzz-parse-attr-line.c
@@ -0,0 +1,39 @@
+#include "git-compat-util.h"
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include "attr.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+	struct match_attr *res;
+	char *buf;
+
+	buf = malloc(size + 1);
+	if (!buf)
+		return 0;
+
+	memcpy(buf, data, size);
+	buf[size] = 0;
+
+	res = parse_attr_line(buf, "dummy", 0, 0);
+
+	if (res) {
+		int j;
+		for (j = 0; j < res->num_attr; j++) {
+			const char *setto = res->state[j].setto;
+			if (ATTR_TRUE(setto) || ATTR_FALSE(setto) ||
+				ATTR_UNSET(setto))
+				;
+			else
+				free((char *)setto);
+		}
+		free(res);
+	}
+	free(buf);
+
+	return 0;
+}
