Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package uftpd for openSUSE:Factory checked 
in at 2026-09-10 17:41:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/uftpd (Old)
 and      /work/SRC/openSUSE:Factory/.uftpd.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "uftpd"

Thu Sep 10 17:41:28 2026 rev:15 rq:1376959 version:2.17

Changes:
--------
--- /work/SRC/openSUSE:Factory/uftpd/uftpd.changes      2026-06-22 
17:37:00.749672183 +0200
+++ /work/SRC/openSUSE:Factory/.uftpd.new.1265/uftpd.changes    2026-09-10 
17:41:34.716170297 +0200
@@ -1,0 +2,16 @@
+Wed Sep  9 19:55:46 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 2.17
+  Changes
+  * New tests for TFTP option negotiation and malformed requests.
+  Fixes
+  * TFTP: reject packets too short for the header their opcode
+    implies.
+  * TFTP: bound RRQ/WRQ option parsing to the received packet, and
+    defer the buffer reallocation until parsing is done.
+  * TFTP: clamp a negotiated blksize to the RFC 2348 maximum, 65464.
+  * TFTP: ignore DATA packets when no transfer is open.
+  * TFTP: ERROR messages used strerror() text for what are protocol
+    codes.
+
+-------------------------------------------------------------------

Old:
----
  uftpd-2.16.tar.gz

New:
----
  uftpd-2.17.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ uftpd.spec ++++++
--- /var/tmp/diff_new_pack.skBiUv/_old  2026-09-10 17:41:35.553205325 +0200
+++ /var/tmp/diff_new_pack.skBiUv/_new  2026-09-10 17:41:35.555205409 +0200
@@ -18,7 +18,7 @@
 
 
 Name:           uftpd
-Version:        2.16
+Version:        2.17
 Release:        0
 Summary:        A combined TFTP/FTP server
 License:        ISC

++++++ uftpd-2.16.tar.gz -> uftpd-2.17.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/.github/CHECKLIST.md 
new/uftpd-2.17/.github/CHECKLIST.md
--- old/uftpd-2.16/.github/CHECKLIST.md 1970-01-01 01:00:00.000000000 +0100
+++ new/uftpd-2.17/.github/CHECKLIST.md 2026-09-09 18:52:23.000000000 +0200
@@ -0,0 +1,140 @@
+Release Checklist
+=================
+
+Steps to cut a new uftpd release.  The version number is `X.Y` for a
+release and `X.Y-rcN` for a release candidate; the GIT tag is the same
+with a leading `v`.
+
+Everything below happens on `master`, in the order given.  Nothing is
+pushed until the whole list is ticked off -- a pushed tag is a released
+tag, and the only recovery from a bad release is the next one.
+
+
+Before Tagging
+--------------
+
+ - [ ] `master` is green in [Bob the Builder][], **including the
+       `debian` job**.  That job builds the `.deb` and runs lintian, so
+       packaging breakage surfaces here rather than in the release job,
+       which only runs once the tag is already immutable
+ - [ ] `ChangeLog.md`: add a `[vX.Y][] - YYYY-MM-DD` section with the
+       Changes and Fixes for *this* release, and add the matching
+       `[vX.Y]: .../compare/vX.Y-1...vX.Y` link at the bottom
+ - [ ] `debian/changelog`: add a matching `uftpd (X.Y) stable;` entry.
+       Easiest is `dch -v X.Y --distribution stable`, which stamps the
+       trailer with your `DEBEMAIL`/`DEBFULLNAME` identity.  The entry
+       covers what changed in the *package*, so list the packaging
+       changes too, not just the upstream ones
+ - [ ] `configure.ac`: bump the version in `AC_INIT()`
+ - [ ] Commit the three together: `git commit -s -m "Bump version for
+       vX.Y release"`
+
+> **Note:** `debian/changelog` is easy to forget because nothing in the
+> build fails without it -- the `.deb` just silently keeps the old
+> version.  It was missed for v2.16.
+
+
+Verify
+------
+
+ - [ ] `./autogen.sh && ./configure`
+ - [ ] `make check` -- see [test/README.md][] for what the suite needs
+ - [ ] `make distcheck`
+ - [ ] `make package`, if you can (see [Packaging][] below)
+
+
+Tag and Push
+------------
+
+ - [ ] `git tag -a vX.Y -m "uftpd vX.Y"`
+ - [ ] `git push`
+ - [ ] `git push --tags`
+
+Pushing the tag starts [Release General][].  The tarball and the `.deb`
+build as two parallel jobs, and a third publishes the release from their
+artifacts once both are green, with the top `ChangeLog.md` section as the
+body.  Release candidates -- `-alpha`, `-beta`, `-rc[0-9]*` -- are marked
+as pre-releases automatically.
+
+The `.deb` job refuses to build unless `debian/changelog` matches the tag,
+comparing with `-` mapped to `~` so a `vX.Y-rcN` tag expects `X.Y~rcN`.
+That is the net under the note above: forget the changelog and the release
+fails loudly rather than shipping a package with the previous version.
+
+ - [ ] Check the workflow went green and the release page looks right
+
+
+After
+-----
+
+ - [ ] Upload the package to [deb.troglobit.com][].  The `.deb` on the
+       release page is built on `ubuntu-latest` and links against that
+       runner's libuEv/libite, so it is not a substitute for a package
+       built for each supported distribution
+ - [ ] Announce
+
+
+Start the Next Cycle
+--------------------
+
+Right after a release, so master never sits on a released version:
+
+ - [ ] `configure.ac`: set the version to the next `X.Y-dev`
+ - [ ] `debian/changelog`: open an `uftpd (X.Y~dev)` entry.  Note the
+       tilde, not the hyphen used in `configure.ac`: this is a native
+       package, so the version may not contain a hyphen, and `2.17-dev`
+       would sort *above* `2.17` and make the release look like a
+       downgrade.  `2.17~dev` sorts between `2.16` and `2.17`
+ - [ ] `ChangeLog.md`: open a `[vX.Y][UNRELEASED] - ` section, point
+       `[UNRELEASED]` at `compare/vX.Y-1...HEAD`, and freeze the link of
+       the release just made at `compare/vX.Y-2...vX.Y-1`
+
+
+Packaging
+---------
+
+The packaging targets Ubuntu 24.04 LTS, which is what `ubuntu-latest`
+resolves to and what the maintainer runs.  Two consequences:
+
+ - `debian/control` declares `Standards-Version: 4.6.2`.  That is what
+   lintian 2.117 (24.04) considers current; declaring the newer 4.7.2
+   earns a `newer-standards-version` warning on every build.  Bump it
+   when CI moves to a newer runner
+ - `debian/control` declares `debhelper-compat (= 13)`
+
+Building the package needs a few tools beyond the normal build deps:
+
+```console
+$ sudo apt install devscripts debhelper lintian po-debconf
+$ ./autogen.sh
+$ ./configure
+$ make package
+```
+
+`make package` runs `debuild ... --lintian-opts --profile debian`, so
+lintian errors fail the build.  Both the source and binary packages are
+expected to be entirely lintian clean.
+
+`dpkg-shlibdeps` resolves the libuEv and libite dependencies from the
+installed *packages*, so `make package` fails if you run those libraries
+from a source install under `/usr/local`:
+
+```
+dpkg-shlibdeps: error: no dependency information found for
+/usr/local/lib/libite.so.5
+```
+
+Install `libuev-dev` and `libite-dev` from apt to build the package
+locally, or just let the `debian` CI job do it for you.
+
+When changing `debian/templates`, refresh the translation template:
+
+```console
+$ debconf-updatepo --podir debian/po
+```
+
+[Bob the Builder]:  
https://github.com/troglobit/uftpd/actions/workflows/build.yml
+[Release General]:  
https://github.com/troglobit/uftpd/actions/workflows/release.yml
+[deb.troglobit.com]: https://deb.troglobit.com/
+[test/README.md]:   
https://github.com/troglobit/uftpd/blob/master/test/README.md
+[Packaging]:        #packaging
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/.github/CONTRIBUTING.md 
new/uftpd-2.17/.github/CONTRIBUTING.md
--- old/uftpd-2.16/.github/CONTRIBUTING.md      2026-06-21 12:56:31.000000000 
+0200
+++ new/uftpd-2.17/.github/CONTRIBUTING.md      2026-09-09 18:52:23.000000000 
+0200
@@ -66,6 +66,14 @@
     Signed-off-by: Jane Doe <[email protected]>
 
 
