25 Commits
Author SHA1 Message Date
Dmitry Baryshkov 443c82aada Merge pull request #31 from lumag/tqftp-1.1.1
meson.build: prepare 1.1.1 release
2026-01-07 19:22:17 +02:00
Dmitry Baryshkov 3e568ebf78 meson.build: prepare 1.1.1 release
Bugfix release:
- fix zstd integration
- fix qrtr-ns service dependencies
- fix systemd unit installation
- fix buffer / ACK handling in corner cases

Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
2026-01-07 19:14:20 +02:00
Dmitry Baryshkov 0ed681362b Merge pull request #30 from fossdd/systemd
build: allow building systemd unit files without systemd dependency
2025-12-24 06:28:44 +02:00
Achill Gilgenast d4c7f06435 build: allow building systemd unit files without systemd dependency
If systemd-unit-prefix is passed, there is no need for the systemd build
dependency.

Allows building in postmarketOS, where the build environment can't
install systemd.

Signed-off-by: Achill Gilgenast <achill@achill.org>
2025-11-18 17:15:00 +01:00
Dmitry Baryshkov 408ca1ed5e Merge pull request #26 from quic-bhaskarv/topic/fix_wsize
tqftpserv: ACK during write based on wsize
2025-08-01 14:48:48 +03:00
Bhaskar Valaboju 33a3b4891e tqftpserv: Cache wsize buffers in client instance
The client sends data packets of blksize size and waits for an ACK
after sending wsize packets. If packets are not received in sequence,
the server should send NACK, prompting the client to resend the packets
starting from beginning of the window.

For example, with wsize = 10 and blksize = 5000, the client sends
blocks [201–210] and waits. If all are received, the server replies
with ACK; otherwise, it sends NACK, and the client restarts from block 201.

Use rw_buf to cache received blocks and track the next expected block
using blk_expected. Reset the expected block to the start of the window
if out-of-sequence packets are detected.

Once all buffers are received, commit the cached buffers to the file.

Signed-off-by: Bhaskar Valaboju <quic_bhaskarv@quicinc.com>
2025-07-18 18:34:46 +05:30
Bhaskar Valaboju ad282fcab3 tqftpserv: Use blksize buffer in send/recvfrom for TFTP packets
Use the blksize-sized buffer from the client instance for send
and recvfrom operations during TFTP packet handling.

This eliminates repeated memory allocation in handler_reader
and supports receiving full blksize payloads in handler_writer.

Signed-off-by: Bhaskar Valaboju <quic_bhaskarv@quicinc.com>
2025-07-18 18:26:43 +05:30
Bhaskar Valaboju 7d5fcd440a tqftpserv: Allocate memory in client instance to hold blksize buffer
Allocate memory in the client instance to support transmission and
reception of TFTP data packets sized according to the negotiated
blksize option. This buffer ensures proper handling of variable-length
payloads during transfer operations.

Signed-off-by: Bhaskar Valaboju <quic_bhaskarv@quicinc.com>
2025-07-18 18:09:57 +05:30
Bhaskar Valaboju 44748aa794 tqftpserv: Set default wsize to 1 if not specified in WRQ/RRQ
Ensure the default wsize value is set to 1 when it is not provided
in a Write Request (WRQ) or Read Request (RRQ).

Signed-off-by: Bhaskar Valaboju <quic_bhaskarv@quicinc.com>
2025-07-18 18:02:19 +05:30
Bhaskar Valaboju 4b3054aed3 tqftpserv: Allocate wsize buffer in TFTP client instance
Allocate a buffer within the client instance that can hold
the wsize value when reading from or writing to the TFTP client.

Signed-off-by: Bhaskar Valaboju <quic_bhaskarv@quicinc.com>
2025-07-18 17:59:41 +05:30
Dmitry Baryshkov 9fc5fdc53b Merge pull request #28 from z3ntu/zstd-errors
translate: Check existance of .zstd firmware before accessing
2025-07-09 13:26:37 +03:00
Luca Weiss 1a516e7a78 translate: Check existance of .zstd firmware before accessing
Since we're trying multiple different paths, fail quietly if the
.zstd-compressed version of the firmware does not exist.

