diff options
Diffstat (limited to 'fs/tftp.c')
-rw-r--r-- | fs/tftp.c | 550 |
1 files changed, 437 insertions, 113 deletions
@@ -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,54 @@ #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 + +/* 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 list; + uint8_t data[]; +}; + +struct tftp_cache { + struct list_head blocks; +}; + 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 +121,78 @@ 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 int tftp_truncate(struct device_d *dev, FILE *f, loff_t size) +static inline bool is_block_before(uint16_t a, uint16_t b) +{ + return (int16_t)(b - a) > 0; +} + +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 void tftp_window_cache_free(struct tftp_cache *cache) +{ + struct tftp_block *block, *tmp; + + list_for_each_entry_safe(block, tmp, &cache->blocks, list) + free(block); +} + +static int tftp_window_cache_insert(struct tftp_cache *cache, uint16_t id, + void const *data, size_t len) +{ + struct tftp_block *block, *new; + + list_for_each_entry(block, &cache->blocks, list) { + if (block->id == id) + return 0; + if (is_block_before(block->id, id)) + continue; + + break; + } + + new = xzalloc(sizeof(*new) + len); + memcpy(new->data, data, len); + new->id = id; + new->len = len; + list_add_tail(&new->list, &block->list); + + return 0; +} + +static int tftp_truncate(struct device *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 +201,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 +209,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,30 +230,42 @@ 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; break; @@ -197,7 +308,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 +322,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 +339,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 +365,140 @@ 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; + } + } + + INIT_LIST_HEAD(&priv->cache.blocks); + + 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 (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; + + if (list_empty(&cache->blocks)) + return; + + block = list_first_entry(&cache->blocks, struct tftp_block, list); + + if (is_block_before(block->id, priv->last_block + 1)) { + /* shouldn't happen, but be sure */ + list_del(&block->list); + free(block); + continue; + } + + if (block->id != (uint16_t)(priv->last_block + 1)) + return; + + tftp_put_data(priv, block->id, block->data, block->len); + + list_del(&block->list); + + free(block); + } +} + +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_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"); + if (g_tftp_window_size > 1) + pr_warn_once("Unexpected packet. global.tftp.windowsize set too high?\n"); + } 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 +521,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 +625,39 @@ static void tftp_handler(void *ctx, char *packet, unsigned len) tftp_recv(priv, pkt, net_eth_to_udplen(packet), udp->uh_sport); } -static struct file_priv *tftp_do_open(struct device_d *dev, - int accmode, struct dentry *dentry) +static int tftp_start_transfer(struct file_priv *priv) { - struct fs_device_d *fsdev = dev_to_fs_device(dev); + 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 *dev, + int accmode, struct dentry *dentry, + bool is_getattr) +{ + struct fs_device *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,59 +679,87 @@ 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; - priv->buf = xmalloc(priv->blocksize); + 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); + + 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); } -static int tftp_open(struct device_d *dev, FILE *file, const char *filename) +static int tftp_open(struct device *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 +799,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); @@ -497,15 +808,15 @@ static int tftp_do_close(struct file_priv *priv) return 0; } -static int tftp_close(struct device_d *dev, FILE *f) +static int tftp_close(struct device *dev, FILE *f) { struct file_priv *priv = f->priv; return tftp_do_close(priv); } -static int tftp_write(struct device_d *_dev, FILE *f, const void *inbuf, - size_t insize) +static int tftp_write(struct device *_dev, FILE *f, const void *inbuf, + size_t insize) { struct file_priv *priv = f->priv; size_t size, now; @@ -540,11 +851,11 @@ static int tftp_write(struct device_d *_dev, FILE *f, const void *inbuf, return insize; } -static int tftp_read(struct device_d *dev, FILE *f, void *buf, size_t insize) +static int tftp_read(struct device *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,23 +864,34 @@ 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; } -static int tftp_lseek(struct device_d *dev, FILE *f, loff_t pos) +static int tftp_lseek(struct device *dev, FILE *f, loff_t pos) { /* We cannot seek backwards without reloading or caching the file */ loff_t f_pos = f->pos; @@ -660,12 +982,12 @@ static struct dentry *tftp_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags) { struct super_block *sb = dir->i_sb; - struct fs_device_d *fsdev = container_of(sb, struct fs_device_d, sb); + struct fs_device *fsdev = container_of(sb, struct fs_device, sb); struct inode *inode; 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; @@ -695,9 +1017,9 @@ static const struct inode_operations tftp_dir_inode_operations = static const struct super_operations tftp_ops; -static int tftp_probe(struct device_d *dev) +static int tftp_probe(struct device *dev) { - struct fs_device_d *fsdev = dev_to_fs_device(dev); + struct fs_device *fsdev = dev_to_fs_device(dev); struct tftp_priv *priv = xzalloc(sizeof(struct tftp_priv)); struct super_block *sb = &fsdev->sb; struct inode *inode; @@ -724,14 +1046,14 @@ err: return ret; } -static void tftp_remove(struct device_d *dev) +static void tftp_remove(struct device *dev) { struct tftp_priv *priv = dev->priv; free(priv); } -static struct fs_driver_d tftp_driver = { +static struct fs_driver tftp_driver = { .open = tftp_open, .close = tftp_close, .read = tftp_read, @@ -748,6 +1070,8 @@ 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); |