+Making a Release
+----------------
+
+Maintainers: the steps for cutting a release, including updating both
+`ChangeLog.md` and `debian/changelog`, are in [CHECKLIST.md][release].
+
+
 [github]:   https://github.com/troglobit/uftpd/
+[release]:  https://github.com/troglobit/uftpd/blob/master/.github/CHECKLIST.md
 [KNF]:      https://en.wikipedia.org/wiki/Kernel_Normal_Form
 [gitbook]:  https://git-scm.com/book/ch5-2.html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/.github/workflows/build.yml 
new/uftpd-2.17/.github/workflows/build.yml
--- old/uftpd-2.16/.github/workflows/build.yml  2026-06-21 12:56:31.000000000 
+0200
+++ new/uftpd-2.17/.github/workflows/build.yml  2026-09-09 18:52:23.000000000 
+0200
@@ -16,6 +16,7 @@
     # Verify we can build on latest Ubuntu with both gcc and clang
     name: ${{ matrix.compiler }}
     runs-on: ubuntu-latest
+    timeout-minutes: 15
     strategy:
       matrix:
         compiler: [gcc, clang]
@@ -25,9 +26,11 @@
     steps:
       - name: Install dependencies
         run: |
-          curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
-          echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" 
\
-               | sudo tee /etc/apt/sources.list.d/troglobit.list
+          # The runners' azure.archive.ubuntu.com mirror intermittently
+          # stalls.  Fail over to the fallback mirror in seconds instead
+          # of grinding through apt's two-minute default timeouts.
+          printf 'Acquire::Retries "3";\nAcquire::http::Timeout 
"15";\nAcquire::https::Timeout "15";\n' \
+              | sudo tee /etc/apt/apt.conf.d/99-timeout >/dev/null
           sudo apt-get -y update
           sudo apt-get -y install tree ftp tnftp tftp-hpa python3 libuev-dev 
libite-dev
           # Build and run the test suite using all available cores
@@ -55,3 +58,64 @@
           ulimit -n 1024
           # Each test runs in its own namespace, so they parallelize
           LD_LIBRARY_PATH=/tmp/lib make check
+      - name: Archive test logs
+        if: always()
+        uses: actions/upload-artifact@v7
+        with:
+          name: test-logs-${{ matrix.compiler }}
+          path: |
+            test/*.log
+            test/*.trs
+          if-no-files-found: warn
+          retention-days: 14
+
+  package:
+    # Verify the Debian packaging still builds and passes lintian.  The
+    # release job runs the same target, but only after the tag is pushed
+    # and immutable, so catch a broken debian/ here instead.
+    name: debian
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    steps:
+      - name: Install dependencies
+        run: |
+          # The runners' azure.archive.ubuntu.com mirror intermittently
+          # stalls.  Fail over to the fallback mirror in seconds instead
+          # of grinding through apt's two-minute default timeouts.
+          printf 'Acquire::Retries "3";\nAcquire::http::Timeout 
"15";\nAcquire::https::Timeout "15";\n' \
+              | sudo tee /etc/apt/apt.conf.d/99-timeout >/dev/null
+          sudo apt-get -y update
+          sudo apt-get -y install build-essential pkgconf libuev-dev 
libite-dev \
+                                  devscripts debhelper lintian po-debconf
+      - uses: actions/checkout@v6
+      - name: Configure
+        run: |
+          ./autogen.sh
+          ./configure
+      - name: Build package
+        run: |
+          # The suite already ran in the build job, and it needs user
+          # namespaces the runner clamps down on.  dh_auto_test honours
+          # nocheck, so skip it here rather than run it twice.
+          DEB_BUILD_OPTIONS=nocheck make package
+      - name: Collect package
+        if: always()
+        run: |
+          # debuild drops its output next to the source tree; artifacts
+          # must live inside the workspace to be uploadable.
+          mkdir -p dist
+          mv ../uftpd_*.deb ../uftpd-dbgsym_*.ddeb dist/ 2>/dev/null || true
+          mv ../uftpd_*.changes ../uftpd_*.buildinfo dist/ 2>/dev/null || true
+          ls -lF dist/
+      - name: Inspect package
+        run: |
+          dpkg -I dist/uftpd_*_amd64.deb
+          dpkg -c dist/uftpd_*_amd64.deb
+      - name: Archive package
+        if: always()
+        uses: actions/upload-artifact@v7
+        with:
+          name: uftpd-deb
+          path: dist/
+          if-no-files-found: warn
+          retention-days: 14
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/.github/workflows/coverity.yml 
new/uftpd-2.17/.github/workflows/coverity.yml
--- old/uftpd-2.16/.github/workflows/coverity.yml       2026-06-21 
12:56:31.000000000 +0200
+++ new/uftpd-2.17/.github/workflows/coverity.yml       2026-09-09 
18:52:23.000000000 +0200
@@ -14,6 +14,7 @@
 jobs:
   coverity:
     runs-on: ubuntu-latest
+    timeout-minutes: 30
     env:
       MAKEFLAGS: -j3
     steps:
@@ -53,9 +54,11 @@
           tar xzf coverity-latest.tar.gz --strip 1 -C coverity
       - name: Install dependencies
         run: |
-          curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
-          echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" 
\
-               | sudo tee /etc/apt/sources.list.d/troglobit.list
+          # The runners' azure.archive.ubuntu.com mirror intermittently
+          # stalls.  Fail over to the fallback mirror in seconds instead
+          # of grinding through apt's two-minute default timeouts.
+          printf 'Acquire::Retries "3";\nAcquire::http::Timeout 
"15";\nAcquire::https::Timeout "15";\n' \
+              | sudo tee /etc/apt/apt.conf.d/99-timeout >/dev/null
           sudo apt-get -y update
           sudo apt-get -y install pkg-config libuev-dev libite-dev
       - uses: actions/checkout@v6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/.github/workflows/release.yml 
new/uftpd-2.17/.github/workflows/release.yml
--- old/uftpd-2.16/.github/workflows/release.yml        2026-06-21 
12:56:31.000000000 +0200
+++ new/uftpd-2.17/.github/workflows/release.yml        2026-09-09 
18:52:23.000000000 +0200
@@ -9,33 +9,25 @@
   contents: write
 
 jobs:
-  release:
-    name: Build and upload release tarball
+  tarball:
+    name: Release tarball
     if: startsWith(github.ref, 'refs/tags/')
     runs-on: ubuntu-latest
+    timeout-minutes: 30
     steps:
       - uses: actions/checkout@v6
         with:
           fetch-depth: 0
       - name: Installing dependencies ...
         run: |
-          curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
-          echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" 
\
-               | sudo tee /etc/apt/sources.list.d/troglobit.list
+          # The runners' azure.archive.ubuntu.com mirror intermittently
+          # stalls.  Fail over to the fallback mirror in seconds instead
+          # of grinding through apt's two-minute default timeouts.
+          printf 'Acquire::Retries "3";\nAcquire::http::Timeout 
"15";\nAcquire::https::Timeout "15";\n' \
+              | sudo tee /etc/apt/apt.conf.d/99-timeout >/dev/null
           sudo apt-get -y update
-          sudo apt-get -y install ftp tnftp tftp-hpa python3 libuev-dev 
libite-dev
-      - name: Setting release variables ...
-        id: build
-        run: |
-          ver=${GITHUB_REF#refs/tags/}
-          ver=${ver#v}
-          if echo "$ver" | grep -qE '(-alpha|-beta|-rc)[0-9]*$'; then
-            echo "pre=true"  >> $GITHUB_OUTPUT
-            echo "latest=false" >> $GITHUB_OUTPUT
-          else
-            echo "pre=false" >> $GITHUB_OUTPUT
-            echo "latest=true" >> $GITHUB_OUTPUT
-          fi
+          sudo apt-get -y install ftp tnftp tftp-hpa python3 \
+                                  build-essential pkgconf libuev-dev libite-dev
       - name: Creating Makefiles ...
         run: |
           ./autogen.sh
@@ -46,9 +38,83 @@
       - name: Build release ...
         run: |
           make release
-          ls -lF ../
           mkdir -p artifacts/
           mv ../*.tar.gz artifacts/
+          ls -lF artifacts/
+      - name: Upload tarball
+        uses: actions/upload-artifact@v7
+        with:
+          name: release-tarball
+          path: artifacts/*
+          if-no-files-found: error
+          retention-days: 1
+
+  debian:
+    name: Debian package
+    if: startsWith(github.ref, 'refs/tags/')
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    steps:
+      - uses: actions/checkout@v6
+      - name: Installing dependencies ...
+        run: |
+          printf 'Acquire::Retries "3";\nAcquire::http::Timeout 
"15";\nAcquire::https::Timeout "15";\n' \
+              | sudo tee /etc/apt/apt.conf.d/99-timeout >/dev/null
+          sudo apt-get -y update
+          sudo apt-get -y install build-essential pkgconf libuev-dev 
libite-dev \
+                                  devscripts debhelper lintian po-debconf
+      - name: Build Debian package ...
+        run: |
+          # A native package version cannot carry a hyphen, so the vX.Y-rcN
+          # tag is X.Y~rcN in debian/changelog.
+          ver=$(echo "${GITHUB_REF_NAME#v}" | tr '-' '~')
+          deb=$(dpkg-parsechangelog -S Version)
+          if [ "$deb" != "$ver" ]; then
+              echo "debian/changelog is at $deb, tag says $ver"
+              exit 1
+          fi
+          ./autogen.sh
+          ./configure
+          # The tarball job runs the suite via distcheck.
+          DEB_BUILD_OPTIONS=nocheck make package
+          mkdir -p artifacts/
+          mv ../uftpd_*.deb artifacts/
+          dpkg -I artifacts/*.deb
+          dpkg -c artifacts/*.deb
+      - name: Upload package
+        uses: actions/upload-artifact@v7
+        with:
+          name: release-debian
+          path: artifacts/*
+          if-no-files-found: error
+          retention-days: 1
+
+  release:
+    name: Publish release
+    if: startsWith(github.ref, 'refs/tags/')
+    needs: [ tarball, debian ]
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    steps:
+      - uses: actions/checkout@v6
+      - name: Setting release variables ...
+        id: build
+        run: |
+          ver=${GITHUB_REF#refs/tags/}
+          ver=${ver#v}
+          if echo "$ver" | grep -qE '(-alpha|-beta|-rc)[0-9]*$'; then
+            echo "pre=true"  >> $GITHUB_OUTPUT
+            echo "latest=false" >> $GITHUB_OUTPUT
+          else
+            echo "pre=false" >> $GITHUB_OUTPUT
+            echo "latest=true" >> $GITHUB_OUTPUT
+          fi
+      - name: Download artifacts ...
+        uses: actions/download-artifact@v7
+        with:
+          pattern: release-*
+          path: artifacts
+          merge-multiple: true
       - name: Extract ChangeLog entry ...
         run: |
           awk '/-----*/{if (x == 1) exit; x=1;next}x' ChangeLog.md \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/ChangeLog.md new/uftpd-2.17/ChangeLog.md
