Merge branch 'nate-i50192-buf' into 'master'

See merge request SchedMD/dev/slurm!4208
diff --git a/src/common/pack.c b/src/common/pack.c
index 34a1403..93285c9 100644
--- a/src/common/pack.c
+++ b/src/common/pack.c
@@ -253,6 +253,59 @@
 	return SLURM_SUCCESS;
 }
 
+extern int buf_append_bytes(buf_t *buf, const void *ptr, size_t bytes)
+{
+	int rc = EINVAL;
+
+	xassert(buf);
+	xassert(buf->magic == BUF_MAGIC);
+
+	if (!bytes)
+		return SLURM_SUCCESS;
+
+	/*
+	 * try_grow_buf_remaining() only rejects these when it has to grow, but
+	 * the memcpy() below writes through to borrowed memory whether or not
+	 * the buffer needs to grow first
+	 */
+	if (buf->mmaped || buf->shadow)
+		return EINVAL;
+
+	/*
+	 * try_grow_buf_remaining() takes a uint32_t which would silently
+	 * truncate bytes while the memcpy() below would still copy all of them
+	 */
+	if (bytes > MAX_BUF_SIZE)
+		return ESLURM_DATA_TOO_LARGE;
+
+	if ((rc = try_grow_buf_remaining(buf, bytes)))
+		return rc;
+
+	(void) memcpy((get_buf_data(buf) + get_buf_offset(buf)), ptr, bytes);
+	set_buf_offset(buf, (get_buf_offset(buf) + bytes));
+
+	return SLURM_SUCCESS;
+}
+
+extern int buf_append_str(buf_t *buf, const char *str)
+{
+	int rc;
+	size_t bytes;
+
+	if (!str)
+		return SLURM_SUCCESS;
+
+	bytes = strlen(str);
+
+	if (!bytes)
+		return SLURM_SUCCESS;
+
+	if ((rc = buf_append_bytes(buf, str, bytes)))
+		return rc;
+
+	return SLURM_SUCCESS;
+}
+
 /* init_buf - create an empty buffer of the given size */
 buf_t *init_buf(uint32_t size)
 {
diff --git a/src/common/pack.h b/src/common/pack.h
index 9f3e3d7..e61192d 100644
--- a/src/common/pack.h
+++ b/src/common/pack.h
@@ -74,11 +74,11 @@
 	bool shadow;
 } buf_t;
 
-#define get_buf_data(__buf)		(__buf->head)
-#define get_buf_offset(__buf)		(__buf->processed)
-#define set_buf_offset(__buf,__val)	(__buf->processed = __val)
-#define remaining_buf(__buf)		(__buf->size - __buf->processed)
-#define size_buf(__buf)			(__buf->size)
+#define get_buf_data(__buf) ((__buf)->head)
+#define get_buf_offset(__buf) ((__buf)->processed)
+#define set_buf_offset(__buf, __val) ((__buf)->processed = (__val))
+#define remaining_buf(__buf) ((__buf)->size - (__buf)->processed)
+#define size_buf(__buf) ((__buf)->size)
 
 /* Initialize shadow buffer to point at data with size of bytes */
 #define SHADOW_BUF_INITIALIZER(data, bytes) \
@@ -132,6 +132,28 @@
  * RET SLURM_SUCCESS or error
  */
 extern int try_grow_buf_remaining(buf_t *buffer, uint32_t size);
+
+/*
+ * Append a binary-safe chunk of memory to a buf_t, growing it as needed.
+ * The appended data is not NUL-terminated.
+ * IN buf - buffer to append to
+ * IN ptr - pointer to the bytes to append; may be NULL only if bytes is 0
+ * IN bytes - number of bytes to append
+ * RET SLURM_SUCCESS or error (EINVAL / ESLURM_DATA_TOO_LARGE / ENOMEM)
+ */
+extern int buf_append_bytes(buf_t *buf, const void *ptr, size_t bytes);
+
+/*
+ * Append a NUL-terminated string to a buf_t but will not append NUL-terminator
+ * to the buffer.
+ *
+ * IN buf - buffer to append to
+ * IN str - NUL-terminated string to append; NULL is treated as an empty
+ *	string (a no-op success)
+ * RET SLURM_SUCCESS or error (EINVAL / ESLURM_DATA_TOO_LARGE / ENOMEM)
+ */
+extern int buf_append_str(buf_t *buf, const char *str);
+
 /*
  * Extract Buffer head pointer
  * NOTE: Use xfer_buf_data() macro instead
diff --git a/src/common/slurm_xlator.h b/src/common/slurm_xlator.h
index 8b04cbe..1273b02 100644
--- a/src/common/slurm_xlator.h
+++ b/src/common/slurm_xlator.h
@@ -401,35 +401,36 @@
 #define	xsignal_sigset_create	slurm_xsignal_sigset_create
 
 /* xstring.[ch] functions */
-#define	_xstrcat		slurm_xstrcat
-#define	_xstrcatat		slurm_xstrcatat
-#define	_xstrncat		slurm_xstrncat
-#define	_xstrcatchar		slurm_xstrcatchar
-#define	_xstrftimecat		slurm_xstrftimecat
-#define	_xiso8601timecat	slurm_xiso8601timecat
-#define	_xrfc5424timecat	slurm_xrfc5424timecat
-#define	_xstrfmtcat		slurm_xstrfmtcat
-#define	_xstrfmtcatat		slurm_xstrfmtcatat
-#define	_xmemcat		slurm_xmemcat
-#define	xstrdup			slurm_xstrdup
-#define	xstrdup_printf		slurm_xstrdup_printf
-#define _xstrdup_vprintf	slurm_xstrdup_vprintf
-#define	xstrndup		slurm_xstrndup
-#define	xbasename		slurm_xbasename
-#define	xdirname		slurm_xdirname
-#define	_xstrsubstitute		slurm_xstrsubstitute
-#define	xshort_hostname		slurm_xshort_hostname
-#define xstring_is_whitespace   slurm_xstring_is_whitespace
-#define	xstrtolower		slurm_xstrtolower
-#define xstrchr			slurm_xstrchr
-#define xstrrchr		slurm_xstrrchr
-#define xstrcmp			slurm_xstrcmp
-#define xstrncmp		slurm_xstrncmp
-#define xstrcasecmp		slurm_xstrcasecmp
-#define xstrncasecmp		slurm_xstrncasecmp
-#define	xstrstr			slurm_xstrstr
-#define xstrcasestr		slurm_xstrcasestr
-#define xbase64_from_base64url	slurm_xbase64_from_base64url
+#define _xstrcat slurm_xstrcat
+#define _xstrcatat slurm_xstrcatat
+#define _xstrncat slurm_xstrncat
+#define _xstrcatchar slurm_xstrcatchar
+#define _xstrftimecat slurm_xstrftimecat
+#define _xiso8601timecat slurm_xiso8601timecat
+#define _xrfc5424timecat slurm_xrfc5424timecat
+#define _xstrfmtcat slurm_xstrfmtcat
+#define _xstrfmtcatat slurm_xstrfmtcatat
+#define _xmemcat slurm_xmemcat
+#define xstrdup slurm_xstrdup
+#define try_xstrndup slurm_try_xstrndup
+#define xstrdup_printf slurm_xstrdup_printf
+#define _xstrdup_vprintf slurm_xstrdup_vprintf
+#define xstrndup slurm_xstrndup
+#define xbasename slurm_xbasename
+#define xdirname slurm_xdirname
+#define _xstrsubstitute slurm_xstrsubstitute
+#define xshort_hostname slurm_xshort_hostname
+#define xstring_is_whitespace slurm_xstring_is_whitespace
+#define xstrtolower slurm_xstrtolower
+#define xstrchr slurm_xstrchr
+#define xstrrchr slurm_xstrrchr
+#define xstrcmp slurm_xstrcmp
+#define xstrncmp slurm_xstrncmp
+#define xstrcasecmp slurm_xstrcasecmp
+#define xstrncasecmp slurm_xstrncasecmp
+#define xstrstr slurm_xstrstr
+#define xstrcasestr slurm_xstrcasestr
+#define xbase64_from_base64url slurm_xbase64_from_base64url
 
 /* xutf.[ch] functions */
 #define utf_encoding_scheme_to_string slurm_utf_encoding_scheme_to_string
