diff --git a/libretro-common/net/net_nfs3.c b/libretro-common/net/net_nfs3.c index 681db7c62e1a..8b80dc33b185 100644 --- a/libretro-common/net/net_nfs3.c +++ b/libretro-common/net/net_nfs3.c @@ -114,6 +114,7 @@ #define NFS4ERR_BADSESSION 10052 #define NFS4ERR_COMPLETE_ALREADY 10054 #define NFS4ERR_DEADSESSION 10078 +#define NFS4ERR_BADXDR 10036 #define NF4DIR 2 /* attribute bits: type(1) size(4) in word 0, time_modify(53) in word 1 */ #define FATTR4_W0_TYPE (1u << 1) @@ -315,19 +316,51 @@ static uint64_t xdr_get_u64(struct xdr *x) return (hi << 32) | xdr_get_u32(x); } -/* Returns a pointer into the buffer; *len the octet count. */ +/* Returns a pointer into the buffer; *len the octet count. The + * length and its pad are checked apart so a server-sent count near + * 2^32 cannot wrap the sum on 32-bit targets. */ static const uint8_t *xdr_get_opaque(struct xdr *x, size_t *len) { const uint8_t *d; size_t n = xdr_get_u32(x); size_t pad = (4 - (n & 3)) & 3; - if (x->fail || (size_t)(x->end - x->p) < n + pad) { x->fail = 1; *len = 0; return NULL; } + size_t avail; + if (x->fail || (avail = (size_t)(x->end - x->p)) < n || avail - n < pad) + { + x->fail = 1; + *len = 0; + return NULL; + } d = x->p; x->p += n + pad; *len = n; return d; } +/* @n fixed octets (a stateid, a verifier, a session id): copied to + * @dst when the reply holds them, else the cursor fails and @dst is + * zeroed. Every raw read goes through here or xdr_skip, so the cursor + * never passes the end. */ +static void xdr_get_fixed(struct xdr *x, void *dst, size_t n) +{ + if (x->fail || (size_t)(x->end - x->p) < n) + { + x->fail = 1; + memset(dst, 0, n); + return; + } + memcpy(dst, x->p, n); + x->p += n; +} + +static void xdr_skip(struct xdr *x, size_t n) +{ + if (x->fail || (size_t)(x->end - x->p) < n) + x->fail = 1; + else + x->p += n; +} + static int xdr_get_fh(struct xdr *x, struct rnfs_fh *fh) { size_t n; @@ -675,7 +708,7 @@ static uint32_t nfs4_sequence_res(struct xdr *r, struct rnfs_ctx *c, c->slot_seq[slot]--; return st; } - r->p += 16; /* sessionid */ + xdr_skip(r, 16); /* sessionid */ xdr_get_u32(r); xdr_get_u32(r); /* sequenceid, slotid */ xdr_get_u32(r); xdr_get_u32(r); /* highest, target highest */ xdr_get_u32(r); /* status flags */ @@ -762,11 +795,14 @@ static int nfs4_compound(struct rnfs_ctx *c, const uint8_t *ops, size_t ops_len, return reply->fail ? -1 : 0; } -/* Next result: its status, positioned at the body. */ +/* Next result: its status, positioned at the body. A reply that + * ended before it is a decode error, never NFS4_OK. */ static uint32_t nfs4_res(struct xdr *r) { + uint32_t st; xdr_get_u32(r); /* opnum */ - return xdr_get_u32(r); + st = xdr_get_u32(r); + return r->fail ? NFS4ERR_BADXDR : st; } static void nfs4_put_fh(struct xdr *x, const struct rnfs_fh *fh) @@ -794,7 +830,7 @@ static int nfs4_get_fattr(struct xdr *r, struct rnfs_stat *st) struct xdr a; if (n >= 1) w0 = xdr_get_u32(r); if (n >= 2) w1 = xdr_get_u32(r); - while (n > 2) { xdr_get_u32(r); n--; } + while (n > 2 && !r->fail) { xdr_get_u32(r); n--; } v = xdr_get_opaque(r, &vl); if (!v) return -1; @@ -908,7 +944,12 @@ static int nfs4_setclientid(struct rnfs_ctx *c) return -1; } clientid = xdr_get_u64(&r); - memcpy(verf, r.p, 8); + xdr_get_fixed(&r, verf, 8); + if (r.fail) + { + rnfs_err(c, "setclientid reply truncated"); + return -1; + } x.p = ops; x.fail = 0; xdr_u32(&x, OP_SETCLIENTID_CONFIRM); xdr_u64(&x, clientid); @@ -1004,7 +1045,7 @@ static int nfs4_session_setup(struct rnfs_ctx *c) rnfs_err(c, "create_session refused"); return -1; } - memcpy(c->sessionid, r.p, 16); r.p += 16; + xdr_get_fixed(&r, c->sessionid, 16); xdr_get_u32(&r); /* sequence */ xdr_get_u32(&r); /* flags */ xdr_get_u32(&r); /* fore: header padding */ @@ -1245,6 +1286,36 @@ static int nfs4_walk_once(struct rnfs_ctx *c, const char *path, struct rnfs_fh * return ret; } +/* A stateid in a reply, written to @stateid only when whole. */ +static int nfs4_get_stateid(struct xdr *r, uint8_t *stateid) +{ + uint8_t sid[16]; + xdr_get_fixed(r, sid, 16); + if (r->fail) + return -1; + memcpy(stateid, sid, 16); + return 0; +} + +/* OPEN4resok up to its delegation: stateid, change_info (atomic + + * 2 x u64), rflags, attrset. No delegation is asked for, so a granted + * one is refused. @stateid is written only for a whole result. */ +static int nfs4_open_res(struct xdr *r, uint8_t *stateid, uint32_t *rflags) +{ + uint8_t sid[16]; + uint32_t nb; + xdr_get_fixed(r, sid, 16); + xdr_get_u32(r); xdr_get_u64(r); xdr_get_u64(r); + *rflags = xdr_get_u32(r); + nb = xdr_get_u32(r); /* attrset */ + while (nb-- && !r->fail) + xdr_get_u32(r); + if (xdr_get_u32(r) != 0 || r->fail) /* delegation type */ + return -1; + memcpy(stateid, sid, 16); + return 0; +} + /* OPEN with create in @dir, then OPEN_CONFIRM when asked; the result * stateid is CLOSEd by rnfs_close. */ static int nfs4_open_create(struct rnfs_ctx *c, const struct rnfs_fh *dir, @@ -1254,7 +1325,6 @@ static int nfs4_open_create(struct rnfs_ctx *c, const struct rnfs_fh *dir, struct xdr x, r; uint32_t rflags; size_t sl; - const uint8_t *sid; x.p = ops; x.end = ops + sizeof(ops); x.fail = 0; nfs4_put_fh(&x, dir); @@ -1290,20 +1360,8 @@ static int nfs4_open_create(struct rnfs_ctx *c, const struct rnfs_fh *dir, return -1; } c->open_seq++; - /* stateid, change_info(atomic + 2 x u64), rflags, attrset, delegation */ - sid = r.p; r.p += 16; - xdr_get_u32(&r); xdr_get_u64(&r); xdr_get_u64(&r); - rflags = xdr_get_u32(&r); - { - uint32_t nb = xdr_get_u32(&r); - while (nb--) xdr_get_u32(&r); - } - { - uint32_t dt = xdr_get_u32(&r); /* delegation: none expected */ - if (dt != 0) - return -1; - } - memcpy(f->stateid, sid, 16); + if (nfs4_open_res(&r, f->stateid, &rflags) != 0) + return -1; if (nfs4_res(&r) != NFS3_OK || xdr_get_fh(&r, &f->fh) != 0) return -1; if (nfs4_res(&r) == NFS3_OK) @@ -1329,7 +1387,8 @@ static int nfs4_open_create(struct rnfs_ctx *c, const struct rnfs_fh *dir, return -1; } c->open_seq++; - memcpy(f->stateid, r.p, 16); + if (nfs4_get_stateid(&r, f->stateid) != 0) + return -1; } (void)sl; return 0; @@ -1372,15 +1431,7 @@ static int nfs4_open_reclaim(struct rnfs_ctx *c, struct rnfs_file *f) if (nfs4_res(&r) != NFS3_OK || (st = nfs4_res(&r)) != NFS3_OK) return -1; c->open_seq++; - memcpy(f->stateid, r.p, 16); r.p += 16; - xdr_get_u32(&r); xdr_get_u64(&r); xdr_get_u64(&r); /* change_info */ - rflags = xdr_get_u32(&r); - { - uint32_t nb = xdr_get_u32(&r); /* attrset */ - while (nb-- && !r.fail) - xdr_get_u32(&r); - } - if (r.fail || xdr_get_u32(&r) != 0) /* no delegation asked, none taken */ + if (nfs4_open_res(&r, f->stateid, &rflags) != 0) return -1; f->opened = 1; nfs4_state_link(c, f); @@ -1395,7 +1446,8 @@ static int nfs4_open_reclaim(struct rnfs_ctx *c, struct rnfs_file *f) || nfs4_res(&r) != NFS3_OK || nfs4_res(&r) != NFS3_OK) return -1; c->open_seq++; - memcpy(f->stateid, r.p, 16); + if (nfs4_get_stateid(&r, f->stateid) != 0) + return -1; } return 0; } @@ -1638,12 +1690,31 @@ static int nfs4_rename(struct rnfs_ctx *c, const char *from, const char *to) return 0; } +/* The rest of a READDIR reply at @r: its cookie verifier, then the + * entry list taken into d->buf as received. @d is left as it was for + * a reply cut short. */ +static int rnfs_readdir_take(struct rnfs_dir *d, struct xdr *r) +{ + uint8_t verf[8]; + size_t n; + xdr_get_fixed(r, verf, 8); + if (r->fail) + return -1; + memcpy(d->cookieverf, verf, 8); + n = (size_t)(r->end - r->p); + if (n > NFS3_MAX_IO) + n = NFS3_MAX_IO; + memcpy(d->buf, r->p, n); + d->buf_len = n; + d->buf_off = 0; + return 0; +} + /* One READDIR page into d->buf: entries of cookie, name, fattr4. */ static int nfs4_readdir_fill(struct rnfs_ctx *c, struct rnfs_dir *d) { uint8_t ops[NFS3_FHSIZE + 64]; struct xdr x, r; - size_t n; x.p = ops; x.end = ops + sizeof(ops); x.fail = 0; nfs4_put_fh(&x, &d->fh); xdr_u32(&x, OP_READDIR); @@ -1659,14 +1730,7 @@ static int nfs4_readdir_fill(struct rnfs_ctx *c, struct rnfs_dir *d) rnfs_err(c, "readdir failed"); return -1; } - memcpy(d->cookieverf, r.p, 8); r.p += 8; - n = (size_t)(r.end - r.p); - if (n > NFS3_MAX_IO) - n = NFS3_MAX_IO; - memcpy(d->buf, r.p, n); - d->buf_len = n; - d->buf_off = 0; - return 0; + return rnfs_readdir_take(d, &r); } struct rnfs_ctx *rnfs_new(void) @@ -2828,7 +2892,6 @@ static int rnfs_readdir_fill(struct rnfs_ctx *c, struct rnfs_dir *d) uint8_t args[NFS3_FHSIZE + 32]; struct xdr x, reply; struct rnfs_stat st; - size_t n; for (;;) { @@ -2855,14 +2918,7 @@ static int rnfs_readdir_fill(struct rnfs_ctx *c, struct rnfs_dir *d) return -1; } xdr_get_post_op_attr(&reply, &st); - memcpy(d->cookieverf, reply.p, 8); reply.p += 8; - n = (size_t)(reply.end - reply.p); - if (n > NFS3_MAX_IO) - n = NFS3_MAX_IO; - memcpy(d->buf, reply.p, n); - d->buf_len = n; - d->buf_off = 0; - return 0; + return rnfs_readdir_take(d, &reply); } /* Attributes of one entry by LOOKUP, for the plain READDIR path. */ diff --git a/samples/network/nfs_retro/Makefile b/samples/network/nfs_retro/Makefile index 7dc0b808a27e..164fff29aec0 100644 --- a/samples/network/nfs_retro/Makefile +++ b/samples/network/nfs_retro/Makefile @@ -11,13 +11,17 @@ else # file mapping as the shipping build has it (Win32 maps without it) MMAP_FLAGS = -DHAVE_MMAP endif -all: nfs_test nfs_sparse_test nfs_idle_test vfs_test vfs_threads_test +all: nfs_test nfs_sparse_test nfs_idle_test nfs_reply_bounds_test vfs_test vfs_threads_test nfs_test: $(SRC) $(CC) $(CPPFLAGS) $(CFLAGS) $(SRC) $(LDLIBS) -o $@ SPARSE_SRC = nfs_sparse_test.c $(filter-out nfs_test.c,$(SRC)) nfs_sparse_test: $(SPARSE_SRC) $(CC) $(CPPFLAGS) $(CFLAGS) $(SPARSE_SRC) $(LDLIBS) -o $@ +# includes net_nfs3.c itself to reach its static reply decoders +BOUNDS_SRC = nfs_reply_bounds_test.c $(filter-out nfs_test.c $(COMMON)/net/net_nfs3.c,$(SRC)) +nfs_reply_bounds_test: $(BOUNDS_SRC) $(COMMON)/net/net_nfs3.c + $(CC) $(CPPFLAGS) $(CFLAGS) $(BOUNDS_SRC) $(LDLIBS) -o $@ IDLE_SRC = nfs_idle_test.c $(filter-out nfs_test.c,$(SRC)) nfs_idle_test: $(IDLE_SRC) $(CC) $(CPPFLAGS) $(CFLAGS) $(IDLE_SRC) $(LDLIBS) -o $@ @@ -32,5 +36,5 @@ THR_SRC = vfs_threads_test.c $(filter-out vfs_test.c,$(VFS_SRC)) $(COMMON)/rthre vfs_threads_test: $(THR_SRC) $(CC) $(CPPFLAGS) -DHAVE_NFSCLIENT -DHAVE_THREADS $(MMAP_FLAGS) -I$(ROOT) $(CFLAGS) $(THR_SRC) $(LDLIBS) -lpthread -o $@ clean: - rm -f nfs_test nfs_test.exe nfs_sparse_test nfs_sparse_test.exe nfs_idle_test nfs_idle_test.exe vfs_test vfs_test.exe vfs_threads_test vfs_threads_test.exe + rm -f nfs_test nfs_test.exe nfs_sparse_test nfs_sparse_test.exe nfs_idle_test nfs_idle_test.exe nfs_reply_bounds_test nfs_reply_bounds_test.exe vfs_test vfs_test.exe vfs_threads_test vfs_threads_test.exe .PHONY: all clean diff --git a/samples/network/nfs_retro/local_unfs3_test.sh b/samples/network/nfs_retro/local_unfs3_test.sh index 4681b31d2e28..d31f16c5357c 100755 --- a/samples/network/nfs_retro/local_unfs3_test.sh +++ b/samples/network/nfs_retro/local_unfs3_test.sh @@ -7,6 +7,9 @@ # skips, and passes. set -e cd "$(dirname "$0")" +# reply parsing against short and hostile replies: needs no server +make -s nfs_reply_bounds_test +${RUNNER:-} ./nfs_reply_bounds_test${EXE:-} UNFSD=$(command -v unfsd || true) if [ -z "$UNFSD" ]; then if command -v git >/dev/null 2>&1 && command -v flex >/dev/null 2>&1 && command -v bison >/dev/null 2>&1; then diff --git a/samples/network/nfs_retro/nfs_reply_bounds_test.c b/samples/network/nfs_retro/nfs_reply_bounds_test.c new file mode 100644 index 000000000000..2dabedaa0c97 --- /dev/null +++ b/samples/network/nfs_retro/nfs_reply_bounds_test.c @@ -0,0 +1,266 @@ +/* net_nfs3.c's reply parsers against short and hostile replies, with + * no server: the file is included so its static decoders are called + * on crafted buffers. Each reply sits at the very end of its own + * allocation, so a read past it is caught under ASan, and the walks + * over server-sent counts are held to a CPU-time bound. */ +#include "../../../libretro-common/net/net_nfs3.c" + +#include +#include + +static int failures; + +#define CHECK(cond, what) do { if (!(cond)) { \ + printf("[FAIL] %s (line %d)\n", what, __LINE__); failures++; } } while (0) + +/* A reply filled into a buffer of exactly its size, so the octet + * after its end is outside the allocation. */ +struct reply +{ + uint8_t *mem; + size_t len; + size_t fill; +}; + +static void reply_init(struct reply *r, size_t len) +{ + r->mem = (uint8_t*)malloc(len ? len : 1); + r->len = len; + r->fill = 0; +} + +static void reply_u32(struct reply *r, uint32_t v) +{ + if (r->len - r->fill < 4) + return; + r->mem[r->fill++] = (uint8_t)(v >> 24); + r->mem[r->fill++] = (uint8_t)(v >> 16); + r->mem[r->fill++] = (uint8_t)(v >> 8); + r->mem[r->fill++] = (uint8_t)v; +} + +static void reply_bytes(struct reply *r, uint8_t v, size_t n) +{ + while (n-- && r->fill < r->len) + r->mem[r->fill++] = v; +} + +static void reply_xdr(struct reply *r, struct xdr *x) +{ + x->p = r->mem; + x->end = r->mem + r->len; + x->fail = 0; +} + +/* a walk bounded by the bytes received finishes at once; one that + * spins on a count of 2^32 takes seconds */ +#define CPU_BOUND (CLOCKS_PER_SEC / 4) + +static void test_nfs4_res(void) +{ + struct reply r; + struct xdr x; + reply_init(&r, 4); + reply_u32(&r, OP_GETATTR); + reply_xdr(&r, &x); + CHECK(nfs4_res(&x) != NFS3_OK, "result cut after its opnum is not NFS4_OK"); + CHECK(nfs4_res(&x) != NFS3_OK, "result past the end is not NFS4_OK"); + free(r.mem); +} + +static void test_fattr(void) +{ + struct reply r; + struct xdr x; + struct rnfs_stat st; + clock_t t; + int ret; + + /* bitmap count of 2^32 - 1 in a reply of three words */ + reply_init(&r, 12); + reply_u32(&r, 0xFFFFFFFFu); + reply_u32(&r, FATTR4_W0_TYPE); + reply_u32(&r, 0); + reply_xdr(&r, &x); + t = clock(); + ret = nfs4_get_fattr(&x, &st); + t = clock() - t; + CHECK(ret == -1, "fattr4 with a bitmap count past the reply fails"); + CHECK(t < CPU_BOUND, "fattr4 bitmap walk stops at the end of the reply"); + free(r.mem); + + /* a whole one: dir, size, mtime */ + reply_init(&r, 4 * 3 + 4 + 24); + reply_u32(&r, 2); + reply_u32(&r, FATTR4_W0_TYPE | FATTR4_W0_SIZE); + reply_u32(&r, FATTR4_W1_TIME_MODIFY); + reply_u32(&r, 24); + reply_u32(&r, NF4DIR); + reply_u32(&r, 0); reply_u32(&r, 0x1234); + reply_u32(&r, 0); reply_u32(&r, 77); reply_u32(&r, 0); + reply_xdr(&r, &x); + CHECK(nfs4_get_fattr(&x, &st) == 0, "whole fattr4 decodes"); + CHECK(st.is_dir && st.size == 0x1234 && st.mtime == 77, "fattr4 values"); + free(r.mem); +} + +/* OPEN4resok: stateid, change_info, rflags, attrset, delegation */ +static void open_head(struct reply *r) +{ + reply_bytes(r, 0x5A, 16); + reply_u32(r, 1); reply_u32(r, 0); reply_u32(r, 1); reply_u32(r, 0); reply_u32(r, 2); + reply_u32(r, 2); /* rflags: CONFIRM */ +} + +static void test_open_res(void) +{ + struct reply r; + struct xdr x; + uint8_t sid[16], keep[16]; + uint32_t rflags = 0; + clock_t t; + int ret; + memset(keep, 0xAA, sizeof(keep)); + + /* attrset count of 2^32 - 1, reply ends after it */ + reply_init(&r, 16 + 24 + 4); + open_head(&r); + reply_u32(&r, 0xFFFFFFFFu); + reply_xdr(&r, &x); + memcpy(sid, keep, 16); + t = clock(); + ret = nfs4_open_res(&x, sid, &rflags); + t = clock() - t; + CHECK(ret == -1, "OPEN with an attrset count past the reply fails"); + CHECK(t < CPU_BOUND, "OPEN attrset walk stops at the end of the reply"); + CHECK(!memcmp(sid, keep, 16), "failed OPEN leaves the stateid alone"); + free(r.mem); + + /* reply ends where the delegation type should be */ + reply_init(&r, 16 + 24 + 8); + open_head(&r); + reply_u32(&r, 1); reply_u32(&r, 0); + reply_xdr(&r, &x); + memcpy(sid, keep, 16); + CHECK(nfs4_open_res(&x, sid, &rflags) == -1, "OPEN cut before its delegation fails"); + CHECK(!memcmp(sid, keep, 16), "OPEN cut before its delegation leaves the stateid alone"); + free(r.mem); + + /* reply ends inside the stateid */ + reply_init(&r, 8); + reply_bytes(&r, 0x5A, 8); + reply_xdr(&r, &x); + memcpy(sid, keep, 16); + CHECK(nfs4_open_res(&x, sid, &rflags) == -1, "OPEN cut inside its stateid fails"); + CHECK(!memcmp(sid, keep, 16), "OPEN cut inside its stateid leaves the stateid alone"); + free(r.mem); + + /* a delegation granted though none was asked for */ + reply_init(&r, 16 + 24 + 8); + open_head(&r); + reply_u32(&r, 0); reply_u32(&r, 1); + reply_xdr(&r, &x); + CHECK(nfs4_open_res(&x, sid, &rflags) == -1, "OPEN with a delegation is refused"); + free(r.mem); + + /* whole */ + reply_init(&r, 16 + 24 + 12); + open_head(&r); + reply_u32(&r, 1); reply_u32(&r, 0); reply_u32(&r, 0); + reply_xdr(&r, &x); + memcpy(sid, keep, 16); + CHECK(nfs4_open_res(&x, sid, &rflags) == 0, "whole OPEN result decodes"); + CHECK(sid[0] == 0x5A && sid[15] == 0x5A && rflags == 2, "OPEN stateid and rflags"); + CHECK(x.p == x.end, "OPEN result consumed exactly"); + free(r.mem); + + /* OPEN_CONFIRM's stateid, cut short */ + reply_init(&r, 12); + reply_bytes(&r, 0x33, 12); + reply_xdr(&r, &x); + memcpy(sid, keep, 16); + CHECK(nfs4_get_stateid(&x, sid) == -1, "stateid cut short fails"); + CHECK(!memcmp(sid, keep, 16), "stateid cut short leaves the stateid alone"); + free(r.mem); +} + +static void test_readdir_take(void) +{ + struct reply r; + struct xdr x; + struct rnfs_dir d; + memset(&d, 0, sizeof(d)); + d.buf = (uint8_t*)malloc(NFS3_MAX_IO); + d.buf_len = 99; + memset(d.cookieverf, 0x77, sizeof(d.cookieverf)); + + /* reply ends inside the cookie verifier */ + reply_init(&r, 4); + reply_bytes(&r, 0x11, 4); + reply_xdr(&r, &x); + CHECK(rnfs_readdir_take(&d, &x) == -1, "READDIR cut inside its verifier fails"); + CHECK(d.buf_len == 99 && d.cookieverf[0] == 0x77, + "failed READDIR leaves the page and verifier alone"); + free(r.mem); + + /* reply already failed before the verifier (status past the end) */ + reply_init(&r, 0); + reply_xdr(&r, &x); + xdr_get_u32(&x); + CHECK(rnfs_readdir_take(&d, &x) == -1, "READDIR after a failed cursor fails"); + free(r.mem); + + /* verifier and an entry list */ + reply_init(&r, 8 + 12); + reply_bytes(&r, 0x22, 8); + reply_u32(&r, 1); reply_u32(&r, 2); reply_u32(&r, 3); + reply_xdr(&r, &x); + CHECK(rnfs_readdir_take(&d, &x) == 0, "whole READDIR page is taken"); + CHECK(d.cookieverf[0] == 0x22 && d.cookieverf[7] == 0x22, "READDIR verifier"); + CHECK(d.buf_len == 12 && d.buf_off == 0 && d.buf[11] == 3, "READDIR entry list"); + free(r.mem); + free(d.buf); +} + +static void test_opaque_and_sequence(void) +{ + static struct rnfs_ctx c; + struct reply r; + struct xdr x; + size_t len = 1; + + /* a length near 2^32 whose pad would wrap a 32-bit sum */ + reply_init(&r, 8); + reply_u32(&r, 0xFFFFFFFDu); + reply_u32(&r, 0); + reply_xdr(&r, &x); + CHECK(xdr_get_opaque(&x, &len) == NULL && x.fail && len == 0, + "opaque longer than the reply fails"); + free(r.mem); + + /* SEQUENCE answered OK, reply ends inside its session id */ + reply_init(&r, 8 + 8); + reply_u32(&r, OP_SEQUENCE); + reply_u32(&r, NFS3_OK); + reply_bytes(&r, 0, 8); + reply_xdr(&r, &x); + CHECK(nfs4_sequence_res(&x, &c, 0) != NFS3_OK, "SEQUENCE cut inside its session id fails"); + CHECK(x.p <= x.end, "cursor stays inside the reply"); + free(r.mem); +} + +int main(void) +{ + test_nfs4_res(); + test_fattr(); + test_open_res(); + test_readdir_take(); + test_opaque_and_sequence(); + if (failures) + { + printf("nfs_reply_bounds_test: %d failure(s)\n", failures); + return 1; + } + printf("[pass] nfs_reply_bounds_test\n"); + return 0; +}