--- old/uftpd-2.16/ChangeLog.md 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/ChangeLog.md 2026-09-09 18:52:23.000000000 +0200
@@ -3,6 +3,32 @@
 
 All notable changes to the project are documented in this file.
 
+
+[v2.17][] - 2026-09-09
+----------------------
+
+### Changes
+- Debian packaging modernized: debhelper compat 13, machine-readable
+  copyright, translatable debconf prompts, lintian clean
+- The `.deb` is built and lintian checked in CI, and attached to each
+  GitHub release
+- New tests for TFTP option negotiation and malformed requests
+- `make package` no longer fails where the kernel denies unprivileged
+  user namespaces; the test suite skips instead
+
+### Fixes
+- TFTP: reject packets too short for the header their opcode implies
+- TFTP: bound RRQ/WRQ option parsing to the received packet, and defer
+  the buffer reallocation until parsing is done
+- TFTP: clamp a negotiated `blksize` to the RFC 2348 maximum, 65464
+- TFTP: ignore DATA packets when no transfer is open
+- TFTP: ERROR replies were malformed, and never sent at all on hardened
+  builds
+- TFTP: ERROR messages used `strerror()` text for what are protocol codes
+- The `ftp` user is only removed on purge, no longer on every removal
+- Missing `#DEBHELPER#` token in the maintainer scripts
+
+
 [v2.16][] - 2026-06-21
 ----------------------
 
@@ -551,8 +577,9 @@
   Lines must end in the old `\r\n` format, rather than UNIX `\n`.
 
 
-[UNRELEASED]:    https://github.com/troglobit/uftpd/compare/v2.15...HEAD
-[v2.16]:         https://github.com/troglobit/uftpd/compare/v2.15...HEAD
+[UNRELEASED]:    https://github.com/troglobit/uftpd/compare/v2.17...HEAD
+[v2.17]:         https://github.com/troglobit/uftpd/compare/v2.16...v2.17
+[v2.16]:         https://github.com/troglobit/uftpd/compare/v2.15...v2.16
 [v2.15]:         https://github.com/troglobit/uftpd/compare/v2.14...v2.15
 [v2.14]:         https://github.com/troglobit/uftpd/compare/v2.13...v2.14
 [v2.13]:         https://github.com/troglobit/uftpd/compare/v2.12...v2.13
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/LICENSE new/uftpd-2.17/LICENSE
--- old/uftpd-2.16/LICENSE      2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/LICENSE      2026-09-09 18:52:23.000000000 +0200
@@ -1,4 +1,4 @@
-Copyright (C) 2014-2021  Joachim Wiberg <[email protected]>
+Copyright (C) 2014-2026  Joachim Wiberg <[email protected]>
 
 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/README.md new/uftpd-2.17/README.md
--- old/uftpd-2.16/README.md    2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/README.md    2026-09-09 18:52:23.000000000 +0200
@@ -137,10 +137,11 @@
 ```
 
 uftpd, as well as its dependencies, can be built as `.deb` packages on
-Debian or Ubuntu based distributions.  Download and install each of the
-dependencies, and then run
+Debian or Ubuntu based distributions.  Install the packaging tools, and
+the `-dev` packages of the dependencies, then run
 
 ```console
+$ sudo apt install devscripts debhelper lintian po-debconf
 $ ./autogen.sh      # Only needed if using GIT sources
 $ ./configure
 ...
