summaryrefslogtreecommitdiffstats
path: root/fs/tftp.c
diff options
context:
space:
mode:
Diffstat (limited to 'fs/tftp.c')
-rw-r--r--fs/tftp.c786
1 files changed, 687 insertions, 99 deletions
diff --git a/fs/tftp.c b/fs/tftp.c
index d186e7983a..e0886c49d2 100644
--- a/fs/tftp.c
+++ b/fs/tftp.c
@@ -26,12 +26,18 @@
#include <libgen.h>
#include <fcntl.h>
#include <getopt.h>
+#include <globalvar.h>
#include <init.h>
+#include <linux/bitmap.h>
#include <linux/stat.h>
#include <linux/err.h>
+#include <linux/kernel.h>
#include <kfifo.h>
+#include <parseopt.h>
#include <linux/sizes.h>
+#include "tftp-selftest.h"
+
#define TFTP_PORT 69 /* Well known TFTP port number */
/* Seconds to wait before remote server is allowed to resend a lost packet */
@@ -58,21 +64,69 @@
#define STATE_WRQ 2
#define STATE_RDATA 3
#define STATE_WDATA 4
-#define STATE_OACK 5
+/* OACK from server has been received and we can begin to sent either the ACK
+ (for RRQ) or data (for WRQ) */
+#define STATE_START 5
#define STATE_WAITACK 6
#define STATE_LAST 7
#define STATE_DONE 8
#define TFTP_BLOCK_SIZE 512 /* default TFTP block size */
-#define TFTP_FIFO_SIZE 4096
+#define TFTP_MTU_SIZE 1432 /* MTU based block size */
+#define TFTP_MAX_WINDOW_SIZE CONFIG_FS_TFTP_MAX_WINDOW_SIZE
+
+/* allocate this number of blocks more than needed in the fifo */
+#define TFTP_EXTRA_BLOCKS 2
+
+/* size of cache which deals with udp reordering */
+#define TFTP_WINDOW_CACHE_NUM CONFIG_FS_TFTP_REORDER_CACHE_SIZE
+
+/* marker for an emtpy 'tftp_cache' */
+#define TFTP_CACHE_NO_ID (-1)
#define TFTP_ERR_RESEND 1
+#if defined(DEBUG) || IS_ENABLED(CONFIG_SELFTEST_TFTP)
+# define debug_assert(_cond) BUG_ON(!(_cond))
+#else
+# define debug_assert(_cond) do { \
+ if (!(_cond)) \
+ __builtin_unreachable(); \
+ } while (0)
+#endif
+
+static int g_tftp_window_size = DIV_ROUND_UP(TFTP_MAX_WINDOW_SIZE, 2);
+
+struct tftp_block {
+ uint16_t id;
+ uint16_t len;
+
+ struct list_head head;
+ uint8_t data[];
+};
+
+struct tftp_cache {
+ /* The id located at @pos or TFTP_CACHE_NO_ID when cache is empty. It
+ is possible that the corresponding bit in @used is NOT set. */
+ unsigned int id;
+ unsigned int pos;
+
+ /* bitmask */
+ unsigned long used[BITS_TO_LONGS(TFTP_WINDOW_CACHE_NUM)];
+
+ /* size of the cache; is limited by TFTP_WINDOW_CACHE_NUM and the
+ actual window size */
+ unsigned int size;
+ unsigned int block_len;
+ struct tftp_block *blocks[TFTP_WINDOW_CACHE_NUM];
+};
+
struct file_priv {
struct net_connection *tftp_con;
int push;
uint16_t block;
uint16_t last_block;
+ uint16_t ack_block;
int state;
int err;
char *filename;
@@ -82,28 +136,239 @@ struct file_priv {
struct kfifo *fifo;
void *buf;
int blocksize;
- int block_requested;
+ unsigned int windowsize;
+ bool is_getattr;
+ struct tftp_cache cache;
};
struct tftp_priv {
IPaddr_t server;
};
+static inline bool is_block_before(uint16_t a, uint16_t b)
+{
+ return (int16_t)(b - a) > 0;
+}
+
+static inline uint16_t get_block_delta(uint16_t a, uint16_t b)
+{
+ debug_assert(!is_block_before(b, a));
+
+ return b - a;
+}
+
+static bool in_window(uint16_t block, uint16_t start, uint16_t end)
+{
+ /* handle the three cases:
+ - [ ......... | start | .. | BLOCK | .. | end | ......... ]
+ - [ ..| BLOCK | .. | end | ................. | start | .. ]
+ - [ ..| end | ................. | start | .. | BLOCK | .. ]
+ */
+ return ((start <= block && block <= end) ||
+ (block <= end && end <= start) ||
+ (end <= start && start <= block));
+}
+
+static inline size_t tftp_window_cache_size(struct tftp_cache const *cache)
+{
+ /* allows to optimize away the cache code when TFTP_WINDOW_CACHE_SIZE
+ is 0 */
+ return TFTP_WINDOW_CACHE_NUM == 0 ? 0 : cache->size;
+}
+
+static void tftp_window_cache_init(struct tftp_cache *cache,
+ uint16_t block_len, uint16_t window_size)
+{
+ debug_assert(window_size > 0);
+
+ *cache = (struct tftp_cache) {
+ .id = TFTP_CACHE_NO_ID,
+ .block_len = block_len,
+ .size = min_t(uint16_t, window_size - 1,
+ ARRAY_SIZE(cache->blocks)),
+ };
+}
+
+static void tftp_window_cache_free(struct tftp_cache *cache)
+{
+ size_t cache_size = tftp_window_cache_size(cache);
+ size_t i;
+
+ for (i = 0; i < cache_size; ++i) {
+ free(cache->blocks[i]);
+ cache->blocks[i] = NULL;
+ }
+}
+
+static void tftp_window_cache_reset(struct tftp_cache *cache)
+{
+ cache->id = TFTP_CACHE_NO_ID;
+ memset(cache->used, 0, sizeof cache->used);
+}
+
+static int tftp_window_cache_get_pos(struct tftp_cache const *cache, uint16_t id)
+{
+ size_t cache_size = tftp_window_cache_size(cache);
+ unsigned int pos;
+
+ if (cache_size == 0)
+ return -1;
+
+ if (cache->id == TFTP_CACHE_NO_ID)
+ return -1;
+
+ if (!in_window(id, cache->id, cache->id + cache_size - 1))
+ return -1;
+
+ pos = cache->pos + get_block_delta(cache->id, id);
+ pos %= cache_size;
+
+ return pos;
+}
+
+/* returns whether the first cached element has the given @id */
+static bool tftp_window_cache_starts_with(struct tftp_cache const *cache,
+ uint16_t id)
+{
+ return (TFTP_WINDOW_CACHE_NUM > 0 &&
+ cache->id != TFTP_CACHE_NO_ID &&
+ cache->id == id &&
+ test_bit(cache->pos, cache->used));
+}
+
+static bool tftp_window_cache_is_empty(struct tftp_cache const *cache)
+{
+ /* use this indirection to avoid warnings about a '0 < 0' comparison
+ in the loop condition when TFTP_WINDOW_CACHE_NUM is zero */
+ size_t cache_size = ARRAY_SIZE(cache->used);
+ size_t i;
+
+ for (i = 0; i < cache_size; ++i) {
+ if (cache->used[i] != 0)
+ return false;
+ }
+
+ return true;
+}
+
+static int tftp_window_cache_insert(struct tftp_cache *cache, uint16_t id,
+ void const *data, size_t len)
+{
+ size_t const cache_size = tftp_window_cache_size(cache);
+ int pos;
+ struct tftp_block *block;
+
+ if (cache_size == 0)
+ return -ENOSPC;
+
+ if (cache->id == TFTP_CACHE_NO_ID) {
+ /* initialize cache when it does not contain elements yet */
+ cache->id = id;
+ cache->pos = 0;
+
+ /* sanity check; cache is expected to be empty */
+ debug_assert(tftp_window_cache_is_empty(cache));
+ }
+
+ pos = tftp_window_cache_get_pos(cache, id);
+ if (pos < 0)
+ return -ENOSPC;
+
+ debug_assert(pos < cache_size);
+
+ if (test_bit(pos, cache->used))
+ /* block already cached */
+ return 0;
+
+ block = cache->blocks[pos];
+ if (!block) {
+ /* allocate space for the block; after being released, this
+ memory can be reused for other blocks during the same tftp
+ transfer */
+ block = malloc(sizeof *block + cache->block_len);
+ if (!block)
+ return -ENOMEM;
+
+ cache->blocks[pos] = block;
+ }
+
+ __set_bit(pos, cache->used);
+ memcpy(block->data, data, len);
+ block->id = id;
+ block->len = len;
+
+ return 0;
+}
+
+/* Marks the element at 'pos' as unused and updates internal cache information.
+ Returns true iff element at pos was valid. */
+static bool tftp_window_cache_remove(struct tftp_cache *cache, unsigned int pos)
+{
+ size_t const cache_size = tftp_window_cache_size(cache);
+ bool res;
+
+ if (cache_size == 0)
+ return 0;
+
+ res = __test_and_clear_bit(pos, cache->used);
+
+ if (tftp_window_cache_is_empty(cache)) {
+ /* cache has been cleared; reset pointers */
+ cache->id = TFTP_CACHE_NO_ID;
+ } else if (pos != cache->pos) {
+ /* noop; elements has been removed from the middle of cache */
+ } else {
+ /* first element of cache has been removed; proceed to the
+ next one */
+ cache->id += 1;
+ cache->pos += 1;
+ cache->pos %= cache_size;
+ }
+
+ return res;
+}
+
+/* Releases the first element from the cache and returns its content.
+ *
+ * Function can return NULL when the element is not cached
+ */
+static struct tftp_block *tftp_window_cache_pop(struct tftp_cache *cache)
+{
+ unsigned int pos = cache->pos;
+
+ debug_assert(cache->id != TFTP_CACHE_NO_ID);
+
+ if (!tftp_window_cache_remove(cache, pos))
+ return NULL;
+
+ return cache->blocks[pos];
+}
+
+static bool tftp_window_cache_remove_id(struct tftp_cache *cache, uint16_t id)
+{
+ int pos = tftp_window_cache_get_pos(cache, id);
+
+ if (pos < 0)
+ return false;
+
+ return tftp_window_cache_remove(cache, pos);
+}
+
static int tftp_truncate(struct device_d *dev, FILE *f, loff_t size)
{
return 0;
}
-static char *tftp_states[] = {
+static char const * const tftp_states[] = {
[STATE_INVALID] = "INVALID",
[STATE_RRQ] = "RRQ",
[STATE_WRQ] = "WRQ",
[STATE_RDATA] = "RDATA",
[STATE_WDATA] = "WDATA",
- [STATE_OACK] = "OACK",
[STATE_WAITACK] = "WAITACK",
[STATE_LAST] = "LAST",
[STATE_DONE] = "DONE",
+ [STATE_START] = "START",
};
static int tftp_send(struct file_priv *priv)
@@ -112,6 +377,7 @@ static int tftp_send(struct file_priv *priv)
int len = 0;
uint16_t *s;
unsigned char *pkt = net_udp_get_payload(priv->tftp_con);
+ unsigned int window_size;
int ret;
pr_vdebug("%s: state %s\n", __func__, tftp_states[priv->state]);
@@ -119,6 +385,15 @@ static int tftp_send(struct file_priv *priv)
switch (priv->state) {
case STATE_RRQ:
case STATE_WRQ:
+ if (priv->push || priv->is_getattr)
+ /* atm, windowsize is supported only for RRQ and there
+ is no need to request a full window when we are
+ just looking up file attributes */
+ window_size = 1;
+ else
+ window_size = min_t(unsigned int, g_tftp_window_size,
+ TFTP_MAX_WINDOW_SIZE);
+
xp = pkt;
s = (uint16_t *)pkt;
if (priv->state == STATE_RRQ)
@@ -131,32 +406,45 @@ static int tftp_send(struct file_priv *priv)
"octet%c"
"timeout%c"
"%d%c"
- "tsize%c"
- "%lld%c"
"blksize%c"
- "1432",
- priv->filename + 1, 0,
- 0,
- 0,
- TIMEOUT, 0,
- 0,
- priv->filesize, 0,
- 0);
+ "%u",
+ priv->filename + 1, '\0',
+ '\0', /* "octet" */
+ '\0', /* "timeout" */
+ TIMEOUT, '\0',
+ '\0', /* "blksize" */
+ /* use only a minimal blksize for getattr
+ operations, */
+ priv->is_getattr ? TFTP_BLOCK_SIZE : TFTP_MTU_SIZE);
pkt++;
+
+ if (!priv->push)
+ /* we do not know the filesize in WRQ requests and
+ 'priv->filesize' will always be zero */
+ pkt += sprintf((unsigned char *)pkt,
+ "tsize%c%lld%c",
+ '\0', priv->filesize,
+ '\0');
+
+ if (window_size > 1)
+ pkt += sprintf((unsigned char *)pkt,
+ "windowsize%c%u%c",
+ '\0', window_size,
+ '\0');
+
len = pkt - xp;
break;
case STATE_RDATA:
- if (priv->block == priv->block_requested)
- return 0;
- case STATE_OACK:
xp = pkt;
s = (uint16_t *)pkt;
*s++ = htons(TFTP_ACK);
- *s++ = htons(priv->block);
- priv->block_requested = priv->block;
+ *s++ = htons(priv->last_block);
+ priv->ack_block = priv->last_block;
+ priv->ack_block += priv->windowsize;
pkt = (unsigned char *)s;
len = pkt - xp;
+ tftp_window_cache_reset(&priv->cache);
break;
}
@@ -197,7 +485,6 @@ static int tftp_poll(struct file_priv *priv)
if (is_timeout(priv->resend_timeout, TFTP_RESEND_TIMEOUT)) {
printf("T ");
priv->resend_timeout = get_time_ns();
- priv->block_requested = -1;
return TFTP_ERR_RESEND;
}
@@ -212,7 +499,7 @@ static int tftp_poll(struct file_priv *priv)
return 0;
}
-static void tftp_parse_oack(struct file_priv *priv, unsigned char *pkt, int len)
+static int tftp_parse_oack(struct file_priv *priv, unsigned char *pkt, int len)
{
unsigned char *opt, *val, *s;
@@ -229,14 +516,25 @@ static void tftp_parse_oack(struct file_priv *priv, unsigned char *pkt, int len)
opt = s;
val = s + strlen(s) + 1;
if (val > s + len)
- return;
+ break;
if (!strcmp(opt, "tsize"))
priv->filesize = simple_strtoull(val, NULL, 10);
if (!strcmp(opt, "blksize"))
priv->blocksize = simple_strtoul(val, NULL, 10);
+ if (!strcmp(opt, "windowsize"))
+ priv->windowsize = simple_strtoul(val, NULL, 10);
pr_debug("OACK opt: %s val: %s\n", opt, val);
s = val + strlen(val) + 1;
}
+
+ if (priv->blocksize > TFTP_MTU_SIZE ||
+ priv->windowsize > TFTP_MAX_WINDOW_SIZE ||
+ priv->windowsize == 0) {
+ pr_warn("tftp: invalid oack response\n");
+ return -EINVAL;
+ }
+
+ return 0;
}
static void tftp_timer_reset(struct file_priv *priv)
@@ -244,10 +542,126 @@ static void tftp_timer_reset(struct file_priv *priv)
priv->progress_timeout = priv->resend_timeout = get_time_ns();
}
+static int tftp_allocate_transfer(struct file_priv *priv)
+{
+ debug_assert(!priv->fifo);
+ debug_assert(!priv->buf);
+
+ /* multiplication is safe; both operands were checked in tftp_parse_oack()
+ and are small integers */
+ priv->fifo = kfifo_alloc(priv->blocksize *
+ (priv->windowsize + TFTP_EXTRA_BLOCKS));
+ if (!priv->fifo)
+ goto err;
+
+ if (priv->push) {
+ priv->buf = xmalloc(priv->blocksize);
+ if (!priv->buf) {
+ kfifo_free(priv->fifo);
+ priv->fifo = NULL;
+ goto err;
+ }
+ } else {
+ tftp_window_cache_init(&priv->cache,
+ priv->blocksize, priv->windowsize);
+ }
+
+ return 0;
+
+err:
+ priv->err = -ENOMEM;
+ priv->state = STATE_DONE;
+
+ return priv->err;
+}
+
+static void tftp_put_data(struct file_priv *priv, uint16_t block,
+ void const *pkt, size_t len)
+{
+ unsigned int sz;
+
+ if (len > priv->blocksize) {
+ pr_warn("tftp: oversized packet (%zu > %d) received\n",
+ len, priv->blocksize);
+ return;
+ }
+
+ priv->last_block = block;
+
+ sz = kfifo_put(priv->fifo, pkt, len);
+
+ if (sz != len) {
+ pr_err("tftp: not enough room in kfifo (only %u out of %zu written)\n",
+ sz, len);
+ priv->err = -ENOMEM;
+ priv->state = STATE_DONE;
+ } else if (len < priv->blocksize) {
+ tftp_send(priv);
+ priv->err = 0;
+ priv->state = STATE_DONE;
+ }
+}
+
+static void tftp_apply_window_cache(struct file_priv *priv)
+{
+ struct tftp_cache *cache = &priv->cache;
+
+ while (tftp_window_cache_starts_with(cache, priv->last_block + 1)) {
+ struct tftp_block *block;
+
+ /* can be changed by tftp_put_data() below and must be
+ checked in each loop */
+ if (priv->state != STATE_RDATA)
+ return;
+
+ block = tftp_window_cache_pop(cache);
+
+ debug_assert(block);
+ debug_assert(block->id == (uint16_t)(priv->last_block + 1));
+
+ tftp_put_data(priv, block->id, block->data, block->len);
+ }
+}
+
+static void tftp_handle_data(struct file_priv *priv, uint16_t block,
+ void const *data, size_t len)
+{
+ uint16_t exp_block;
+ int rc;
+
+ exp_block = priv->last_block + 1;
+
+ if (exp_block == block) {
+ /* datagram over network is the expected one; put it in the
+ fifo directly and try to apply cached items then */
+ tftp_timer_reset(priv);
+ tftp_put_data(priv, block, data, len);
+ tftp_window_cache_remove_id(&priv->cache, block);
+ tftp_apply_window_cache(priv);
+ } else if (!in_window(block, exp_block, priv->ack_block)) {
+ /* completely unexpected and unrelated to actual window;
+ ignore the packet. */
+ printf("B");
+ } else {
+ /* The 'rc < 0' below happens e.g. when datagrams in the first
+ part of the transfer window are dropped.
+
+ TODO: this will usually result in a timeout
+ (TFTP_RESEND_TIMEOUT). It should be possible to bypass
+ this timeout by acknowledging the last packet (e.g. by
+ doing 'priv->ack_block = priv->last_block' here). */
+ rc = tftp_window_cache_insert(&priv->cache, block, data, len);
+ if (rc < 0)
+ printf("M");
+ }
+}
+
static void tftp_recv(struct file_priv *priv,
uint8_t *pkt, unsigned len, uint16_t uh_sport)
{
uint16_t opcode;
+ uint16_t block;
+ int rc;
/* according to RFC1350 minimal tftp packet length is 4 bytes */
if (len < 4)
@@ -270,75 +684,80 @@ static void tftp_recv(struct file_priv *priv,
if (!priv->push)
break;
- priv->block = ntohs(*(uint16_t *)pkt);
- if (priv->block != priv->last_block) {
- pr_vdebug("ack %d != %d\n", priv->block, priv->last_block);
+ block = ntohs(*(uint16_t *)pkt);
+ if (block != priv->last_block) {
+ pr_vdebug("ack %d != %d\n", block, priv->last_block);
break;
}
- priv->block++;
+ switch (priv->state) {
+ case STATE_WRQ:
+ priv->tftp_con->udp->uh_dport = uh_sport;
+ priv->state = STATE_START;
+ break;
- tftp_timer_reset(priv);
+ case STATE_WAITACK:
+ priv->state = STATE_WDATA;
+ break;
- if (priv->state == STATE_LAST) {
+ case STATE_LAST:
priv->state = STATE_DONE;
break;
+
+ default:
+ pr_warn("ACK packet in %s state\n",
+ tftp_states[priv->state]);
+ goto ack_out;
}
- priv->tftp_con->udp->uh_dport = uh_sport;
- priv->state = STATE_WDATA;
+
+ priv->block = block + 1;
+ tftp_timer_reset(priv);
+
+ ack_out:
break;
case TFTP_OACK:
- tftp_parse_oack(priv, pkt, len);
+ if (priv->state != STATE_RRQ && priv->state != STATE_WRQ) {
+ pr_warn("OACK packet in %s state\n",
+ tftp_states[priv->state]);
+ break;
+ }
+
priv->tftp_con->udp->uh_dport = uh_sport;
- if (priv->push) {
- /* send first block */
- priv->state = STATE_WDATA;
- priv->block = 1;
- } else {
- /* send ACK */
- priv->state = STATE_OACK;
- priv->block = 0;
- tftp_send(priv);
+ if (tftp_parse_oack(priv, pkt, len) < 0) {
+ priv->err = -EINVAL;
+ priv->state = STATE_DONE;
+ break;
}
+ priv->state = STATE_START;
break;
+
case TFTP_DATA:
len -= 2;
- priv->block = ntohs(*(uint16_t *)pkt);
+ block = ntohs(*(uint16_t *)pkt);
- if (priv->state == STATE_RRQ || priv->state == STATE_OACK) {
- /* first block received */
- priv->state = STATE_RDATA;
+ if (priv->state == STATE_RRQ) {
+ /* first block received; entered only with non rfc
+ 2347 (TFTP Option extension) compliant servers */
priv->tftp_con->udp->uh_dport = uh_sport;
+ priv->state = STATE_RDATA;
priv->last_block = 0;
+ priv->ack_block = priv->windowsize;
- if (priv->block != 1) { /* Assertion */
- pr_err("error: First block is not block 1 (%d)\n",
- priv->block);
- priv->err = -EINVAL;
- priv->state = STATE_DONE;
+ rc = tftp_allocate_transfer(priv);
+ if (rc < 0)
break;
- }
}
- if (priv->block == priv->last_block)
- /* Same block again; ignore it. */
+ if (priv->state != STATE_RDATA) {
+ pr_warn("DATA packet in %s state\n",
+ tftp_states[priv->state]);
break;
-
- priv->last_block = priv->block;
-
- tftp_timer_reset(priv);
-
- kfifo_put(priv->fifo, pkt + 2, len);
-
- if (len < priv->blocksize) {
- tftp_send(priv);
- priv->err = 0;
- priv->state = STATE_DONE;
}
+ tftp_handle_data(priv, block, pkt + 2, len);
break;
case TFTP_ERROR:
@@ -369,13 +788,38 @@ static void tftp_handler(void *ctx, char *packet, unsigned len)
tftp_recv(priv, pkt, net_eth_to_udplen(packet), udp->uh_sport);
}
+static int tftp_start_transfer(struct file_priv *priv)
+{
+ int rc;
+
+ rc = tftp_allocate_transfer(priv);
+ if (rc < 0)
+ /* function sets 'priv->state = STATE_DONE' and 'priv->err' in
+ error case */
+ return rc;
+
+ if (priv->push) {
+ /* send first block */
+ priv->state = STATE_WDATA;
+ priv->block = 1;
+ } else {
+ /* send ACK */
+ priv->state = STATE_RDATA;
+ priv->last_block = 0;
+ tftp_send(priv);
+ }
+
+ return 0;
+}
+
static struct file_priv *tftp_do_open(struct device_d *dev,
- int accmode, struct dentry *dentry)
+ int accmode, struct dentry *dentry, bool is_getattr)
{
struct fs_device_d *fsdev = dev_to_fs_device(dev);
struct file_priv *priv;
struct tftp_priv *tpriv = dev->priv;
int ret;
+ unsigned short port = TFTP_PORT;
priv = xzalloc(sizeof(*priv));
@@ -397,49 +841,77 @@ static struct file_priv *tftp_do_open(struct device_d *dev,
priv->err = -EINVAL;
priv->filename = dpath(dentry, fsdev->vfsmount.mnt_root);
priv->blocksize = TFTP_BLOCK_SIZE;
- priv->block_requested = -1;
+ priv->windowsize = 1;
+ priv->is_getattr = is_getattr;
- priv->fifo = kfifo_alloc(TFTP_FIFO_SIZE);
- if (!priv->fifo) {
- ret = -ENOMEM;
- goto out;
- }
+ parseopt_hu(fsdev->options, "port", &port);
- priv->tftp_con = net_udp_new(tpriv->server, TFTP_PORT, tftp_handler,
- priv);
+ priv->tftp_con = net_udp_new(tpriv->server, port, tftp_handler, priv);
if (IS_ERR(priv->tftp_con)) {
ret = PTR_ERR(priv->tftp_con);
- goto out1;
+ goto out;
}
ret = tftp_send(priv);
if (ret)
- goto out2;
+ goto out1;
tftp_timer_reset(priv);
- while (priv->state != STATE_RDATA &&
- priv->state != STATE_DONE &&
- priv->state != STATE_WDATA) {
- ret = tftp_poll(priv);
- if (ret == TFTP_ERR_RESEND)
- tftp_send(priv);
- if (ret < 0)
- goto out2;
- }
- if (priv->state == STATE_DONE && priv->err) {
- ret = priv->err;
- goto out2;
- }
+ /* - 'ret < 0' ... error
+ - 'ret == 0' ... further tftp_poll() required
+ - 'ret == 1' ... startup finished */
+ do {
+ switch (priv->state) {
+ case STATE_DONE:
+ /* branch is entered in two situations:
+ - non rfc 2347 compliant servers finished the
+ transfer by sending a small file
+ - some error occurred */
+ if (priv->err < 0)
+ ret = priv->err;
+ else
+ ret = 1;
+ break;
+
+ case STATE_START:
+ ret = tftp_start_transfer(priv);
+ if (!ret)
+ ret = 1;
+ break;
+
+ case STATE_RDATA:
+ /* first data block of non rfc 2347 servers */
+ ret = 1;
+ break;
+
+ case STATE_RRQ:
+ case STATE_WRQ:
+ ret = tftp_poll(priv);
+ if (ret == TFTP_ERR_RESEND) {
+ tftp_send(priv);
+ ret = 0;
+ }
+ break;
+
+ default:
+ debug_assert(false);
+ break;
+ }
+ } while (ret == 0);
- priv->buf = xmalloc(priv->blocksize);
+ if (ret < 0)
+ goto out1;
return priv;
-out2:
- net_unregister(priv->tftp_con);
out1:
- kfifo_free(priv->fifo);
+ net_unregister(priv->tftp_con);
out:
+ if (priv->fifo)
+ kfifo_free(priv->fifo);
+
+ free(priv->filename);
+ free(priv->buf);
free(priv);
return ERR_PTR(ret);
@@ -449,7 +921,7 @@ static int tftp_open(struct device_d *dev, FILE *file, const char *filename)
{
struct file_priv *priv;
- priv = tftp_do_open(dev, file->flags, file->dentry);
+ priv = tftp_do_open(dev, file->flags, file->dentry, false);
if (IS_ERR(priv))
return PTR_ERR(priv);
@@ -489,6 +961,7 @@ static int tftp_do_close(struct file_priv *priv)
}
net_unregister(priv->tftp_con);
+ tftp_window_cache_free(&priv->cache);
kfifo_free(priv->fifo);
free(priv->filename);
free(priv->buf);
@@ -544,7 +1017,7 @@ static int tftp_read(struct device_d *dev, FILE *f, void *buf, size_t insize)
{
struct file_priv *priv = f->priv;
size_t outsize = 0, now;
- int ret;
+ int ret = 0;
pr_vdebug("%s %zu\n", __func__, insize);
@@ -553,19 +1026,30 @@ static int tftp_read(struct device_d *dev, FILE *f, void *buf, size_t insize)
outsize += now;
buf += now;
insize -= now;
- if (priv->state == STATE_DONE)
- return outsize;
- if (TFTP_FIFO_SIZE - kfifo_len(priv->fifo) >= priv->blocksize)
+ if (priv->state == STATE_DONE) {
+ ret = priv->err;
+ break;
+ }
+
+ /* send the ACK only when fifo has been nearly depleted; else,
+ when tftp_read() is called with small 'insize' values, it
+ is possible that there is read more data from the network
+ than consumed by kfifo_get() and the fifo overflows */
+ if (priv->last_block == priv->ack_block &&
+ kfifo_len(priv->fifo) <= TFTP_EXTRA_BLOCKS * priv->blocksize)
tftp_send(priv);
ret = tftp_poll(priv);
if (ret == TFTP_ERR_RESEND)
tftp_send(priv);
if (ret < 0)
- return ret;
+ break;
}
+ if (ret < 0)
+ return ret;
+
return outsize;
}
@@ -665,7 +1149,7 @@ static struct dentry *tftp_lookup(struct inode *dir, struct dentry *dentry,
struct file_priv *priv;
loff_t filesize;
- priv = tftp_do_open(&fsdev->dev, O_RDONLY, dentry);
+ priv = tftp_do_open(&fsdev->dev, O_RDONLY, dentry, true);
if (IS_ERR(priv))
return NULL;
@@ -748,6 +1232,110 @@ static struct fs_driver_d tftp_driver = {
static int tftp_init(void)
{
+ globalvar_add_simple_int("tftp.windowsize", &g_tftp_window_size, "%u");
+
return register_fs_driver(&tftp_driver);
}
coredevice_initcall(tftp_init);
+
+
+BSELFTEST_GLOBALS();
+
+static int __maybe_unused tftp_window_cache_selftest(void)
+{
+ struct tftp_cache *cache = malloc(sizeof *cache);
+
+ if (!cache)
+ return -ENOMEM;
+
+ (void)skipped_tests;
+
+ expect_it( is_block_before(0, 1));
+ expect_it(!is_block_before(1, 0));
+ expect_it( is_block_before(65535, 0));
+ expect_it(!is_block_before(0, 65535));
+
+ expect_eq(get_block_delta(0, 1), 1);
+ expect_eq(get_block_delta(65535, 0), 1);
+ expect_eq(get_block_delta(65535, 1), 2);
+
+ expect_it(!in_window(0, 1, 3));
+ expect_it( in_window(1, 1, 3));
+ expect_it( in_window(2, 1, 3));
+ expect_it( in_window(3, 1, 3));
+ expect_it(!in_window(4, 1, 3));
+
+ expect_it(!in_window(65534, 65535, 1));
+ expect_it( in_window(65535, 65535, 1));
+ expect_it( in_window( 0, 65535, 1));
+ expect_it( in_window( 1, 65535, 1));
+ expect_it(!in_window( 2, 65535, 1));
+
+
+ tftp_window_cache_init(cache, 512, 5);
+
+ if (tftp_window_cache_size(cache) < 4)
+ goto out;
+
+ expect_eq(tftp_window_cache_size(cache), 4);
+
+ /* sequence 1 */
+ expect_ok (tftp_window_cache_insert(cache, 20, "20", 2));
+ expect_ok (tftp_window_cache_insert(cache, 22, "22", 2));
+ expect_ok (tftp_window_cache_insert(cache, 21, "21", 2));
+ expect_ok (tftp_window_cache_insert(cache, 23, "23", 2));
+ expect_err(tftp_window_cache_insert(cache, 24, "24", 2));
+ expect_err(tftp_window_cache_insert(cache, 19, "19", 2));
+ expect_ok (tftp_window_cache_insert(cache, 22, "22", 2));
+ expect_ok (tftp_window_cache_insert(cache, 20, "20", 2));
+
+ expect_eq(tftp_window_cache_pop(cache)->id, 20);
+ expect_eq(tftp_window_cache_pop(cache)->id, 21);
+ expect_eq(tftp_window_cache_pop(cache)->id, 22);
+ expect_eq(tftp_window_cache_pop(cache)->id, 23);
+ expect_eq(cache->id, TFTP_CACHE_NO_ID);
+
+ /* sequence 2 */
+ expect_ok (tftp_window_cache_insert(cache, 30, "30", 2));
+ expect_ok (tftp_window_cache_insert(cache, 32, "32", 2));
+ expect_err(tftp_window_cache_insert(cache, 34, "34", 2));
+
+ expect_it(tftp_window_cache_starts_with(cache, 30));
+ expect_eq(tftp_window_cache_pop(cache)->id, 30);
+
+ expect_ok (tftp_window_cache_insert(cache, 34, "34", 2));
+ expect_err(tftp_window_cache_insert(cache, 35, "35", 2));
+
+ expect_it(!tftp_window_cache_starts_with(cache, 30));
+ expect_it(!tftp_window_cache_starts_with(cache, 31));
+ expect_it(!tftp_window_cache_starts_with(cache, 32));
+ expect_NULL(tftp_window_cache_pop(cache));
+
+ expect_it(tftp_window_cache_starts_with(cache, 32));
+ expect_eq(tftp_window_cache_pop(cache)->id, 32);
+
+ expect_NULL(tftp_window_cache_pop(cache));
+ expect_eq(tftp_window_cache_pop(cache)->id, 34);
+
+ expect_eq(cache->id, TFTP_CACHE_NO_ID);
+
+ /* sequence 3 */
+ expect_ok(tftp_window_cache_insert(cache, 40, "40", 2));
+ expect_ok(tftp_window_cache_insert(cache, 42, "42", 2));
+ expect_ok(tftp_window_cache_insert(cache, 43, "43", 2));
+
+ expect_it(!tftp_window_cache_remove_id(cache, 30));
+ expect_it(!tftp_window_cache_remove_id(cache, 41));
+ expect_it(!tftp_window_cache_remove_id(cache, 44));
+
+ expect_it( tftp_window_cache_remove_id(cache, 42));
+ expect_it(!tftp_window_cache_remove_id(cache, 42));
+
+out:
+ tftp_window_cache_free(cache);
+
+ return 0;
+}
+#ifdef CONFIG_SELFTEST_TFTP
+bselftest(core, tftp_window_cache_selftest);
+#endif