libc: add link_ntoa_r()

this is a re-entrant version of link_ntoa.  use an in-out parameter for
the buffer size, so the user requires at most two calls to determine the
needed size.

reimplement link_ntoa using link_ntoa_r with a static buffer.

Reviewed by:	des
Approved by:	des (mentor)
Differential Revision:	https://reviews.freebsd.org/D50202
This commit is contained in:
Lexi Winter 2025-05-07 10:34:08 +01:00
parent 6f2b1b56ac
commit da509c2908
6 changed files with 247 additions and 29 deletions

View file

@ -141,7 +141,8 @@ MLINKS+=inet6_opt_init.3 inet6_opt_append.3 \
inet6_rthdr_space.3 inet6_rthdr_reverse.3 \
inet6_rthdr_space.3 inet6_rthdr_segments.3
MLINKS+=linkaddr.3 link_addr.3 \
linkaddr.3 link_ntoa.3
linkaddr.3 link_ntoa.3 \
linkaddr.3 link_ntoa_r.3
MLINKS+=rcmd.3 iruserok.3 \
rcmd.3 iruserok_sa.3 \
rcmd.3 rcmd_af.3 \

View file

@ -138,6 +138,10 @@ FBSD_1.3 {
sctp_sendv;
};
FBSD_1.8 {
link_ntoa_r;
};
FBSDprivate_1.0 {
_nsdispatch;
_nsyyerror; /* generated from nslexer.l */

View file

@ -28,12 +28,13 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd February 28, 2007
.Dd May 7, 2025
.Dt LINK_ADDR 3
.Os
.Sh NAME
.Nm link_addr ,
.Nm link_ntoa
.Nm link_ntoa ,
.Nm link_ntoa_r
.Nd elementary address specification routines for link level access
.Sh LIBRARY
.Lb libc
@ -45,12 +46,15 @@
.Fn link_addr "const char *addr" "struct sockaddr_dl *sdl"
.Ft char *
.Fn link_ntoa "const struct sockaddr_dl *sdl"
.Ft int
.Fn link_ntoa_r "const struct sockaddr_dl *sdl" "char *obuf" "size_t *buflen"
.Sh DESCRIPTION
The routine
.Fn link_addr
interprets character strings representing
link-level addresses, returning binary information suitable
for use in system calls.
.Pp
The routine
.Fn link_ntoa
takes
@ -60,9 +64,34 @@ address and returns an
string representing some of the information present,
including the link level address itself, and the interface name
or number, if present.
The returned string is stored in a static buffer.
This facility is experimental and is
still subject to change.
.Pp
The routine
.Fn link_ntoa_r
behaves like
.Fn link_ntoa ,
except the string is placed in the provided buffer instead of a static
buffer.
The caller should initialize
.Fa buflen
to the number of bytes available in
.Fa obuf .
On return,
.Fa buflen
is set to the actual number of bytes required for the output buffer,
including the NUL terminator.
If
.Fa obuf
is NULL, then
.Fa buflen
is set as described, but nothing is written.
This may be used to determine the required length of the buffer before
calling
.Fn link_ntoa_r
a second time.
.Pp
For
.Fn link_addr ,
the string
@ -94,6 +123,14 @@ The
.Fn link_ntoa
function
always returns a null terminated string.
.Pp
The
.Fn link_ntoa_r
function returns 0 on success, or -1 if the provided buffer was not
large enough; in the latter case, the contents of the buffer are
indeterminate, but a trailing NUL will always be written if the buffer
was at least one byte in size.
.Pp
The
.Fn link_addr
function
@ -109,6 +146,10 @@ and
.Fn link_ntoa
functions appeared in
.Bx 4.3 Reno .
The
.Fn link_ntoa_r
function appeared in
.Fx 15.0 .
.Sh BUGS
The returned values for link_ntoa
reside in a static memory area.

View file

@ -31,8 +31,12 @@
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <assert.h>
#include <stdint.h>
#include <string.h>
/* States*/
@ -113,53 +117,109 @@ link_addr(const char *addr, struct sockaddr_dl *sdl)
return;
}
static const char hexlist[] = "0123456789abcdef";
char *
link_ntoa(const struct sockaddr_dl *sdl)
{
static char obuf[64];
size_t buflen;
_Static_assert(sizeof(obuf) >= IFNAMSIZ + 20, "obuf is too small");
/*
* Ignoring the return value of link_ntoa_r() is safe here because it
* always writes the terminating NUL. This preserves the traditional
* behaviour of link_ntoa().
*/
buflen = sizeof(obuf);
(void)link_ntoa_r(sdl, obuf, &buflen);
return obuf;
}
int
link_ntoa_r(const struct sockaddr_dl *sdl, char *obuf, size_t *buflen)
{
static const char hexlist[] = "0123456789abcdef";
char *out;
const u_char *in, *inlim;
int namelen, i, rem;
size_t needed;
assert(sdl);
assert(buflen);
/* obuf may be null */
needed = 1; /* 1 for the NUL */
out = obuf;
if (obuf)
rem = *buflen;
else
rem = 0;
/*
* Check if at least n bytes are available in the output buffer, plus 1 for the
* trailing NUL. If not, set rem = 0 so we stop writing.
* Either way, increment needed by the amount we would have written.
*/
#define CHECK(n) do { \
if ((SIZE_MAX - (n)) >= needed) \
needed += (n); \
if (rem >= ((n) + 1)) \
rem -= (n); \
else \
rem = 0; \
} while (0)
/*
* Write the char c to the output buffer, unless the buffer is full.
* Note that if obuf is NULL, rem is always zero.
*/
#define OUT(c) do { \
if (rem > 0) \
*out++ = (c); \
} while (0)
namelen = (sdl->sdl_nlen <= IFNAMSIZ) ? sdl->sdl_nlen : IFNAMSIZ;
out = obuf;
rem = sizeof(obuf);
if (namelen > 0) {
bcopy(sdl->sdl_data, out, namelen);
out += namelen;
rem -= namelen;
CHECK(namelen);
if (rem > 0) {
bcopy(sdl->sdl_data, out, namelen);
out += namelen;
}
if (sdl->sdl_alen > 0) {
*out++ = ':';
rem--;
CHECK(1);
OUT(':');
}
}
in = (const u_char *)sdl->sdl_data + sdl->sdl_nlen;
in = (const u_char *)LLADDR(sdl);
inlim = in + sdl->sdl_alen;
while (in < inlim && rem > 1) {
if (in != (const u_char *)sdl->sdl_data + sdl->sdl_nlen) {
*out++ = '.';
rem--;
while (in < inlim) {
if (in != (const u_char *)LLADDR(sdl)) {
CHECK(1);
OUT('.');
}
i = *in++;
if (i > 0xf) {
if (rem < 3)
break;
*out++ = hexlist[i >> 4];
*out++ = hexlist[i & 0xf];
rem -= 2;
CHECK(2);
OUT(hexlist[i >> 4]);
OUT(hexlist[i & 0xf]);
} else {
if (rem < 2)
break;
*out++ = hexlist[i];
rem--;
CHECK(1);
OUT(hexlist[i]);
}
}
*out = 0;
return (obuf);
#undef CHECK
#undef OUT
/*
* We always leave enough room for the NUL if possible, but the user
* might have passed a NULL or zero-length buffer.
*/
if (out && *buflen)
*out = '\0';
*buflen = needed;
return ((rem > 0) ? 0 : -1);
}

View file

@ -26,6 +26,7 @@
#include <net/if_dl.h>
#include <format>
#include <iostream>
#include <ranges>
#include <span>
#include <utility>
@ -262,10 +263,120 @@ ATF_TEST_CASE_BODY(overlong)
::link_ntoa(&sdl));
}
/*
* Test link_ntoa_r, the re-entrant version of link_ntoa().
*/
ATF_TEST_CASE_WITHOUT_HEAD(link_ntoa_r)
ATF_TEST_CASE_BODY(link_ntoa_r)
{
static constexpr char garbage = 0x41;
std::vector<char> buf;
sockaddr_dl sdl;
size_t len;
int ret;
// Return the contents of buf as a string, using the NUL terminator to
// determine length. This is to ensure we're using the return value in
// the same way C code would, but we do a bit more verification to
// elicit a test failure rather than a SEGV if it's broken.
auto bufstr = [&buf]() -> std::string_view {
// Find the NUL.
auto end = std::ranges::find(buf, '\0');
ATF_REQUIRE(end != buf.end());
// Intentionally chopping the NUL off.
return {begin(buf), end};
};
// Resize the buffer and set the contents to a known garbage value, so
// we don't accidentally have a NUL in the right place when link_ntoa_r
// didn't put it there.
auto resetbuf = [&buf, &len](std::size_t size) {
len = size;
buf.resize(len);
std::ranges::fill(buf, garbage);
};
// Test a short address with a large buffer.
sdl = make_linkaddr("ix0:1.2.3");
resetbuf(64);
ret = ::link_ntoa_r(&sdl, &buf[0], &len);
ATF_REQUIRE_EQ(0, ret);
ATF_REQUIRE_EQ(10, len);
ATF_REQUIRE_EQ("ix0:1.2.3"s, bufstr());
// Test a buffer which is exactly the right size.
sdl = make_linkaddr("ix0:1.2.3");
resetbuf(10);
ret = ::link_ntoa_r(&sdl, &buf[0], &len);
ATF_REQUIRE_EQ(0, ret);
ATF_REQUIRE_EQ(10, len);
ATF_REQUIRE_EQ("ix0:1.2.3"sv, bufstr());
// Test various short buffers, using a table of buffer length and the
// output we expect. All of these should produce valid but truncated
// strings, with a trailing NUL and with buflen set correctly.
auto buftests = std::vector<std::pair<std::size_t, std::string_view>>{
{1u, ""sv},
{2u, ""sv},
{3u, ""sv},
{4u, "ix0"sv},
{5u, "ix0:"sv},
{6u, "ix0:1"sv},
{7u, "ix0:1."sv},
{8u, "ix0:1.2"sv},
{9u, "ix0:1.2."sv},
};
for (auto const &[buflen, expected] : buftests) {
sdl = make_linkaddr("ix0:1.2.3");
resetbuf(buflen);
ret = ::link_ntoa_r(&sdl, &buf[0], &len);
ATF_REQUIRE_EQ(-1, ret);
ATF_REQUIRE_EQ(10, len);
ATF_REQUIRE_EQ(expected, bufstr());
}
// Test a NULL buffer, which should just set buflen.
sdl = make_linkaddr("ix0:1.2.3");
len = 0;
ret = ::link_ntoa_r(&sdl, NULL, &len);
ATF_REQUIRE_EQ(-1, ret);
ATF_REQUIRE_EQ(10, len);
// A NULL buffer with a non-zero length should also be accepted.
sdl = make_linkaddr("ix0:1.2.3");
len = 64;
ret = ::link_ntoa_r(&sdl, NULL, &len);
ATF_REQUIRE_EQ(-1, ret);
ATF_REQUIRE_EQ(10, len);
// Test a non-NULL buffer, but with a length of zero.
sdl = make_linkaddr("ix0:1.2.3");
resetbuf(1);
len = 0;
ret = ::link_ntoa_r(&sdl, &buf[0], &len);
ATF_REQUIRE_EQ(-1, ret);
ATF_REQUIRE_EQ(10, len);
// Check we really didn't write anything.
ATF_REQUIRE_EQ(garbage, buf[0]);
// Test a buffer which would be truncated in the middle of a two-digit
// hex octet, which should not write the truncated octet at all.
sdl = make_linkaddr("ix0:1.22.3");
resetbuf(8);
ret = ::link_ntoa_r(&sdl, &buf[0], &len);
ATF_REQUIRE_EQ(-1, ret);
ATF_REQUIRE_EQ(11, len);
ATF_REQUIRE_EQ("ix0:1."sv, bufstr());
}
ATF_INIT_TEST_CASES(tcs)
{
ATF_ADD_TEST_CASE(tcs, basic);
ATF_ADD_TEST_CASE(tcs, ifname);
ATF_ADD_TEST_CASE(tcs, nonether);
ATF_ADD_TEST_CASE(tcs, overlong);
ATF_ADD_TEST_CASE(tcs, link_ntoa_r);
}

View file

@ -84,6 +84,7 @@ struct sockaddr_dl *link_init_sdl(struct ifnet *, struct sockaddr *, u_char);
__BEGIN_DECLS
void link_addr(const char *, struct sockaddr_dl *);
char *link_ntoa(const struct sockaddr_dl *);
int link_ntoa_r(const struct sockaddr_dl *, char *, size_t *);
__END_DECLS
#endif /* !_KERNEL */