@@ -148,6 +149,10 @@
 ...
 ```
 
+Note, `dpkg-shlibdeps` looks the dependencies up in the installed `.deb`
+packages, so `libuev-dev` and `libite-dev` must come from apt -- a
+libuEv or libite installed from source under `/usr/local` is not enough.
+
 The `.deb` package takes care of setting up `/etc/inetd.conf`, create an
 `ftp` user and an `/srv/ftp` home directory with write permissions for
 all members of the `users` group.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/configure.ac new/uftpd-2.17/configure.ac
--- old/uftpd-2.16/configure.ac 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/configure.ac 2026-09-09 18:52:23.000000000 +0200
@@ -1,4 +1,4 @@
-AC_INIT([uftpd], [2.16], [https://github.com/troglobit/uftpd/issues], [],
+AC_INIT([uftpd], [2.17], [https://github.com/troglobit/uftpd/issues], [],
        [https://troglobit.com/projects/uftpd/])
 AC_CONFIG_AUX_DIR(aux)
 AM_INIT_AUTOMAKE([1.11 foreign])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/README.Debian 
new/uftpd-2.17/debian/README.Debian
--- old/uftpd-2.16/debian/README.Debian 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/README.Debian 2026-09-09 18:52:23.000000000 +0200
@@ -3,6 +3,6 @@
 
 uftpd is a true UNIX TFTP/FTP daemon, it serves files, and nothing more.
 It runs from inetd on ports specified in /etc/services, serving files
-from the ftp user's $HOME, /src/ftp -- it just works.
+from the ftp user's $HOME, /srv/ftp -- it just works.
 
  -- Joachim Wiberg <[email protected]>, Sat, 11 Dec 2021 08:46:52 +0100
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/changelog 
new/uftpd-2.17/debian/changelog
--- old/uftpd-2.16/debian/changelog     2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/changelog     2026-09-09 18:52:23.000000000 +0200
@@ -1,3 +1,46 @@
+uftpd (2.17) stable; urgency=medium
+
+  * TFTP: reject packets too short for the header their opcode implies
+  * TFTP: bound RRQ/WRQ option parsing, defer the buffer reallocation
+    until parsing is done, and clamp a negotiated blksize to the RFC
+    2348 maximum
+  * TFTP: ignore DATA packets when no transfer is open
+  * TFTP: fix ERROR replies, malformed and never sent on hardened builds
+  * Switch to debhelper compat level 13, drop the obsolete autoreconf and
+    systemd dh sequences
+  * Update to Standards-Version 4.6.2, declare Rules-Requires-Root: no
+  * Convert copyright to machine-readable DEP-5 format
+  * Make the debconf templates translatable, using po-debconf
+  * Add missing #DEBHELPER# token to the maintainer scripts
+  * Only remove the ftp user on purge, not on every package removal
+  * Use getent(1) instead of parsing /etc/passwd in postinst
+  * Fix ftp home directory typo in README.Debian, /srv/ftp not /src/ftp
+  * Update copyright years to 2026
+
+ -- Joachim Wiberg <[email protected]>  Wed, 09 Sep 2026 18:49:36 +0200
+
+uftpd (2.16) stable; urgency=medium
+
+  * Issue #40: IPv6 support, separate IPv6 listeners for FTP and TFTP,
+    alongside the IPv4 ones.  The FTP data channel works over IPv6 using
+    EPSV and EPRT, the legacy PASV/PORT commands stay IPv4 only
+  * Fix #32: session failing during setup returned into the parent's
+    accept loop instead of exiting, becoming a rogue listener forking
+    endless sessions until the system ran out of PIDs
+  * Fix #41: retransmitted TFTP WRQ reopened the destination file,
+    leaking a descriptor on every retry and truncating received data
+  * Fix #42: bogus file size in FTP MLST/MLSD on 32-bit platforms
+  * Fix #43: malformed TFTP OACK, trailing NUL bytes rejected by strict
+    clients, e.g. Cisco switches and U-Boot
+  * Fix #44: TFTP server ignored the ACK block number, a lost DATA
+    packet is now detected and the missing block retransmitted
+  * Fix #45: TFTP transfers larger than 65535 blocks failed at the
+    16-bit block number rollover
+  * Fix TFTP ACK packets padded with four stray NUL bytes when
+    receiving an upload (WRQ)
+
+ -- Joachim Wiberg <[email protected]>  Sun, 21 Jun 2026 12:53:32 +0200
+
 uftpd (2.15) stable; urgency=medium
 
   * Silence some developer debug messages
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/compat new/uftpd-2.17/debian/compat
--- old/uftpd-2.16/debian/compat        2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/compat        1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-10
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/control 
new/uftpd-2.17/debian/control
--- old/uftpd-2.16/debian/control       2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/control       2026-09-09 18:52:23.000000000 +0200
@@ -2,9 +2,12 @@
 Section: net
 Priority: optional
 Maintainer: Joachim Wiberg <[email protected]>
-Build-Depends: debhelper (>= 10), libuev-dev, libite-dev
-Standards-Version: 4.3.0
+Build-Depends: debhelper-compat (= 13), po-debconf, pkgconf, libuev-dev (>= 
2.2.0), libite-dev (>= 1.5.0)
+Standards-Version: 4.6.2
+Rules-Requires-Root: no
 Homepage: https://troglobit.com/projects/uftpd/
+Vcs-Browser: https://github.com/troglobit/uftpd
+Vcs-Git: https://github.com/troglobit/uftpd.git
 
 Package: uftpd
 Architecture: any
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/copyright 
new/uftpd-2.17/debian/copyright
--- old/uftpd-2.16/debian/copyright     2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/copyright     2026-09-09 18:52:23.000000000 +0200
@@ -1,11 +1,17 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: uftpd
+Upstream-Contact: Joachim Wiberg <[email protected]>
+Source: https://github.com/troglobit/uftpd
 
-Copyright: (c) 2014-2021  Joachim Wiberg <[email protected]>
+Files: *
+Copyright: 2014-2026 Joachim Wiberg <[email protected]>
+License: ISC
 
 License: ISC
  Permission to use, copy, modify, and/or distribute this software for any
  purpose with or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.
- 
+ .
  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
@@ -13,4 +19,3 @@
  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/po/POTFILES.in 
new/uftpd-2.17/debian/po/POTFILES.in
--- old/uftpd-2.16/debian/po/POTFILES.in        1970-01-01 01:00:00.000000000 
+0100
+++ new/uftpd-2.17/debian/po/POTFILES.in        2026-09-09 18:52:23.000000000 
+0200
@@ -0,0 +1 @@
+[type: gettext/rfc822deb] templates
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/po/templates.pot 
new/uftpd-2.17/debian/po/templates.pot
--- old/uftpd-2.16/debian/po/templates.pot      1970-01-01 01:00:00.000000000 
+0100
+++ new/uftpd-2.17/debian/po/templates.pot      2026-09-09 18:52:23.000000000 
+0200
@@ -0,0 +1,60 @@
+# Translation of uftpd debconf templates.
+# Copyright (C) 2014-2026 Joachim Wiberg <[email protected]>
+# This file is distributed under the same license as the uftpd package.
+# Joachim Wiberg <[email protected]>, 2026.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: uftpd\n"
+"Report-Msgid-Bugs-To: [email protected]\n"
+"POT-Creation-Date: 2026-08-19 15:13+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <[email protected]>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: boolean
+#. Description
+#: ../templates:1001
+msgid "Enable FTP service?"
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../templates:1001
+msgid ""
+"uftpd can serve files over FTP, started on demand by the inetd super "
+"server.  The service is anonymous and read-only, serving files from the ftp "
+"user's home directory, /srv/ftp."
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../templates:1001
+msgid "Decline this if you only want the TFTP service."
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../templates:2001
+msgid "Enable TFTP service?"
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../templates:2001
+msgid ""
+"uftpd can serve files over TFTP, started on demand by the inetd super "
+"server.  The service is read-only, serving files from the ftp user's home "
+"directory, /srv/ftp."
+msgstr ""
+
+#. Type: boolean
+#. Description
+#: ../templates:2001
+msgid "Decline this if you only want the FTP service."
+msgstr ""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/postinst 
new/uftpd-2.17/debian/postinst
--- old/uftpd-2.16/debian/postinst      2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/postinst      2026-09-09 18:52:23.000000000 +0200
@@ -2,53 +2,57 @@
 
 set -e
 
-[ "$1" = "configure" ] || exit 0
-
-# Source debconf library.
-. /usr/share/debconf/confmodule
-
-FTPENTRY="ftp          stream  tcp     nowait  root    /usr/sbin/tcpd  in.ftpd"
-TFTPENTRY="tftp                dgram   udp     wait    root    /usr/sbin/tcpd  
in.tftpd"
-
-if [ ! -f /etc/inetd.conf -a -d /etc/xinetd.d -a -x /usr/sbin/xinetd ]; then
-       cat <<-TEXT
-               -------------------------------------------------
-               There is no configuration support for using uftpd
-               under the control of xinetd.
-               -------------------------------------------------
-       TEXT
+if [ "$1" = "configure" ]; then
+       # Source debconf library.
+       . /usr/share/debconf/confmodule
+
+       FTPENTRY="ftp           stream  tcp     nowait  root    /usr/sbin/tcpd  
in.ftpd"
+       TFTPENTRY="tftp         dgram   udp     wait    root    /usr/sbin/tcpd  
in.tftpd"
+
+       if [ ! -f /etc/inetd.conf -a -d /etc/xinetd.d -a -x /usr/sbin/xinetd ]; 
then
+               cat <<-TEXT
+                       -------------------------------------------------
+                       There is no configuration support for using uftpd
+                       under the control of xinetd.
+                       -------------------------------------------------
+               TEXT
+       fi
+
+       if grep -q '[[:blank:]]/usr/sbin/uftpd.*' /etc/inetd.conf 2>/dev/null; 
then
+               update-inetd --pattern '/usr/sbin/uftpd' --remove ".*ftp"
+       fi
+
+       update-inetd --group STANDARD --add "$FTPENTRY"
+       update-inetd --group STANDARD --add "$TFTPENTRY"
+
+       db_get uftpd/ftp
+       if [ "$RET" = "true" ]; then
+           update-inetd --enable ftp
+       else
+           update-inetd --disable ftp
+       fi
+
+       db_get uftpd/tftp
+       if [ "$RET" = "true" ]; then
+           update-inetd --enable tftp
+       else
+           update-inetd --disable tftp
+       fi
+
+       # Redirect errors from adduser since 1) user may exist already,
+       # 2) directory may exist and not be writable by user.  Ignore.
+       if ! getent passwd ftp >/dev/null; then
+               addgroup --quiet --system ftp
+               adduser --quiet --system --disabled-login --home /srv/ftp \
+                       --ingroup ftp ftp 2>/dev/null || true
+       fi
+
+       # Adjust for any previous server, users wanting to share files using
+       # TFTP/FTP should be in the users group
+       chown --changes ftp:users /srv/ftp
+       chmod --changes 0575      /srv/ftp
 fi
 
-if grep -q '[[:blank:]]/usr/sbin/uftpd.*' /etc/inetd.conf 2>/dev/null; then
-       update-inetd --pattern '/usr/sbin/uftpd' --remove ".*ftp"
-fi
-
-update-inetd --group STANDARD --add "$FTPENTRY"
-update-inetd --group STANDARD --add "$TFTPENTRY"
-
-db_get uftpd/ftp
-if [ "$RET" = "true" ]; then
-    update-inetd --enable ftp
-else
-    update-inetd --disable ftp
-fi
-
-db_get uftpd/tftp
-if [ "$RET" = "true" ]; then
-    update-inetd --enable tftp
-else
-    update-inetd --disable tftp
-fi
-
-# Redirect errors from adduser since 1) user may exist already,
-# 2) directory may exist and not be writable by user.  Ignore.
-if ! grep -q "^ftp:" /etc/passwd; then
-       addgroup --quiet --system ftp
-       adduser --quiet --system --disabled-login --home /srv/ftp \
-               --ingroup ftp ftp 2>/dev/null || true
-fi
+#DEBHELPER#
 
-# Adjust for any previous server, users wanting to share files using
-# TFTP/FTP should be in the users group
-chown --changes ftp:users /srv/ftp
-chmod --changes 0575      /srv/ftp
+exit 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/postrm new/uftpd-2.17/debian/postrm
--- old/uftpd-2.16/debian/postrm        2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/postrm        2026-09-09 18:52:23.000000000 +0200
@@ -1,4 +1,5 @@
 #!/bin/sh
+
 set -e
 
 if [ "$1" = "purge" ]; then
@@ -8,13 +9,9 @@
                update-inetd --pattern 'in.tftpd' --remove tftp
        fi
 
-       # Remove uftpd entries from db
-       if [ -f /usr/share/debconf/confmodule ]; then
-               . /usr/share/debconf/confmodule
-               db_purge
-       fi
+       deluser --quiet --system ftp || true
 fi
 
-deluser --quiet --system ftp
+#DEBHELPER#
 
 exit 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/prerm new/uftpd-2.17/debian/prerm
--- old/uftpd-2.16/debian/prerm 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/prerm 2026-09-09 18:52:23.000000000 +0200
@@ -4,3 +4,7 @@
 
 update-inetd --pattern 'in.ftpd' --multi --disable ftp
 update-inetd --pattern 'in.tftpd' --multi --disable tftp
+
+#DEBHELPER#
+
+exit 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/rules new/uftpd-2.17/debian/rules
--- old/uftpd-2.16/debian/rules 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/rules 2026-09-09 18:52:23.000000000 +0200
@@ -1,10 +1,9 @@
 #!/usr/bin/make -f
 # export DH_VERBOSE=1
 export DEB_BUILD_MAINT_OPTIONS = hardening=+all
-export DEB_BUILD_OPTIONS='parallel=1'
 
 %:
-       dh $@ --with autoreconf,systemd
+       dh $@
 
 override_dh_installchangelogs:
        dh_installchangelogs ChangeLog.md
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/debian/templates 
new/uftpd-2.17/debian/templates
--- old/uftpd-2.16/debian/templates     2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/debian/templates     2026-09-09 18:52:23.000000000 +0200
@@ -1,10 +1,19 @@
 Template: uftpd/ftp
 Type: boolean
 Default: true
-Description: Enable FTP service?
+_Description: Enable FTP service?
+ uftpd can serve files over FTP, started on demand by the inetd super
+ server.  The service is anonymous and read-only, serving files from the
+ ftp user's home directory, /srv/ftp.
+ .
+ Decline this if you only want the TFTP service.
 
 Template: uftpd/tftp
 Type: boolean
 Default: true
-Description: Enable TFTP service?
-
+_Description: Enable TFTP service?
+ uftpd can serve files over TFTP, started on demand by the inetd super
+ server.  The service is read-only, serving files from the ftp user's
+ home directory, /srv/ftp.
+ .
+ Decline this if you only want the FTP service.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/man/uftpd.8 new/uftpd-2.17/man/uftpd.8
--- old/uftpd-2.16/man/uftpd.8  2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/man/uftpd.8  2026-09-09 18:52:23.000000000 +0200
@@ -1,5 +1,5 @@
 .\"
-.\" Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+.\" Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
 .\"
 .\" Permission to use, copy, modify, and/or distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/src/common.c new/uftpd-2.17/src/common.c
--- old/uftpd-2.16/src/common.c 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/src/common.c 2026-09-09 18:52:23.000000000 +0200
@@ -1,6 +1,6 @@
 /* Common methods shared between FTP and TFTP engines
  *
- * Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+ * Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/src/ftpcmd.c new/uftpd-2.17/src/ftpcmd.c
--- old/uftpd-2.16/src/ftpcmd.c 2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/src/ftpcmd.c 2026-09-09 18:52:23.000000000 +0200
@@ -1,6 +1,6 @@
 /* FTP engine
  *
- * Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+ * Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/src/log.c new/uftpd-2.17/src/log.c
--- old/uftpd-2.16/src/log.c    2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/src/log.c    2026-09-09 18:52:23.000000000 +0200
@@ -1,6 +1,6 @@
 /* uftpd -- the no nonsense (T)FTP server
  *
- * Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+ * Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/src/tftpcmd.c new/uftpd-2.17/src/tftpcmd.c
--- old/uftpd-2.16/src/tftpcmd.c        2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/src/tftpcmd.c        2026-09-09 18:52:23.000000000 +0200
@@ -1,6 +1,6 @@
 /* TFTP Engine
  *
- * Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+ * Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -132,20 +132,43 @@
        return do_send(ctrl, ptr - ctrl->th->th_stuff);
 }
 
+/* RFC 1350 error strings, the codes are not errno values */
+static char *tftp_strerror(int code)
+{
+       switch (code) {
+       case ENOTFOUND: return "File not found";
+       case EACCESS:   return "Access violation";
+       case ENOSPACE:  return "Disk full or allocation exceeded";
+       case EBADOP:    return "Illegal TFTP operation";
+       case EBADID:    return "Unknown transfer ID";
+       case EEXISTS:   return "File already exists";
+       case ENOUSER:   return "No such user";
+       default:        return "Not defined";
+       }
+}
+
 static int send_ERROR(ctrl_t *ctrl, int code, char *str)
 {
-       size_t len;
+       size_t hdrsz, avail, len;
 
        if (!str)
-               str = strerror(code);
-       len = strlen(str);
+               str = tftp_strerror(code);
 
-       memset(ctrl->buf, 0, ctrl->segsize);
+       memset(ctrl->buf, 0, ctrl->bufsz);
 
        /* Create error message */
        ctrl->th->th_opcode = htons(ERROR);
        ctrl->th->th_code   = htons(code);
-       strlcpy(ctrl->th->th_msg, str, len);
+
+       /* Copy through buf, th_msg is a [0] array that _FORTIFY_SOURCE
+        * sizes at one byte and aborts on. */
+       hdrsz = (size_t)(ctrl->th->th_msg - ctrl->buf);
+       avail = ctrl->bufsz - hdrsz;
+       len   = strlen(str);
+       if (len >= avail)
+               len = avail - 1;
+       memcpy(&ctrl->buf[hdrsz], str, len);
+       ctrl->buf[hdrsz + len] = 0;
        DBG("ERR %d: %s", code, str);
 
        /* Error is ASCIIZ string, hence +1 */
@@ -175,42 +198,76 @@
        return 0;
 }
 
+/*
+ * Pop the next NUL terminated field off @buf, NULL when the remaining
+ * bytes hold no complete one.  Keeps every read inside the packet we
+ * were handed, however truncated or unterminated it is.
+ */
+static char *next_field(char **buf, size_t *len)
+{
+       char  *field = *buf;
+       size_t field_len;
+
+       field_len = strnlen(field, *len);
+       if (field_len == *len)
+               return NULL;    /* no NUL within the packet */
+
+       *buf += field_len + 1;
+       *len -= field_len + 1;
+
+       return field;
+}
+
 /* Parse TFTP payload in WRQ/RRQ for filename and optional blksize+timeout */
 static int parse_RWRQ(ctrl_t *ctrl, char *buf, size_t len)
 {
-       size_t opt_len = strlen(buf) + 1;
+       size_t segsize = 0;
+       char *file, *opt;
 
        /* First opt is always filename */
-       ctrl->file = strdup(buf);
-       if (!ctrl->file)
-               return send_ERROR(ctrl, EUNDEF, NULL);
-
-       do {
-               /* Prepare to read options */
-               buf += opt_len;
-               len -= opt_len;
-               opt_len = strlen(buf) + 1;
-
-               if (!strncasecmp(buf, "blksize", 7)) {
-                       size_t sz = 0;
-
-                       buf += opt_len;
-                       len -= opt_len;
-                       opt_len = strlen(buf) + 1;
-
-                       sscanf(buf, "%zd", &sz);
-                       if (sz < MIN_SEGSIZE)
-                               continue; /* Ignore if too small for us. */
-
-                       if (alloc_buf(ctrl, sz)) {
-                               ERR(errno, "Failed reallocating TFTP buffer 
memory");
-                               return send_ERROR(ctrl, EUNDEF, NULL);
-                       }
+       file = next_field(&buf, &len);
+       if (!file || !file[0]) {
+               send_ERROR(ctrl, EBADOP, "Malformed request");
+               return 1;
+       }
+
+       ctrl->file = strdup(file);
+       if (!ctrl->file) {
+               send_ERROR(ctrl, EUNDEF, NULL);
+               return 1;
+       }
 
-                       DBG("Negotiated blksize %zd", sz);
-                       setbit(&ctrl->tftp_options, 1);
+       /* Then comes the mode, followed by any options */
+       while ((opt = next_field(&buf, &len))) {
+               char *val;
+
+               if (strncasecmp(opt, "blksize", 7))
+                       continue;
+
+               val = next_field(&buf, &len);
+               if (!val)
+                       break;
+
+               if (sscanf(val, "%zu", &segsize) != 1)
+                       segsize = 0;
+               if (segsize < MIN_SEGSIZE)
+                       segsize = 0;    /* Ignore if too small for us. */
+               else if (segsize > MAX_SEGSIZE)
+                       segsize = MAX_SEGSIZE;
+       }
+
+       /* alloc_buf() reallocates the buffer @buf points into, so it can
+        * only run once we are done walking the options. */
+       if (segsize) {
+               if (alloc_buf(ctrl, segsize)) {
+                       ERR(errno, "Failed reallocating TFTP buffer memory");
+                       send_ERROR(ctrl, EUNDEF, NULL);
+                       return 1;
                }
-       } while (len);
+
+               DBG("Negotiated blksize %zd", segsize);
+               setbit(&ctrl->tftp_options, 1);
+       }
 
        if (!ctrl->tftp_options)
                return 0;
@@ -286,6 +343,9 @@
        char errmsg[80];
        int block;
 
+       if (!ctrl->fp)
+               return !send_ERROR(ctrl, EBADOP, "No transfer in progress");
+
        block = ntohs(ctrl->th->th_block);
        if (block != ctrl->offset) {
                snprintf(errmsg, sizeof(errmsg), "Expected block %ld, "
@@ -344,6 +404,7 @@
        int              active = 1;
        ctrl_t          *ctrl = (ctrl_t *)arg;
        ssize_t          len;
+       size_t           hdrsz;
        uint16_t         port, op, block;
        struct sockaddr *addr = (struct sockaddr *)&ctrl->client_sa;
        socklen_t        addr_len = sizeof(ctrl->client_sa);
@@ -366,6 +427,21 @@
        op     = ntohs(ctrl->th->th_opcode);
        block  = ntohs(ctrl->th->th_block);
 
+       /* The handlers measure their payload from the header, so a packet
+        * too short to hold one would underflow the subtractions below. */
+       if (op == RRQ || op == WRQ)
+               hdrsz = ctrl->th->th_stuff - ctrl->buf;
+       else
+               hdrsz = ctrl->th->th_data - ctrl->buf;
+
+       if (len < (ssize_t)hdrsz) {
+               DBG("tftp runt packet from %s:%d, %zd bytes, opcode %d",
+                   ctrl->clientaddr, port, len, op);
+               send_ERROR(ctrl, EBADOP, "Malformed packet");
+               uev_exit(w->ctx);
+               return;
+       }
+
        switch (op) {
        case RRQ:
                len -= ctrl->th->th_stuff - ctrl->buf;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/src/uftpd.c new/uftpd-2.17/src/uftpd.c
--- old/uftpd-2.16/src/uftpd.c  2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/src/uftpd.c  2026-09-09 18:52:23.000000000 +0200
@@ -1,6 +1,6 @@
 /* uftpd -- the no nonsense (T)FTP server
  *
- * Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+ * Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/src/uftpd.h new/uftpd-2.17/src/uftpd.h
--- old/uftpd-2.16/src/uftpd.h  2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/src/uftpd.h  2026-09-09 18:52:23.000000000 +0200
@@ -1,6 +1,6 @@
 /* uftpd -- the no nonsense (T)FTP server
  *
- * Copyright (c) 2014-2021  Joachim Wiberg <[email protected]>
+ * Copyright (c) 2014-2026  Joachim Wiberg <[email protected]>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -77,6 +77,8 @@
 
 /* TFTP Minimum segment size, specific to uftpd */
 #define MIN_SEGSIZE       32
+/* TFTP Maximum segment size, RFC 2348 */
+#define MAX_SEGSIZE       65464
 
 #define LOGIT(severity, code, fmt, args...)                            \
        do {                                                            \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/test/Makefile.am 
new/uftpd-2.17/test/Makefile.am
--- old/uftpd-2.16/test/Makefile.am     2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/test/Makefile.am     2026-09-09 18:52:23.000000000 +0200
@@ -1,8 +1,8 @@
-EXTRA_DIST         = README.md lib.sh ftp.sh tftp.sh oack.sh dupack.sh 
rollover.sh wrq.sh zombies.sh ipv6.sh mlst.sh maxfiles.sh
+EXTRA_DIST         = README.md lib.sh unshare.sh ftp.sh tftp.sh oack.sh 
dupack.sh lockstep.sh malformed.sh rollover.sh wrq.sh zombies.sh ipv6.sh 
mlst.sh maxfiles.sh
 CLEANFILES         = *~ *.trs *.log
 
 TEST_EXTENSIONS    = .sh
-TESTS_ENVIRONMENT  = unshare -mrun --map-auto
+TESTS_ENVIRONMENT  = $(srcdir)/unshare.sh
 
 # Slowest tests first so they start early when 'make -j check' has fewer
 # job slots than tests (e.g. on CI), minimizing total wall-clock time.
@@ -12,6 +12,8 @@
 TESTS             += tftp.sh
 TESTS             += oack.sh
 TESTS             += dupack.sh
+TESTS             += lockstep.sh
+TESTS             += malformed.sh
 TESTS             += wrq.sh
 TESTS             += zombies.sh
 TESTS             += ipv6.sh
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/test/README.md 
new/uftpd-2.17/test/README.md
--- old/uftpd-2.16/test/README.md       2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/test/README.md       2026-09-09 18:52:23.000000000 +0200
@@ -38,11 +38,12 @@
 | `tnftp`   | tnftp       | `mlst`                                             
    |
 | `tftp`    | tftp-hpa    | `tftp`, `ipv6`                                     
    |
 | `pgrep`   | procps      | `zombies`                                          
    |
-| `python3` | python3     | `oack`, `dupack`, `rollover`, `wrq`, `ipv6`, 
`zombies` |
+| `python3` | python3     | `oack`, `dupack`, `lockstep`, `malformed`, 
`rollover`, `wrq`, `ipv6`, `zombies` |
 
 `python3` is used where a test must craft or inspect raw TFTP packets
-(checking the exact OACK bytes, replaying a stale ACK, injecting a
-duplicate WRQ) — things the CLI clients cannot do.  A test that is
+(checking the exact OACK bytes, replaying a stale ACK, withholding one
+to see what the server does anyway, injecting a duplicate WRQ) — things
+the CLI clients cannot do.  A test that is
 missing its tool is skipped, not failed.
 
 
@@ -56,3 +57,9 @@
 ```console
 $ sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
 ```
+
+Where the kernel denies them anyway -- a container without `newuidmap`,
+a Debian buildd -- the whole suite is skipped rather than failed.  This
+matters for `make package`, which runs the tests via `dh_auto_test`.
+Skipping is also what `DEB_BUILD_OPTIONS=nocheck` does, if you want to
+leave them out of a package build entirely.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/test/lib.sh new/uftpd-2.17/test/lib.sh
--- old/uftpd-2.16/test/lib.sh  2026-06-21 12:56:31.000000000 +0200
+++ new/uftpd-2.17/test/lib.sh  2026-09-09 18:52:23.000000000 +0200
@@ -114,6 +114,15 @@
        return 0
 }
 
+# Set by the unshare.sh wrapper when the kernel denies us unprivileged
+# user namespaces.  Without them the tests would fight over the real
+# network and port 69, so skip rather than run them unisolated.
+if [ -n "$UFTPD_NO_USERNS" ]; then
+       SKIP "Unprivileged user namespaces unavailable." \
+            "On Ubuntu, allow them with:" \
+            "    sysctl kernel.apparmor_restrict_unprivileged_userns=0"
+fi
+
 # Runs once when including lib.sh
 mkdir -p "${DIR}"
 mkdir -p "${CDIR}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/test/lockstep.sh 
new/uftpd-2.17/test/lockstep.sh
--- old/uftpd-2.16/test/lockstep.sh     1970-01-01 01:00:00.000000000 +0100
+++ new/uftpd-2.17/test/lockstep.sh     2026-09-09 18:52:23.000000000 +0200
@@ -0,0 +1,102 @@
+#!/bin/sh
+# Regression test for the OACK half of the lockstep rules.  TFTP is a
+# strict stop-and-wait protocol: exactly one unacknowledged DATA packet
+# may be in flight, and per RFC 2347 a server that answered a RRQ with
+# an OACK must wait for the client's ACK 0 before sending DATA block 1.
+#
+# oack.sh checks the shape of the OACK and dupack.sh checks the resend
+# behaviour, but dupack.sh uses a plain RRQ, so nothing covered what an
+# option-negotiating client sees.  That is the path U-Boot and other
+# bootloaders take, and getting it wrong desyncs the transfer: the
+# unsolicited DATA 1 puts the stream a block ahead of the ACKs, the
+# client starts re-ACKing the last block it liked, and a server that
+# advances on every ACK streams past the gap until the client gives up.
+#
+# Small files still work, which is what makes it look like a size
+# problem: the whole file fits in the one block sent before the desync.
+
+if [ x"${srcdir}" = x ]; then
+    srcdir=.
+fi
+. ${srcdir}/lib.sh
+
+check_dep python3
+
+# Ten blocks at the block size U-Boot asks for.
+BLKSIZE=1468
+dd if=/dev/urandom of="$DIR/big.bin" bs=$BLKSIZE count=10 2>/dev/null
+
+print "Negotiating blksize, verifying the server stays in lockstep ..."
+
+BLKSIZE=$BLKSIZE python3 - <<'EOF'
+import os, socket, struct, sys
+
+DATA, ACK, ERROR, OACK = 3, 4, 5, 6
+blksize = os.environ["BLKSIZE"].encode()
+srv = ("127.0.0.1", 69)
+
+s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+def recv(timeout=3):
+    s.settimeout(timeout)
+    try:
+        pkt, peer = s.recvfrom(4096)
+    except socket.timeout:
+        return None, None, None
+    op = struct.unpack(">H", pkt[:2])[0]
+    if op == ERROR:
+        print("server ERROR:", pkt[4:].split(b"\0")[0].decode("latin1"))
+        sys.exit(1)
+    blk = struct.unpack(">H", pkt[2:4])[0] if op in (DATA, ACK) else None
+    return op, blk, peer
+
+s.sendto(b"\x00\x01big.bin\x00octet\x00blksize\x00" + blksize + b"\x00", srv)
+
+op, _, tid = recv()
+if op != OACK:
+    print("expected OACK, got opcode", op)
+    sys.exit(1)
+print("got OACK, deliberately not acknowledging it yet")
+
+# RFC 2347: nothing may follow the OACK until we ACK block 0.  A server
+# that sends DATA 1 here is already a block ahead of us.
+op, blk, _ = recv(timeout=2)
+if op is not None:
+    print(f"FAIL: server sent opcode {op} block {blk} before our ACK 0")
+    sys.exit(1)
+print("server correctly waited for ACK 0")
+
+s.sendto(struct.pack(">HH", ACK, 0), tid)
+op, blk, _ = recv()
+if op != DATA or blk != 1:
+    print(f"expected DATA 1 after ACK 0, got opcode {op} block {blk}")
+    sys.exit(1)
+
+# One DATA in flight at a time: nothing more until we ACK block 1.
+op, blk, _ = recv(timeout=2)
+if op is not None:
+    print(f"FAIL: server sent opcode {op} block {blk} before our ACK 1")
+    sys.exit(1)
+print("one block in flight at a time")
+
+s.sendto(struct.pack(">HH", ACK, 1), tid)
+op, blk, _ = recv()
+if op != DATA or blk != 2:
+    print(f"expected DATA 2 after ACK 1, got opcode {op} block {blk}")
+    sys.exit(1)
+
+# Simulate a lost DATA 2: re-ACK block 1.  The server must resend block
+# 2, not treat the ACK as permission to send block 3.
+s.sendto(struct.pack(">HH", ACK, 1), tid)
+op, blk, _ = recv()
+print("after stale ACK(1) on the OACK path, server sent block", blk)
+if op != DATA or blk != 2:
+    print("FAIL: server streamed past the gap instead of resending")
+    sys.exit(1)
+
+print("lockstep maintained across the whole exchange")
+sys.exit(0)
+EOF
+
+[ $? -eq 0 ] && OK
+FAIL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/test/malformed.sh 
new/uftpd-2.17/test/malformed.sh
--- old/uftpd-2.16/test/malformed.sh    1970-01-01 01:00:00.000000000 +0100
+++ new/uftpd-2.17/test/malformed.sh    2026-09-09 18:52:23.000000000 +0200
@@ -0,0 +1,109 @@
+#!/bin/sh
+# Malformed TFTP requests must draw an ERROR and leave the server
+# serving, rather than ending the session:
+#
+#   RRQ/WRQ/DATA opcode only   packet shorter than the header its opcode
+#                              implies, so the payload length is bogus
+#   unterminated filename      an option walk with no terminator to stop on
+#   blksize + trailing option  alloc_buf() reallocates the buffer the
+#                              option walk is still reading from
+#   huge blksize               an unbounded session buffer from one packet
+#   DATA with no transfer      fwrite() with ctrl->fp still NULL
+#
+# A dead session answers nothing, so the check is that an ERROR comes
+# back, not whether the parent survived -- it forks per session and
+# survives either way.
+
+if [ x"${srcdir}" = x ]; then
+    srcdir=.
+fi
+. ${srcdir}/lib.sh
+
+check_dep python3
+
+print "Sending malformed TFTP requests, expecting ERROR not a crash ..."
+
+python3 - <<'EOF'
+import socket, struct, sys
+
+DATA, ERROR, OACK = 3, 5, 6
+NUL = b"\x00"
+srv = ("127.0.0.1", 69)
+fail = 0
+
+# Keep every socket alive for the whole run.  A closed one frees its
+# ephemeral port for the next probe to reuse, and a lingering session we
+# never acknowledged can then land its retransmit on the wrong probe.
+socks = []
+
+def send(pkt, timeout=3):
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.settimeout(timeout)
+    socks.append(s)
+    s.sendto(pkt, srv)
+    try:
+        rsp, peer = s.recvfrom(4096)
+    except socket.timeout:
+        return None
+    # An OACK opens a session that waits for our ACK 0; end it rather
+    # than leave it retransmitting into the rest of the test.
+    if rsp[:2] == struct.pack(">H", OACK):
+        s.sendto(struct.pack(">HH", ERROR, 0) + b"done" + NUL, peer)
+    return rsp
+
+def req(op, *fields):
+    return struct.pack(">H", op) + b"".join(f.encode() + NUL for f in fields)
+
+def opcode(rsp):
+    return struct.unpack(">H", rsp[:2])[0] if rsp and len(rsp) >= 2 else None
+
+# Packets that must be rejected with an ERROR, not a dead session.
+for name, pkt in [
+    ("RRQ, opcode only",       b"\x00\x01"),
+    ("WRQ, opcode only",       b"\x00\x02"),
+    ("DATA, opcode only",      b"\x00\x03"),
+    ("RRQ, unterminated file", b"\x00\x01testfile.txt"),
+    ("DATA, no transfer open", struct.pack(">HH", DATA, 0)),
+]:
+    rsp = send(pkt)
+    if rsp is None:
+        print(f"  {name:<26} no reply, session died")
+        fail = 1
+    elif opcode(rsp) != ERROR:
+        print(f"  {name:<26} expected ERROR, got {rsp[:24]!r}")
+        fail = 1
+    else:
+        print(f"  {name:<26} ERROR: {rsp[4:].split(NUL)[0].decode('latin1')}")
+
+# Option handling that must not touch freed or unbounded memory.
+rsp = send(req(1, "testfile.txt", "octet", "blksize", "1468", "timeout", "5"))
+if opcode(rsp) != OACK:
+    print(f"  {'blksize + trailing opt':<26} expected OACK, got {rsp!r}")
+    fail = 1
+else:
+    print(f"  {'blksize + trailing opt':<26} OACK: {b' 
'.join(rsp[2:].split(NUL)).decode('latin1').strip()}")
+
+rsp = send(req(1, "testfile.txt", "octet", "blksize", "999999999999"))
+if opcode(rsp) != OACK:
+    print(f"  {'huge blksize':<26} expected OACK, got {rsp!r}")
+    fail = 1
+else:
+    sz = int(rsp[2:].split(NUL)[1])
+    print(f"  {'huge blksize':<26} clamped to {sz}")
+    if sz > 65464:
+        print("      not clamped to the RFC 2348 maximum")
+        fail = 1
+
+# The server must still serve a normal request afterwards.
+rsp = send(req(1, "testfile.txt", "octet"))
+if opcode(rsp) != DATA:
+    print(f"  {'control, plain RRQ':<26} server no longer serving, got 
{rsp!r}")
+    fail = 1
+else:
+    print(f"  {'control, plain RRQ':<26} DATA block 1, {len(rsp) - 4} bytes")
+
+sys.exit(fail)
+EOF
+
+[ $? -eq 0 ] && OK
+FAIL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/uftpd-2.16/test/unshare.sh 
new/uftpd-2.17/test/unshare.sh
--- old/uftpd-2.16/test/unshare.sh      1970-01-01 01:00:00.000000000 +0100
+++ new/uftpd-2.17/test/unshare.sh      2026-09-09 18:52:23.000000000 +0200
@@ -0,0 +1,23 @@
+#!/bin/sh
+# TESTS_ENVIRONMENT wrapper.  Every test runs in its own mount, user, and
+# network namespace so the suite needs no root and can run in parallel.
+#
+# Where that is unavailable -- a container without newuidmap, a Debian
+# buildd, Ubuntu's AppArmor clamp on unprivileged user namespaces -- the
+# tests would otherwise die with a bare exit 127 from unshare(1), which
+# automake reports as a hard failure.  That breaks `make package`, since
+# dh_auto_test runs the suite during the package build.
+#
+# We cannot exit 77 (automake's SKIP) from here: TESTS_ENVIRONMENT wraps
+# the log driver, not the test, so the driver would never run and never
+# record the result.  Instead tell lib.sh to skip, and let each test
+# report it from inside the driver where the exit status is understood.
+
+if unshare -mrun --map-auto true 2>/dev/null; then
+       exec unshare -mrun --map-auto "$@"
+fi
+
+UFTPD_NO_USERNS=1
+export UFTPD_NO_USERNS
+
+exec "$@"

Reply via email to