diff --git a/src/common/xstring.c b/src/common/xstring.c
index 9e27c42..cc14731 100644
--- a/src/common/xstring.c
+++ b/src/common/xstring.c
@@ -67,34 +67,35 @@
  * Define slurm-specific aliases for use by plugins, see slurm_xlator.h
  * for details.
  */
-strong_alias(_xstrcat,		slurm_xstrcat);
-strong_alias(_xstrncat,		slurm_xstrncat);
-strong_alias(_xstrncatat,	slurm_xstrncatat);
-strong_alias(_xstrcatchar,	slurm_xstrcatchar);
-strong_alias(_xstrftimecat,	slurm_xstrftimecat);
-strong_alias(_xiso8601timecat,	slurm_xiso8601timecat);
-strong_alias(_xrfc5424timecat,	slurm_xrfc5424timecat);
-strong_alias(_xstrfmtcat,	slurm_xstrfmtcat);
-strong_alias(_xstrfmtcatat,	slurm_xstrfmtcatat);
-strong_alias(_xmemcat,		slurm_xmemcat);
-strong_alias(xstrdup,		slurm_xstrdup);
-strong_alias(xstrdup_printf,	slurm_xstrdup_printf);
-strong_alias(_xstrdup_vprintf,	slurm_xstrdup_vprintf);
-strong_alias(xstrndup,		slurm_xstrndup);
-strong_alias(xbasename,		slurm_xbasename);
-strong_alias(xdirname,		slurm_xdirname);
-strong_alias(_xstrsubstitute,   slurm_xstrsubstitute);
-strong_alias(xshort_hostname,   slurm_xshort_hostname);
+strong_alias(_xstrcat, slurm_xstrcat);
+strong_alias(_xstrncat, slurm_xstrncat);
+strong_alias(_xstrncatat, slurm_xstrncatat);
+strong_alias(_xstrcatchar, slurm_xstrcatchar);
+strong_alias(_xstrftimecat, slurm_xstrftimecat);
+strong_alias(_xiso8601timecat, slurm_xiso8601timecat);
+strong_alias(_xrfc5424timecat, slurm_xrfc5424timecat);
+strong_alias(_xstrfmtcat, slurm_xstrfmtcat);
+strong_alias(_xstrfmtcatat, slurm_xstrfmtcatat);
+strong_alias(_xmemcat, slurm_xmemcat);
+strong_alias(xstrdup, slurm_xstrdup);
+strong_alias(xstrdup_printf, slurm_xstrdup_printf);
+strong_alias(_xstrdup_vprintf, slurm_xstrdup_vprintf);
+strong_alias(xstrndup, slurm_xstrndup);
+strong_alias(try_xstrndup, slurm_try_xstrndup);
+strong_alias(xbasename, slurm_xbasename);
+strong_alias(xdirname, slurm_xdirname);
+strong_alias(_xstrsubstitute, slurm_xstrsubstitute);
+strong_alias(xshort_hostname, slurm_xshort_hostname);
 strong_alias(xstring_is_whitespace, slurm_xstring_is_whitespace);
-strong_alias(xstrtolower,       slurm_xstrtolower);
-strong_alias(xstrchr,           slurm_xstrchr);
-strong_alias(xstrrchr,          slurm_xstrrchr);
-strong_alias(xstrcmp,           slurm_xstrcmp);
-strong_alias(xstrncmp,          slurm_xstrncmp);
-strong_alias(xstrcasecmp,       slurm_xstrcasecmp);
-strong_alias(xstrncasecmp,      slurm_xstrncasecmp);
-strong_alias(xstrstr,           slurm_xstrstr);
-strong_alias(xstrcasestr,       slurm_xstrcasestr);
+strong_alias(xstrtolower, slurm_xstrtolower);
+strong_alias(xstrchr, slurm_xstrchr);
+strong_alias(xstrrchr, slurm_xstrrchr);
+strong_alias(xstrcmp, slurm_xstrcmp);
+strong_alias(xstrncmp, slurm_xstrncmp);
+strong_alias(xstrcasecmp, slurm_xstrcasecmp);
+strong_alias(xstrncasecmp, slurm_xstrncasecmp);
+strong_alias(xstrstr, slurm_xstrstr);
+strong_alias(xstrcasestr, slurm_xstrcasestr);
 strong_alias(xstrtoken, slurm_xstrtoken);
 strong_alias(xbase64_from_base64url, slurm_xbase64_from_base64url);
 
@@ -529,6 +530,26 @@
 	return result;
 }
 
+extern char *try_xstrndup(const char *str, const size_t n)
+{
+	size_t siz = 0;
+	char *result = NULL;
+
+	if (!str)
+		return NULL;
+
+	siz = strnlen(str, n);
+	result = try_xmalloc(siz + 1);
+
+	if (!result)
+		return NULL;
+
+	(void) memcpy(result, str, siz);
+	result[siz] = '\0';
+
+	return result;
+}
+
 /*
 ** strtol which only reads 'n' number of chars in the str to get the number
 */