We're trying multiple different paths anyways and have error prints for
this in other places.

Tested cases with 901_0_0.mbn as an example:
* 901_0_0.mbn exists -> no error
* 901_0_0.mbn.zstd exists -> no error
* neither exists -> error print
    [TQFTP] unable to open /readonly/firmware/image/modem_pr/so/901_0_0.mbn (2), reject

Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-07-07 09:27:42 +02:00
Luca Weiss 72aad03e24 zstd-decompress: Print file path in "stat failed" message
Make the errors a bit more useful than just "stat failed" to know which
file it's about.

[TQFTP] RRQ: /readonly/firmware/image/modem_pr/so/901_0_0.mbn (mode=octet rsize=4096 seek=0)
stat /lib/firmware/postmarketos/qcom/qcm6490/fairphone5/modem_pr/so/901_0_0.mbn.zst failed (No such file or directory)
stat /lib/firmware/updates/qcom/qcm6490/fairphone5/modem_pr/so/901_0_0.mbn.zst failed (No such file or directory)

Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-07-07 09:27:42 +02:00
Dmitry Baryshkov b0f786c1a7 Merge pull request #27 from z3ntu/readme
Add README.md
2025-07-06 13:24:51 +03:00
Luca Weiss e2bfdbb52f Add README.md
Try and document what tqftpserv is actually doing, so that people who
are new to the stack can get some understanding of it, without first
needing to figure out everything.

I've spent sime time working on tqftpserv so I hope the description is
correct, but no guarantees.

Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-07-06 12:06:06 +02:00
Dmitry Baryshkov 533779cb8a Merge pull request #24 from nik012003/master
tqftpserv.service.in: remove dependency on qrtr-ns.service
2024-11-21 18:13:30 +02:00
Nicola Guerrera cab8b18f9a tqftpserv.service.in: remove dependency on qrtr-ns.service
QRTR is built in to the kernel now, the other packages (such as rmtfs) have since removed qrtr-ns as a service dependency

Signed-off-by: Nicola Guerrera <guerrera.nicola@gmail.com>
2024-11-09 00:22:37 +01:00
Dmitry Baryshkov 859c6e3969 Merge pull request #23 from a-wai/firmware-updates
translate: lookup firmware files under /lib/firmware/updates
2024-10-19 19:37:05 +03:00
Dmitry Baryshkov bbfff09e16 Merge pull request #22 from evelikov/zstd-followups
Follow-up zstd fixes
2024-10-16 17:33:58 +03:00
Arnaud Ferraris 4744c0ce07 translate: lookup firmware files under /lib/firmware/updates
`/lib/firmware/updates` is a legitimate location for firmware files,
`tqftpserv` should look there as well.
2024-09-25 10:52:52 +02:00
Emil Velikov f9da0667fb Fix Android build without zstd
Bit of a blind fix, since I don't have an Android dev setup nearby.

Signed-off-by: Emil Velikov <emil.l.velikov@gmail.com>
2024-09-21 16:10:16 +01:00
Emil Velikov 0f7fd27524 Use context-less zstd decompression API
Currently we pre-allocate around 128K of zstd state ahead of time,
keeping if for the whole daemon lifetime... Even if we don't need to
decompress any files. As mentioned by Dmitry:

    Granted that tqftpserv handles only few files during the whole
    lifetime and most of devices don't have swap, I think it's fine to
    use non-context versions of the functions. Let's free the memory for
    other software.

Swap ZSTD_decompressDCtx() for ZSTD_decompress() which will allocate and
free the state when needed.