diff --git a/src/common/xstring.h b/src/common/xstring.h
index 299c4c2..dc2532f 100644
--- a/src/common/xstring.h
+++ b/src/common/xstring.h
@@ -155,6 +155,9 @@
 */
 char *xstrndup(const char *str, size_t n);
 
+/* xstrndup which uses xmalloc routines but may return NULL on ENOMEM */
+extern char *try_xstrndup(const char *str, const size_t n);
+
 /*
 ** strtol which only reads 'n' number of chars in the str to get the number
 */
diff --git a/testsuite/slurm_unit/common/Makefile.am b/testsuite/slurm_unit/common/Makefile.am
index c9a914a..0bc302a 100644
--- a/testsuite/slurm_unit/common/Makefile.am
+++ b/testsuite/slurm_unit/common/Makefile.am
@@ -20,6 +20,7 @@
 MYCFLAGS  = @CHECK_CFLAGS@ -Wall
 MYCFLAGS += -D_ISO99_SOURCE
 TESTS += xhash-test \
+	 buf-test \
 	 data-test \
 	 dns-test \
 	 http-test \
@@ -35,6 +36,8 @@
 xhash_test_LDADD  = $(LDADD) @CHECK_LIBS@
 xahash_test_CFLAGS = $(MYCFLAGS)
 xahash_test_LDADD  = $(LDADD) @CHECK_LIBS@
+buf_test_CFLAGS   = $(MYCFLAGS)
+buf_test_LDADD    = $(LDADD) @CHECK_LIBS@
 data_test_CFLAGS  = $(MYCFLAGS)
 data_test_LDADD   = $(LDADD) @CHECK_LIBS@
 dns_test_CFLAGS  = $(MYCFLAGS)
diff --git a/testsuite/slurm_unit/common/Makefile.in b/testsuite/slurm_unit/common/Makefile.in
index 200737e..2f1e40e 100644
--- a/testsuite/slurm_unit/common/Makefile.in
+++ b/testsuite/slurm_unit/common/Makefile.in
@@ -93,6 +93,7 @@
 check_PROGRAMS = $(am__EXEEXT_3)
 TESTS = log-test$(EXEEXT) $(am__EXEEXT_1) $(am__EXEEXT_2)
 @HAVE_CHECK_TRUE@am__append_1 = xhash-test \
+@HAVE_CHECK_TRUE@	 buf-test \
 @HAVE_CHECK_TRUE@	 data-test \
 @HAVE_CHECK_TRUE@	 dns-test \
 @HAVE_CHECK_TRUE@	 http-test \
@@ -169,24 +170,30 @@
 	$(top_builddir)/slurm/slurm_version.h
 CONFIG_CLEAN_FILES =
 CONFIG_CLEAN_VPATH_FILES =
-@HAVE_CHECK_TRUE@am__EXEEXT_1 = xhash-test$(EXEEXT) data-test$(EXEEXT) \
-@HAVE_CHECK_TRUE@	dns-test$(EXEEXT) http-test$(EXEEXT) \
-@HAVE_CHECK_TRUE@	sluid-test$(EXEEXT) xbase64-test$(EXEEXT) \
-@HAVE_CHECK_TRUE@	xstring-test$(EXEEXT) \
+@HAVE_CHECK_TRUE@am__EXEEXT_1 = xhash-test$(EXEEXT) buf-test$(EXEEXT) \
+@HAVE_CHECK_TRUE@	data-test$(EXEEXT) dns-test$(EXEEXT) \
+@HAVE_CHECK_TRUE@	http-test$(EXEEXT) sluid-test$(EXEEXT) \
+@HAVE_CHECK_TRUE@	xbase64-test$(EXEEXT) xstring-test$(EXEEXT) \
 @HAVE_CHECK_TRUE@	parse_time-test$(EXEEXT) pack-test$(EXEEXT) \
 @HAVE_CHECK_TRUE@	reverse_tree-test$(EXEEXT) \
 @HAVE_CHECK_TRUE@	xahash-test$(EXEEXT)
 @HAVE_CHECK_TRUE@@HAVE_LUA_TRUE@am__EXEEXT_2 = lua-test$(EXEEXT)
 am__EXEEXT_3 = log-test$(EXEEXT) $(am__EXEEXT_1) $(am__EXEEXT_2)
-data_test_SOURCES = data-test.c
-data_test_OBJECTS = data_test-data-test.$(OBJEXT)
+buf_test_SOURCES = buf-test.c
+buf_test_OBJECTS = buf_test-buf-test.$(OBJEXT)
 am__DEPENDENCIES_1 =
 am__DEPENDENCIES_2 = $(am__DEPENDENCIES_1)
-@HAVE_CHECK_TRUE@data_test_DEPENDENCIES = $(am__DEPENDENCIES_2)
+@HAVE_CHECK_TRUE@buf_test_DEPENDENCIES = $(am__DEPENDENCIES_2)
 AM_V_lt = $(am__v_lt_@AM_V@)
 am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
 am__v_lt_0 = --silent
 am__v_lt_1 = 