Signed-off-by: Emil Velikov <emil.l.velikov@gmail.com>
2024-09-21 16:09:21 +01:00
Emil Velikov 95d0c67322 Explicitly open zstd file as read-only
Signed-off-by: Emil Velikov <emil.l.velikov@gmail.com>
2024-09-19 12:53:27 +01:00
Emil Velikov 085a87c279 Clear memory leaks during zstd decompression
Signed-off-by: Emil Velikov <emil.l.velikov@gmail.com>
2024-09-19 12:48:16 +01:00
Konrad Dybcio 48e143d273 meson.build: Set v1.1 version
Signed-off-by: Konrad Dybcio <konrad.dybcio@linaro.org>
2024-07-23 14:25:36 +02:00
7 changed files with 184 additions and 67 deletions

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
# tqftpserv
The tqftpserv software is an implementation of a TFTP (Trivial File Transfer Protocol) server which runs on top of the AF_QIPCRTR (a.k.a QRTR) socket type.
The main purpose of tqftpserv is to serve files from the Linux file system to other processors on the Qualcomm SoCs as requested.
The protocol implemented here is (loosely) based on RFC 1350 including some extensions to the protocol.
In basic terms, the protocol supports RRQ (Read Request) and WRQ (Write Request) messages which read and write files respectively. A request can have some extra options, like `blksize` ior `wsize`. The meaning of those is documented in the `parse_options` function.
For reference, the proprietary implementation of this that is used on practically all Qualcomm-based Android devices is called "tftp_server".
## File paths
There's two different virtual paths prefixes that are supported:
* `/readonly/firmware/image/` for read-only files such as firmware files
* `/readwrite/` for read-write (temporary) files
Requests to those paths are "translated" (see `translate.c`) to paths in the Linux filesystem. In a regular setup readonly requests go to `/lib/firmware/` and readwrite requests go to `/tmp/tqftpserv/`.
Translating those readonly request paths tries to take into account custom Linux firmware paths, custom "firmware-name" paths for remoteprocs, .zstd compressed firmware and more. In case of doubt, please consult the source code.
For example on a QCM6490 Fairphone 5 smartphone the path for `/readonly/firmware/image/modem_pr/so/901_0_0.mbn` will be translated to `/lib/firmware/qcom/qcm6490/fairphone5/modem_pr/so/901_0_0.mbn`, based on the devicetree property `firmware-name = "qcom/qcm6490/fairphone5/modem.mbn";` in the modem/mpss DT node.
The actual paths that are used and requested are dependent on the TFTP clients, which is usually the firmware that is running on the Hexagon-based modem processor on the SoC.
## Example requests
The following are some example requests which are grabbed from the tqftpserv log during modem bootup.
* Write content to a file called "server_check.txt":
```
[TQFTP] WRQ: /readwrite/server_check.txt (octet)
```
* Read a file called "ota_firewall/ruleset" which does not exist:
```
[TQFTP] RRQ: /readwrite/ota_firewall/ruleset (mode=octet rsize=0 seek=0)
tqftpserv: failed to open ota_firewall/ruleset: No such file or directory
[TQFTP] unable to open /readwrite/ota_firewall/ruleset (2), reject
```
* Stat a file with the path "modem_pr/so/901_0_0.mbn", and then read it:
```
[TQFTP] RRQ: /readonly/firmware/image/modem_pr/so/901_0_0.mbn (mode=octet rsize=0 seek=0)
[TQFTP] Remote returned END OF TRANSFER: 9 - End of Transfer
[TQFTP] RRQ: /readonly/firmware/image/modem_pr/so/901_0_0.mbn (mode=octet rsize=52 seek=0)
```
In binary such a request can look like the following:
```
\0\1/readonly/firmware/image/modem_pr/so/901_0_0.mbn\0octet\0blksize\0007680\0timeoutms\0001000\0tsize\0000\0wsize\00010\0
```
+11 -10
View File
@@ -2,7 +2,7 @@
project('tqftpserv',
'c',
version: '1.1',
version: '1.1.1',
default_options: [
'warning_level=1',
'buildtype=release',
@@ -11,16 +11,17 @@ project('tqftpserv',
prefix = get_option('prefix')
zstd_dep = dependency('libzstd')
add_project_arguments('-DHAVE_ZSTD', language : 'c')
# Not required to build the executable, only to install unit file
# Not required to build the executable,
# only to automatically retrieve the install dir for unit files
systemd = dependency('systemd', required : false)
if systemd.found()
systemd_system_unit_dir = get_option('systemd-unit-prefix')
if systemd_system_unit_dir == ''
systemd_system_unit_dir = systemd.get_variable(
pkgconfig : 'systemdsystemunitdir',
pkgconfig_define: ['prefix', prefix])
endif
if get_option('systemd-unit-prefix') != ''
systemd_system_unit_dir = get_option('systemd-unit-prefix')
elif systemd.found()
systemd_system_unit_dir = systemd.get_variable(
pkgconfig : 'systemdsystemunitdir',
pkgconfig_define: ['prefix', prefix])
endif
qrtr_dep = dependency('qrtr')
@@ -33,7 +34,7 @@ executable('tqftpserv',
dependencies : [qrtr_dep, zstd_dep],
install : true)
if systemd.found()
if systemd_system_unit_dir != ''
systemd_unit_conf = configuration_data()
systemd_unit_conf.set('prefix', prefix)
configure_file(
+79 -24
View File
@@ -15,7 +15,6 @@
#include "list.h"
#include "translate.h"
#include "zstd-decompress.h"
#define MAX(x, y) ((x) > (y) ? (x) : (y))
@@ -47,6 +46,12 @@ struct tftp_client {
size_t wsize;
unsigned int timeoutms;
off_t seek;
uint8_t *blk_buf;
uint8_t *rw_buf;
size_t rw_buf_size;
size_t blk_offset;
uint16_t blk_expected;
};
static struct list_head readers = LIST_INIT(readers);
@@ -57,11 +62,8 @@ static ssize_t tftp_send_data(struct tftp_client *client,
{
ssize_t len;
size_t send_len;
char *buf;
char *p;
buf = malloc(4 + client->blksize);
p = buf;
char *buf = client->blk_buf;
char *p = buf;
*p++ = 0;
*p++ = OP_DATA;
@@ -92,11 +94,7 @@ static ssize_t tftp_send_data(struct tftp_client *client,
}
// printf("[TQFTP] Sending %zd bytes of DATA\n", send_len);
len = send(client->sock, buf, send_len, 0);
free(buf);
return len;
return send(client->sock, buf, send_len, 0);
}
@@ -252,7 +250,7 @@ static void handle_rrq(const char *buf, size_t len, struct sockaddr_qrtr *sq)
size_t blksize = 512;
unsigned int timeoutms = 1000;
size_t rsize = 0;
size_t wsize = 0;
size_t wsize = 1;
off_t seek = 0;
bool do_oack = false;
int sock;
@@ -316,6 +314,19 @@ static void handle_rrq(const char *buf, size_t len, struct sockaddr_qrtr *sq)
client->wsize = wsize;
client->timeoutms = timeoutms;
client->seek = seek;
client->rw_buf_size = blksize * wsize;
client->blk_buf = calloc(1, blksize + 4);
if (!client->blk_buf) {
printf("[TQFTP] Memory allocation failure\n");
return;
}
client->rw_buf = calloc(1, client->rw_buf_size);
if (!client->rw_buf) {
printf("[TQFTP] Memory allocation failure\n");
return;
}
// printf("[TQFTP] new reader added\n");
@@ -343,7 +354,7 @@ static void handle_wrq(const char *buf, size_t len, struct sockaddr_qrtr *sq)
size_t blksize = 512;
unsigned int timeoutms = 1000;
size_t rsize = 0;
size_t wsize = 0;
size_t wsize = 1;
off_t seek = 0;
bool do_oack = false;
int sock;
@@ -398,6 +409,20 @@ static void handle_wrq(const char *buf, size_t len, struct sockaddr_qrtr *sq)
client->wsize = wsize;
client->timeoutms = timeoutms;
client->seek = seek;
client->rw_buf_size = blksize * wsize;
client->blk_expected = 1;
client->blk_buf = calloc(1, blksize + 4);
if (!client->blk_buf) {
printf("[TQFTP] Memory allocation failure\n");
return;
}
client->rw_buf = calloc(1, client->rw_buf_size);
if (!client->rw_buf) {
printf("[TQFTP] Memory allocation failure\n");
return;
}
// printf("[TQFTP] new writer added\n");
@@ -494,14 +519,14 @@ static int handle_writer(struct tftp_client *client)
struct sockaddr_qrtr sq;
uint16_t block;
size_t payload;
char buf[516];
uint8_t *buf = client->blk_buf;
socklen_t sl;
ssize_t len;
int opcode;
int ret;
sl = sizeof(sq);
len = recvfrom(client->sock, buf, sizeof(buf), 0, (void *)&sq, &sl);
len = recvfrom(client->sock, buf, client->blksize + 4, 0, (void *)&sq, &sl);
if (len < 0) {
ret = -errno;
if (ret != -ENETRESET)
@@ -523,17 +548,48 @@ static int handle_writer(struct tftp_client *client)
}
payload = len - 4;
buf += 4;
/* Check if we recieved expected block */
if (block != client->blk_expected) {
uint16_t blk_expected = client->blk_expected;
printf("[TQFTP] Block number out of sequence: %d (expected %d)\n",
block, blk_expected);
tftp_send_error(client->sock, 4, "Block number out of sequence");
/* Set blk_expected to beginning of current window */
if ((blk_expected % client->wsize) == 0)
blk_expected -= client->wsize + 1;
else
blk_expected -= (blk_expected % client->wsize) - 1;
client->blk_expected = blk_expected;
client->blk_offset = 0;
ret = write(client->fd, buf + 4, payload);
if (ret < 0) {
/* XXX: report error */
printf("[TQFTP] failed to write data\n");
return -1;
}
tftp_send_ack(client->sock, block);
client->blk_expected++;
return payload == 512 ? 1 : 0;
/* Copy the data to the destination buffer */
memcpy(client->rw_buf + client->blk_offset, buf, payload);
client->blk_offset += payload;
/* Write to file if all the wsize blocks are recieved */
if (block % client->wsize == 0) {
ret = write(client->fd, client->rw_buf, client->blk_offset);
if (ret < 0) {
/* XXX: report error */
printf("[TQFTP] failed to write data\n");
return -1;
}
client->blk_offset = 0;
tftp_send_ack(client->sock, block);
}
return payload == client->blksize ? 1 : 0;
}
static void client_close_and_free(struct tftp_client *client)
@@ -541,6 +597,8 @@ static void client_close_and_free(struct tftp_client *client)
list_del(&client->node);
close(client->sock);
close(client->fd);
free (client->blk_buf);
free (client->rw_buf);
free(client);
}
@@ -571,8 +629,6 @@ int main(int argc, char **argv)
exit(1);
}
zstd_init();
for (;;) {
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
@@ -674,7 +730,6 @@ int main(int argc, char **argv)
}
close(fd);
zstd_free();
return 0;
}
-2
View File
@@ -1,7 +1,5 @@
[Unit]
Description=QRTR TFTP service
Requires=qrtr-ns.service
After=qrtr-ns.service
[Service]
ExecStart=@prefix@/bin/tqftpserv
+19 -8
View File
@@ -24,6 +24,7 @@
#ifndef ANDROID
#define FIRMWARE_BASE "/lib/firmware/"
#define TQFTPSERV_TMP "/tmp/tqftpserv"
#define UPDATES_DIR "updates/"
#else
#define FIRMWARE_BASE "/vendor/firmware/"
#define TQFTPSERV_TMP "/data/vendor/tmp/tqftpserv"
@@ -129,17 +130,25 @@ static int translate_readonly(const char *file)
}
/* now try with base path */
if (strlen(FIRMWARE_BASE) + strlen(firmware_value) + 1 +
if (strlen(FIRMWARE_BASE) + strlen(UPDATES_DIR) + strlen(firmware_value) + 1 +
strlen(file) + 1 > sizeof(path))
continue;
strcpy(path, FIRMWARE_BASE);
strcat(path, UPDATES_DIR);
strcat(path, firmware_path);
strcat(path, "/");
strcat(path, file);
fd = open_maybe_compressed(path);
if (fd < 0) {
strcpy(path, FIRMWARE_BASE);
strcat(path, firmware_path);
strcat(path, "/");
strcat(path, file);
fd = open_maybe_compressed(path);
}
if (fd >= 0)
break;
@@ -217,16 +226,18 @@ int translate_open(const char *path, int flags)
*/
static int open_maybe_compressed(const char *path)
{
char *path_with_zstd_extension = NULL;
int fd = -1;
if (access(path, F_OK) == 0) {
fd = open(path, O_RDONLY);
} else {
char *path_with_zstd_extension = NULL;
asprintf(&path_with_zstd_extension, "%s%s", path, ZSTD_EXTENSION);
if (access(path, F_OK) == 0)
return open(path, O_RDONLY);
asprintf(&path_with_zstd_extension, "%s%s", path, ZSTD_EXTENSION);
if (access(path_with_zstd_extension, F_OK) == 0)
fd = zstd_decompress_file(path_with_zstd_extension);
free(path_with_zstd_extension);
}
free(path_with_zstd_extension);
return fd;
}
+14 -21
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2024, Stefan Hansson
* Copyright (c) 2024, Emil Velikov
*/
/* For memfd_create */
@@ -8,6 +9,7 @@
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
@@ -17,24 +19,6 @@
#include "zstd-decompress.h"
static ZSTD_DCtx *zstd_context = NULL;
/**
* zstd_init() - set up state for decompression. Needs to be called before zstd_decompress_file()
*/
void zstd_init()
{
zstd_context = ZSTD_createDCtx();
}
/**
* zstd_free() - free state used for decompression. zstd_decompress_file() may not be called after this
*/
void zstd_free()
{
ZSTD_freeDCtx(zstd_context);
}
/**
* zstd_decompress_file() - decompress a zstd-compressed file
* @filename: path to a file to decompress
@@ -46,13 +30,13 @@ int zstd_decompress_file(const char *filename)
/* Figure out the size of the file. */
struct stat file_stat;
if (stat(filename, &file_stat) == -1) {
perror("stat failed");
fprintf(stderr, "stat %s failed (%s)\n", filename, strerror(errno));
return -1;
}
const size_t file_size = file_stat.st_size;
const int input_file_fd = open(filename, 0);
const int input_file_fd = open(filename, O_RDONLY);
if (input_file_fd == -1) {
perror("open failed");
return -1;
@@ -69,34 +53,43 @@ int zstd_decompress_file(const char *filename)
const unsigned long long decompressed_size = ZSTD_getFrameContentSize(compressed_buffer, file_size);
if (decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN) {
fprintf(stderr, "Content size could not be determined for %s\n", filename);
munmap(compressed_buffer, file_size);
return -1;
}
if (decompressed_size == ZSTD_CONTENTSIZE_ERROR) {
fprintf(stderr, "Error getting content size for %s\n", filename);
munmap(compressed_buffer, file_size);
return -1;
}
void* const decompressed_buffer = malloc((size_t)decompressed_size);
if (decompressed_buffer == NULL) {
perror("malloc failed");
munmap(compressed_buffer, file_size);
return -1;
}
const size_t return_size = ZSTD_decompressDCtx(zstd_context, decompressed_buffer, decompressed_size, compressed_buffer, file_size);
const size_t return_size = ZSTD_decompress(decompressed_buffer, decompressed_size, compressed_buffer, file_size);
if (ZSTD_isError(return_size)) {
fprintf(stderr, "ZSTD_decompress failed: %s\n", ZSTD_getErrorName(return_size));
free(decompressed_buffer);
munmap(compressed_buffer, file_size);
return -1;
}
const int output_file_fd = memfd_create(filename, 0);
if (output_file_fd == -1) {
perror("memfd_create failed");
free(decompressed_buffer);
munmap(compressed_buffer, file_size);
return -1;
}
if (write(output_file_fd, decompressed_buffer, decompressed_size) != decompressed_size) {
perror("write failed");
close(output_file_fd);
free(decompressed_buffer);
munmap(compressed_buffer, file_size);
return -1;
}
+8 -2
View File
@@ -8,8 +8,14 @@
#include <stdbool.h>
void zstd_init();
void zstd_free();
#ifdef HAVE_ZSTD
int zstd_decompress_file(const char *filename);
#else
static int zstd_decompress_file(const char *filename)
{
fprintf(stderr, "Built without ZSTD support\n");
return -1;
}
#endif
#endif