+buf_test_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+	$(LIBTOOLFLAGS) --mode=link $(CCLD) $(buf_test_CFLAGS) \
+	$(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+data_test_SOURCES = data-test.c
+data_test_OBJECTS = data_test-data-test.$(OBJEXT)
+@HAVE_CHECK_TRUE@data_test_DEPENDENCIES = $(am__DEPENDENCIES_2)
 data_test_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
 	$(LIBTOOLFLAGS) --mode=link $(CCLD) $(data_test_CFLAGS) \
 	$(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
@@ -282,7 +289,8 @@
 DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) -I$(top_builddir)/slurm
 depcomp = $(SHELL) $(top_srcdir)/auxdir/depcomp
 am__maybe_remake_depfiles = depfiles
-am__depfiles_remade = ./$(DEPDIR)/data_test-data-test.Po \
+am__depfiles_remade = ./$(DEPDIR)/buf_test-buf-test.Po \
+	./$(DEPDIR)/data_test-data-test.Po \
 	./$(DEPDIR)/dns_test-dns-test.Po \
 	./$(DEPDIR)/http_test-http-test.Po ./$(DEPDIR)/log-test.Po \
 	./$(DEPDIR)/lua_test-lua-test.Po \
@@ -313,9 +321,10 @@
 am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
 am__v_CCLD_0 = @echo "  CCLD    " $@;
 am__v_CCLD_1 = 
-SOURCES = data-test.c dns-test.c http-test.c log-test.c lua-test.c \
-	pack-test.c parse_time-test.c reverse_tree-test.c sluid-test.c \
-	xahash-test.c xbase64-test.c xhash-test.c xstring-test.c
+SOURCES = buf-test.c data-test.c dns-test.c http-test.c log-test.c \
+	lua-test.c pack-test.c parse_time-test.c reverse_tree-test.c \
+	sluid-test.c xahash-test.c xbase64-test.c xhash-test.c \
+	xstring-test.c
 RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
 	ctags-recursive dvi-recursive html-recursive info-recursive \
 	install-data-recursive install-dvi-recursive \
@@ -842,6 +851,8 @@
 @HAVE_CHECK_TRUE@xhash_test_LDADD = $(LDADD) @CHECK_LIBS@
 @HAVE_CHECK_TRUE@xahash_test_CFLAGS = $(MYCFLAGS)
 @HAVE_CHECK_TRUE@xahash_test_LDADD = $(LDADD) @CHECK_LIBS@
+@HAVE_CHECK_TRUE@buf_test_CFLAGS = $(MYCFLAGS)
+@HAVE_CHECK_TRUE@buf_test_LDADD = $(LDADD) @CHECK_LIBS@
 @HAVE_CHECK_TRUE@data_test_CFLAGS = $(MYCFLAGS)
 @HAVE_CHECK_TRUE@data_test_LDADD = $(LDADD) @CHECK_LIBS@
 @HAVE_CHECK_TRUE@dns_test_CFLAGS = $(MYCFLAGS)
@@ -906,6 +917,10 @@
 	$(am__rm_f) $(check_PROGRAMS)
 	test -z "$(EXEEXT)" || $(am__rm_f) $(check_PROGRAMS:$(EXEEXT)=)
 
+buf-test$(EXEEXT): $(buf_test_OBJECTS) $(buf_test_DEPENDENCIES) $(EXTRA_buf_test_DEPENDENCIES) 
+	@rm -f buf-test$(EXEEXT)
+	$(AM_V_CCLD)$(buf_test_LINK) $(buf_test_OBJECTS) $(buf_test_LDADD) $(LIBS)
+
 data-test$(EXEEXT): $(data_test_OBJECTS) $(data_test_DEPENDENCIES) $(EXTRA_data_test_DEPENDENCIES) 
 	@rm -f data-test$(EXEEXT)
 	$(AM_V_CCLD)$(data_test_LINK) $(data_test_OBJECTS) $(data_test_LDADD) $(LIBS)
@@ -964,6 +979,7 @@
 distclean-compile:
 	-rm -f *.tab.c
 
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/buf_test-buf-test.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/data_test-data-test.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dns_test-dns-test.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http_test-http-test.Po@am__quote@ # am--include-marker
@@ -1005,6 +1021,20 @@
 @AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
 @am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
 
+buf_test-buf-test.o: buf-test.c
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(buf_test_CFLAGS) $(CFLAGS) -MT buf_test-buf-test.o -MD -MP -MF $(DEPDIR)/buf_test-buf-test.Tpo -c -o buf_test-buf-test.o `test -f 'buf-test.c' || echo '$(srcdir)/'`buf-test.c
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/buf_test-buf-test.Tpo $(DEPDIR)/buf_test-buf-test.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='buf-test.c' object='buf_test-buf-test.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(buf_test_CFLAGS) $(CFLAGS) -c -o buf_test-buf-test.o `test -f 'buf-test.c' || echo '$(srcdir)/'`buf-test.c
+
+buf_test-buf-test.obj: buf-test.c
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(buf_test_CFLAGS) $(CFLAGS) -MT buf_test-buf-test.obj -MD -MP -MF $(DEPDIR)/buf_test-buf-test.Tpo -c -o buf_test-buf-test.obj `if test -f 'buf-test.c'; then $(CYGPATH_W) 'buf-test.c'; else $(CYGPATH_W) '$(srcdir)/buf-test.c'; fi`
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/buf_test-buf-test.Tpo $(DEPDIR)/buf_test-buf-test.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='buf-test.c' object='buf_test-buf-test.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(buf_test_CFLAGS) $(CFLAGS) -c -o buf_test-buf-test.obj `if test -f 'buf-test.c'; then $(CYGPATH_W) 'buf-test.c'; else $(CYGPATH_W) '$(srcdir)/buf-test.c'; fi`
+
 data_test-data-test.o: data-test.c
 @am__fastdepCC_TRUE@	$(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(data_test_CFLAGS) $(CFLAGS) -MT data_test-data-test.o -MD -MP -MF $(DEPDIR)/data_test-data-test.Tpo -c -o data_test-data-test.o `test -f 'data-test.c' || echo '$(srcdir)/'`data-test.c
 @am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/data_test-data-test.Tpo $(DEPDIR)/data_test-data-test.Po
@@ -1458,6 +1488,13 @@
 	--log-file $$b.log --trs-file $$b.trs \
 	$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
 	"$$tst" $(AM_TESTS_FD_REDIRECT)
+buf-test.log: buf-test$(EXEEXT)
+	@p='buf-test$(EXEEXT)'; \
+	b='buf-test'; \
+	$(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+	--log-file $$b.log --trs-file $$b.trs \
+	$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+	"$$tst" $(AM_TESTS_FD_REDIRECT)
 data-test.log: data-test$(EXEEXT)
 	@p='data-test$(EXEEXT)'; \
 	b='data-test'; \
@@ -1595,6 +1632,7 @@
 	mostlyclean-am
 
 distclean: distclean-recursive
+	-rm -f ./$(DEPDIR)/buf_test-buf-test.Po
 	-rm -f ./$(DEPDIR)/data_test-data-test.Po
 	-rm -f ./$(DEPDIR)/dns_test-dns-test.Po
 	-rm -f ./$(DEPDIR)/http_test-http-test.Po
@@ -1653,6 +1691,7 @@
 installcheck-am:
 
 maintainer-clean: maintainer-clean-recursive
+	-rm -f ./$(DEPDIR)/buf_test-buf-test.Po
 	-rm -f ./$(DEPDIR)/data_test-data-test.Po
 	-rm -f ./$(DEPDIR)/dns_test-dns-test.Po
 	-rm -f ./$(DEPDIR)/http_test-http-test.Po
diff --git a/testsuite/slurm_unit/common/buf-test.c b/testsuite/slurm_unit/common/buf-test.c
new file mode 100644
index 0000000..325fe1e
--- /dev/null
+++ b/testsuite/slurm_unit/common/buf-test.c
@@ -0,0 +1,613 @@
+/*****************************************************************************\
+ *  buf-test.c - unit tests for the buf_t lifecycle and accessors
+ *****************************************************************************
+ *  Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+ *
+ *  This file is part of Slurm, a resource management program.
+ *  For details, see <https://slurm.schedmd.com/>.
+ *  Please also read the included file: DISCLAIMER.
+ *
+ *  Slurm is free software; you can redistribute it and/or modify it under
+ *  the terms of the GNU General Public License as published by the Free
+ *  Software Foundation; either version 2 of the License, or (at your option)
+ *  any later version.
+ *
+ *  In addition, as a special exception, the copyright holders give permission
+ *  to link the code of portions of this program with the OpenSSL library under
+ *  certain conditions as described in each individual source file, and
+ *  distribute linked combinations including the two. You must obey the GNU
+ *  General Public License in all respects for all of the code used other than
+ *  OpenSSL. If you modify file(s) with this exception, you may extend this
+ *  exception to your version of the file(s), but you are not obligated to do
+ *  so. If you do not wish to do so, delete this exception statement from your
+ *  version.  If you delete this exception statement from all source files in
+ *  the program, then also delete it here.
+ *
+ *  Slurm is distributed in the hope that it will be useful, but WITHOUT ANY
+ *  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ *  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ *  details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with Slurm; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
+\*****************************************************************************/
+
+#include <check.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "slurm/slurm_errno.h"
+
+#include "src/common/log.h"
+#include "src/common/pack.h"
+#include "src/common/read_config.h"
+#include "src/common/xmalloc.h"
+
+/*
+ * Tests for the buf_t life cycle and accessors in src/common/pack.c.
+ * The pack*()/unpack*() serializers are covered by pack-test.c.
+ */
+
+/* Verify buffer is empty and sized as requested */
+static void _check_empty_buf(buf_t *buf, const uint32_t size)
+{
+	ck_assert(buf != NULL);
+	ck_assert(get_buf_data(buf) != NULL);
+	ck_assert_int_eq(size_buf(buf), size);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+	ck_assert_int_eq(remaining_buf(buf), size);
+}
+
+START_TEST(test_init_buf)
+{
+	buf_t *buf = init_buf(128);
+	const char zeros[128] = { 0 };
+
+	_check_empty_buf(buf, 128);
+	/* init_buf() uses xmalloc() which always zeros the allocation */
+	ck_assert_msg(!memcmp(get_buf_data(buf), zeros, sizeof(zeros)),
+		      "init_buf() did not zero the allocation");
+	free_buf(buf);
+
+	/* a size of zero must fall back to the BUF_SIZE default */
+	buf = init_buf(0);
+	_check_empty_buf(buf, BUF_SIZE);
+	free_buf(buf);
+
+	/* free_buf() must accept NULL */
+	free_buf(NULL);
+
+	buf = init_buf(0);
+	FREE_NULL_BUFFER(buf);
+	ck_assert(buf == NULL);
+	/* FREE_NULL_BUFFER() must be a no-op when already NULL */
+	FREE_NULL_BUFFER(buf);
+	ck_assert(buf == NULL);
+}
+
+END_TEST
+
+START_TEST(test_try_init_buf)
+{
+	buf_t *buf = try_init_buf(128);
+
+	_check_empty_buf(buf, 128);
+	free_buf(buf);
+
+	/* a size of zero must fall back to the BUF_SIZE default */
+	buf = try_init_buf(0);
+	_check_empty_buf(buf, BUF_SIZE);
+	free_buf(buf);
+
+	/* oversized request must be rejected instead of fatal()ing */
+	ck_assert(try_init_buf(MAX_BUF_SIZE + 1) == NULL);
+}
+
+END_TEST
+
+START_TEST(test_create_buf)
+{
+	char *data = xmalloc(16);
+	buf_t *buf;
+
+	memcpy(data, "0123456789abcdef", 16);
+
+	buf = create_buf(data, 16);
+	ck_assert(buf != NULL);
+	/* create_buf() takes ownership of data instead of copying it */
+	ck_assert(get_buf_data(buf) == data);
+	ck_assert_int_eq(size_buf(buf), 16);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+	ck_assert_int_eq(remaining_buf(buf), 16);
+	free_buf(buf);
+
+	/* oversized request must be rejected */
+	ck_assert(create_buf(NULL, MAX_BUF_SIZE + 1) == NULL);
+}
+
+END_TEST
+
+START_TEST(test_create_shadow_buf)
+{
+	char data[] = "shadowed";
+	buf_t *buf = create_shadow_buf(data, (sizeof(data) - 1));
+
+	ck_assert(buf != NULL);
+	ck_assert(get_buf_data(buf) == data);
+	ck_assert_int_eq(size_buf(buf), (sizeof(data) - 1));
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+
+	/* shadow buffers do not own their memory and can not be grown */
+	ck_assert_int_eq(try_grow_buf(buf, 1), EINVAL);
+	ck_assert_int_eq(size_buf(buf), (sizeof(data) - 1));
+
+	/*
+	 * try_grow_buf_remaining() only rejects an append when it has to grow,
+	 * so with room already remaining an append must still be rejected by
+	 * buf_append_bytes()/buf_append_str()'s own shadow check, or it would
+	 * write through to the caller's memory
+	 */
+	ck_assert_int_eq(buf_append_bytes(buf, "XXXX", 4), EINVAL);
+	ck_assert_int_eq(buf_append_str(buf, "XXXX"), EINVAL);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+	ck_assert_str_eq(data, "shadowed");
+
+	/* free_buf() must not release the shadowed memory */
+	free_buf(buf);
+	ck_assert_str_eq(data, "shadowed");
+
+	ck_assert(create_shadow_buf(data, (MAX_BUF_SIZE + 1)) == NULL);
+}
+
+END_TEST
+
+START_TEST(test_create_mmap_buf)
+{
+	const char contents[] = "mmap()ed buffer contents";
+	const size_t bytes = (sizeof(contents) - 1);
+	char path[PATH_MAX];
+	int fd;
+	buf_t *buf;
+
+	snprintf(path, sizeof(path), "%s/buf-test-XXXXXX",
+		 (getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp"));
+	fd = mkstemp(path);
+
+	ck_assert_int_ge(fd, 0);
+	ck_assert_int_eq(write(fd, contents, bytes), bytes);
+	ck_assert_int_eq(close(fd), 0);
+
+	buf = create_mmap_buf(path);
+	/* the mapping outlives the file, so unlink before asserting anything */
+	ck_assert_int_eq(unlink(path), 0);
+
+	ck_assert(buf != NULL);
+	ck_assert_int_eq(size_buf(buf), bytes);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+	ck_assert_int_eq(remaining_buf(buf), bytes);
+	ck_assert_msg(!memcmp(get_buf_data(buf), contents, bytes),
+		      "mmap()ed buffer does not match the file contents");
+
+	/* mmap()ed buffers are read only and can not be grown */
+	ck_assert_int_eq(try_grow_buf(buf, 1), EINVAL);
+
+	/*
+	 * room remains in the mapping, so only buf_append_bytes()/
+	 * buf_append_str()'s own mmaped check stops a write through a
+	 * PROT_READ mapping -- try_grow_buf_remaining() alone would not
+	 */
+	ck_assert_int_eq(buf_append_bytes(buf, "X", 1), EINVAL);
+	ck_assert_int_eq(buf_append_str(buf, "X"), EINVAL);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+	ck_assert_msg(!memcmp(get_buf_data(buf), contents, bytes),
+		      "a rejected append must not modify the mapped contents");
+
+	free_buf(buf);
+
+	/* a file that can not be opened must not be fatal */
+	ck_assert(create_mmap_buf(path) == NULL);
+}
+
+END_TEST
+
+START_TEST(test_shadow_buf_initializer)
+{
+	char data[] = "shadowed";
+	buf_t buf = SHADOW_BUF_INITIALIZER(data, (sizeof(data) - 1));
+
+	ck_assert(get_buf_data(&buf) == data);
+	ck_assert_int_eq(size_buf(&buf), (sizeof(data) - 1));
+	/*
+	 * Unlike create_shadow_buf(), the initializer marks the buffer as fully
+	 * populated so it is ready to be read back
+	 */
+	ck_assert_int_eq(get_buf_offset(&buf), (sizeof(data) - 1));
+	ck_assert_int_eq(remaining_buf(&buf), 0);
+
+	/* the shadowed memory must never be grown or written past */
+	ck_assert_int_eq(try_grow_buf(&buf, 1), EINVAL);
+	ck_assert_int_eq(try_grow_buf_remaining(&buf, 1), EINVAL);
+	ck_assert_int_eq(buf_append_bytes(&buf, "x", 1), EINVAL);
+	ck_assert_int_eq(buf_append_str(&buf, "x"), EINVAL);
+	ck_assert_int_eq(get_buf_offset(&buf), (sizeof(data) - 1));
+	ck_assert_str_eq(data, "shadowed");
+}
+
+END_TEST
+
+START_TEST(test_buf_macros)
+{
+	/*
+	 * Every macro argument below is an expression that only parses (or only
+	 * evaluates correctly) when the macro parenthesizes its parameters.
+	 * These are regression guards for the get_buf_data(), get_buf_offset(),
+	 * set_buf_offset(), remaining_buf() and size_buf() macros.
+	 */
+	char data[] = "shadowed";
+	buf_t stack_buf = SHADOW_BUF_INITIALIZER(data, (sizeof(data) - 1));
+	buf_t *buf = init_buf(64);
+	buf_t **buf_ptr = &buf;
+	void *ptr = buf;
+
+	ck_assert(get_buf_data((buf_t *) ptr) == get_buf_data(buf));
+	ck_assert_int_eq(get_buf_offset(*buf_ptr), 0);
+
+	set_buf_offset(*buf_ptr, (get_buf_offset(buf) + 8));
+	ck_assert_int_eq(get_buf_offset(buf), 8);
+
+	ck_assert_int_eq(remaining_buf(&stack_buf), 0);
+	ck_assert_int_eq(remaining_buf(buf ? buf : &stack_buf), (64 - 8));
+	ck_assert_int_eq(size_buf(buf ? buf : &stack_buf), 64);
+	ck_assert_int_eq(size_buf(&stack_buf), (sizeof(data) - 1));
+
+	free_buf(buf);
+}
+
+END_TEST
+
+START_TEST(test_grow_buf)
+{
+	buf_t *buf = init_buf(16);
+
+	ck_assert_int_eq(buf_append_str(buf, "0123456789abcdef"),
+			 SLURM_SUCCESS);
+	ck_assert_int_eq(remaining_buf(buf), 0);
+
+	/* grow_buf() grows by exactly the requested number of bytes */
+	grow_buf(buf, 16);
+	ck_assert_int_eq(size_buf(buf), 32);
+	ck_assert_int_eq(get_buf_offset(buf), 16);
+	ck_assert_int_eq(remaining_buf(buf), 16);
+	/* existing contents must survive the resize */
+	ck_assert_msg(!memcmp(get_buf_data(buf), "0123456789abcdef", 16),
+		      "grow_buf() did not preserve the contents");
+
+	free_buf(buf);
+}
+
+END_TEST
+
+START_TEST(test_try_grow_buf)
+{
+	buf_t *buf = init_buf(16);
+	/* buffer with no memory to avoid allocating MAX_BUF_SIZE bytes */
+	buf_t full_buf = {
+		.magic = BUF_MAGIC,
+		.size = MAX_BUF_SIZE,
+	};
+
+	/*
+	 * Growth is always at least BUF_SIZE to avoid a xrealloc() per append,
+	 * and requests of BUF_SIZE or larger are added on top of that
+	 */
+	ck_assert_int_eq(try_grow_buf(buf, 1), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), (16 + BUF_SIZE));
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+
+	ck_assert_int_eq(try_grow_buf(buf, (BUF_SIZE - 1)), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), (16 + (2 * BUF_SIZE)));
+
+	ck_assert_int_eq(try_grow_buf(buf, BUF_SIZE), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), (16 + (4 * BUF_SIZE)));
+
+	free_buf(buf);
+
+	/* growing past MAX_BUF_SIZE must be rejected without changing size */
+	ck_assert_int_eq(try_grow_buf(&full_buf, 1), ESLURM_DATA_TOO_LARGE);
+	ck_assert_int_eq(size_buf(&full_buf), MAX_BUF_SIZE);
+}
+
+END_TEST
+
+START_TEST(test_try_grow_buf_remaining)
+{
+	buf_t *buf = init_buf(BUF_SIZE);
+	/* buffer with no memory to avoid allocating MAX_BUF_SIZE bytes */
+	buf_t full_buf = {
+		.magic = BUF_MAGIC,
+		.size = MAX_BUF_SIZE,
+		.processed = MAX_BUF_SIZE,
+	};
+
+	/* an exact fit must not grow the buffer */
+	ck_assert_int_eq(try_grow_buf_remaining(buf, BUF_SIZE), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), BUF_SIZE);
+
+	set_buf_offset(buf, 1);
+	ck_assert_int_eq(remaining_buf(buf), (BUF_SIZE - 1));
+
+	/* one byte short must grow the buffer */
+	ck_assert_int_eq(try_grow_buf_remaining(buf, BUF_SIZE), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), (3 * BUF_SIZE));
+	/* growing must never move the read/write offset */
+	ck_assert_int_eq(get_buf_offset(buf), 1);
+
+	free_buf(buf);
+
+	ck_assert_int_eq(try_grow_buf_remaining(&full_buf, 1),
+			 ESLURM_DATA_TOO_LARGE);
+	ck_assert_int_eq(size_buf(&full_buf), MAX_BUF_SIZE);
+}
+
+END_TEST
+
+START_TEST(test_buf_append_bytes)
+{
+	const char bytes[] = { 'a', '\0', 'b', '\0', 'c' };
+	buf_t *buf = init_buf(16);
+
+	/* appending nothing is always a no-op success */
+	ck_assert_int_eq(buf_append_bytes(buf, NULL, 0), SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+	ck_assert_int_eq(size_buf(buf), 16);
+
+	/* appending is binary safe */
+	ck_assert_int_eq(buf_append_bytes(buf, bytes, sizeof(bytes)),
+			 SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), sizeof(bytes));
+	ck_assert_int_eq(size_buf(buf), 16);
+	ck_assert_msg(!memcmp(get_buf_data(buf), bytes, sizeof(bytes)),
+		      "appended bytes do not match the source");
+
+	/* appends are contiguous */
+	ck_assert_int_eq(buf_append_bytes(buf, bytes, sizeof(bytes)),
+			 SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), (2 * sizeof(bytes)));
+	ck_assert_msg(!memcmp((get_buf_data(buf) + sizeof(bytes)), bytes,
+			      sizeof(bytes)),
+		      "second append is not contiguous with the first");
+
+	free_buf(buf);
+}
+
+END_TEST
+
+START_TEST(test_buf_append_bytes_grow)
+{
+	char *chunk = xmalloc(BUF_SIZE);
+	buf_t *buf = init_buf(BUF_SIZE);
+
+	memset(chunk, 'a', BUF_SIZE);
+
+	/* an exact fit must not grow the buffer */
+	ck_assert_int_eq(buf_append_bytes(buf, chunk, BUF_SIZE), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), BUF_SIZE);
+	ck_assert_int_eq(get_buf_offset(buf), BUF_SIZE);
+	ck_assert_int_eq(remaining_buf(buf), 0);
+
+	/* a full buffer must grow to take another byte */
+	ck_assert_int_eq(buf_append_bytes(buf, "b", 1), SLURM_SUCCESS);
+	ck_assert_int_eq(size_buf(buf), (2 * BUF_SIZE));
+	ck_assert_int_eq(get_buf_offset(buf), (BUF_SIZE + 1));
+	/* contents must survive the implicit grow */
+	ck_assert_msg(!memcmp(get_buf_data(buf), chunk, BUF_SIZE),
+		      "buf_append_bytes() did not preserve the contents");
+	ck_assert_int_eq(get_buf_data(buf)[BUF_SIZE], 'b');
+
+	free_buf(buf);
+	xfree(chunk);
+}
+
+END_TEST
+
+START_TEST(test_buf_append_bytes_too_large)
+{
+	/* buffer with no memory to avoid allocating MAX_BUF_SIZE bytes */
+	buf_t full_buf = {
+		.magic = BUF_MAGIC,
+		.size = MAX_BUF_SIZE,
+		.processed = MAX_BUF_SIZE,
+	};
+
+	ck_assert_int_eq(buf_append_bytes(&full_buf, "x", 1),
+			 ESLURM_DATA_TOO_LARGE);
+	ck_assert_int_eq(buf_append_str(&full_buf, "x"), ESLURM_DATA_TOO_LARGE);
+	/* a failed append must not advance the offset */
+	ck_assert_int_eq(get_buf_offset(&full_buf), MAX_BUF_SIZE);
+
+	/*
+	 * A single request larger than MAX_BUF_SIZE must be rejected outright,
+	 * even against a small buffer with room to spare
+	 */
+	{
+		buf_t *buf = init_buf(16);
+
+		ck_assert_int_eq(buf_append_bytes(buf, NULL,
+						  ((size_t) MAX_BUF_SIZE) + 1),
+				 ESLURM_DATA_TOO_LARGE);
+		ck_assert_int_eq(get_buf_offset(buf), 0);
+		ck_assert_int_eq(size_buf(buf), 16);
+
+		free_buf(buf);
+	}
+
+	/*
+	 * try_grow_buf_remaining() takes a uint32_t, so a bytes count that is
+	 * itself a multiple of 2^32 truncates to 0 on the implicit conversion
+	 * if the "bytes > MAX_BUF_SIZE" check above is ever lost -- remaining
+	 * space then looks sufficient and the memcpy() below still copies the
+	 * full untruncated count. Only reachable where size_t is wider than
+	 * uint32_t; NULL is safe here because the guard must reject this
+	 * before ever touching ptr.
+	 */
+	if (sizeof(size_t) > sizeof(uint32_t)) {
+		buf_t *buf = init_buf(16);
+
+		ck_assert_int_eq(buf_append_bytes(buf, NULL,
+						  (((size_t) UINT32_MAX) + 1)),
+				 ESLURM_DATA_TOO_LARGE);
+		ck_assert_int_eq(get_buf_offset(buf), 0);
+		ck_assert_int_eq(size_buf(buf), 16);
+
+		free_buf(buf);
+	}
+}
+
+END_TEST
+
+START_TEST(test_buf_append_str)
+{
+	buf_t *buf = init_buf(16);
+
+	/* a NULL string is treated like an empty one, not a fatal error */
+	ck_assert_int_eq(buf_append_str(buf, NULL), SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+
+	/* appending an empty string is a no-op success */
+	ck_assert_int_eq(buf_append_str(buf, ""), SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), 0);
+
+	ck_assert_int_eq(buf_append_str(buf, "abc"), SLURM_SUCCESS);
+	/* the NUL terminator must not be appended */
+	ck_assert_int_eq(get_buf_offset(buf), 3);
+
+	ck_assert_int_eq(buf_append_str(buf, "def"), SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), 6);
+	ck_assert_msg(!memcmp(get_buf_data(buf), "abcdef", 6),
+		      "appended strings do not match the source");
+
+	ck_assert_int_eq(buf_append_str(buf, ""), SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), 6);
+
+	free_buf(buf);
+}
+
+END_TEST
+
+START_TEST(test_buf_append_str_grow)
+{
+	char *str = xmalloc(BUF_SIZE + 1);
+	buf_t *buf = init_buf(16);
+
+	memset(str, 'a', BUF_SIZE);
+
+	ck_assert_int_eq(buf_append_str(buf, str), SLURM_SUCCESS);
+	ck_assert_int_eq(get_buf_offset(buf), BUF_SIZE);
+	ck_assert_int_eq(size_buf(buf), (16 + (2 * BUF_SIZE)));
+	ck_assert_msg(!memcmp(get_buf_data(buf), str, BUF_SIZE),
+		      "buf_append_str() did not preserve the contents");
+
+	free_buf(buf);
+	xfree(str);
+}
+
+END_TEST
+
+START_TEST(test_assign_buf)
+{
+	char *data = xmalloc(64);
+	buf_t *buf = init_buf(16);
+
+	memcpy(data, "assigned", 8);
+
+	assign_buf(buf, &data, 8);
+	/* assign_buf() takes ownership of data */
+	ck_assert(data == NULL);
+	/* the buffer is sized by the allocation, not by the byte count */
+	ck_assert_int_eq(size_buf(buf), 64);
+	ck_assert_int_eq(get_buf_offset(buf), 8);
+	ck_assert_int_eq(remaining_buf(buf), (64 - 8));
+	ck_assert_msg(!memcmp(get_buf_data(buf), "assigned", 8),
+		      "assign_buf() did not preserve the contents");
+
+	free_buf(buf);
+}
+
+END_TEST
+
+START_TEST(test_xfer_buf_data)
+{
+	buf_t *buf = init_buf(16);
+	char *data;
+
+	ck_assert_int_eq(buf_append_str(buf, "xfer"), SLURM_SUCCESS);
+
+	data = xfer_buf_data(buf);
+	/* the buf_t is released and the caller now owns the data */
+	ck_assert(buf == NULL);
+	ck_assert(data != NULL);
+	ck_assert_msg(!memcmp(data, "xfer", 4),
+		      "xfer_buf_data() did not preserve the contents");
+
+	xfree(data);
+}
+
+END_TEST
+
+static Suite *suite_buf(void)
+{
+	Suite *s = suite_create("buf");
+	TCase *tc_core = tcase_create("buf");
+
+	tcase_add_test(tc_core, test_init_buf);
+	tcase_add_test(tc_core, test_try_init_buf);
+	tcase_add_test(tc_core, test_create_buf);
+	tcase_add_test(tc_core, test_create_shadow_buf);
+	tcase_add_test(tc_core, test_create_mmap_buf);
+	tcase_add_test(tc_core, test_shadow_buf_initializer);
+	tcase_add_test(tc_core, test_buf_macros);
+	tcase_add_test(tc_core, test_grow_buf);
+	tcase_add_test(tc_core, test_try_grow_buf);
+	tcase_add_test(tc_core, test_try_grow_buf_remaining);
+	tcase_add_test(tc_core, test_buf_append_bytes);
+	tcase_add_test(tc_core, test_buf_append_bytes_grow);
+	tcase_add_test(tc_core, test_buf_append_bytes_too_large);
+	tcase_add_test(tc_core, test_buf_append_str);
+	tcase_add_test(tc_core, test_buf_append_str_grow);
+	tcase_add_test(tc_core, test_assign_buf);
+	tcase_add_test(tc_core, test_xfer_buf_data);
+
+	suite_add_tcase(s, tc_core);
+	return s;
+}
+
+int main(void)
+{
+	int number_failed;
+	log_options_t log_opts = LOG_OPTS_INITIALIZER;
+	const char *debug_env = getenv("SLURM_DEBUG");
+	const char *debug_flags_env = getenv("SLURM_DEBUG_FLAGS");
+	SRunner *sr;
+
+	if (debug_env)
+		log_opts.stderr_level = log_string2num(debug_env);
+	if (debug_flags_env)
+		debug_str2flags(debug_flags_env, &slurm_conf.debug_flags);
+
+	log_init("buf-test", log_opts, 0, NULL);
+
+	sr = srunner_create(suite_buf());
+
+	srunner_run_all(sr, CK_ENV);
+	number_failed = srunner_ntests_failed(sr);
+	srunner_free(sr);
+
+	log_fini();
+	return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/testsuite/slurm_unit/common/xstring-test.c b/testsuite/slurm_unit/common/xstring-test.c
index 961800f..91ac9da 100644
--- a/testsuite/slurm_unit/common/xstring-test.c
+++ b/testsuite/slurm_unit/common/xstring-test.c
@@ -97,12 +97,64 @@
 }
 END_TEST
 
+START_TEST(test_try_xstrndup)
+{
+	char *str = NULL, *xstr = NULL;
+
+	ck_assert(try_xstrndup(NULL, 10) == NULL);
+
+	/* n larger than the string must copy the entire string */
+	str = try_xstrndup("abcdef", 10);
+	ck_assert(str != NULL);
+	ck_assert_str_eq(str, "abcdef");
+	/* the result is always NUL terminated */
+	ck_assert_int_eq(xsize(str), 7);
+	xfree(str);
+
+	/* n smaller than the string must truncate the copy */
+	str = try_xstrndup("abcdef", 3);
+	ck_assert(str != NULL);
+	ck_assert_str_eq(str, "abc");
+	ck_assert_int_eq(xsize(str), 4);
+	xfree(str);
+
+	/* copying must stop at the first NUL */
+	str = try_xstrndup("abc\0def", 7);
+	ck_assert(str != NULL);
+	ck_assert_str_eq(str, "abc");
+	ck_assert_int_eq(xsize(str), 4);
+	xfree(str);
+
+	/* an empty string must not return NULL */
+	str = try_xstrndup("", 10);
+	ck_assert(str != NULL);
+	ck_assert_str_eq(str, "");
+	xfree(str);
+
+	/* an n of zero must not return NULL */
+	str = try_xstrndup("abcdef", 0);
+	ck_assert(str != NULL);
+	ck_assert_str_eq(str, "");
+	xfree(str);
+
+	/* results must match xstrndup() */
+	str = try_xstrndup("abcdef", 3);
+	xstr = xstrndup("abcdef", 3);
+	ck_assert_str_eq(str, xstr);
+	ck_assert_int_eq(xsize(str), xsize(xstr));
+	xfree(str);
+	xfree(xstr);
+}
+
+END_TEST
+
 Suite *xstring_suite(void)
 {
 	Suite *s = suite_create("xstring");
 	TCase *tc_core = tcase_create("Core");
 	tcase_add_loop_test(tc_core, test_xstrtrim, 0 , sizeof(xstrtrim_data) /
 			    sizeof(xstrtrim_data_t) );
+	tcase_add_test(tc_core, test_try_xstrndup);
 	suite_add_tcase(s, tc_core);
 	return s;
 }