Compare commits

..

13 Commits

Author SHA1 Message Date
Ramon Roche 68e55cfc2a ci(ros): use matching branch for px4-ros2-interface-lib (1.17)
Backport of #26781 with the follow-up fix from #27000 folded in.

On release branches, the ROS integration test now clones a matching
branch from Auterion/px4-ros2-interface-lib instead of always using
main, preventing build failures from uORB message divergence between
main and release branches.

Branch resolution uses GITHUB_BASE_REF on pull requests (the branch
the code is merging into) and falls back to GITHUB_REF_NAME for
direct pushes, so backport PRs resolve to their target release
branch instead of the PR author's branch.

Fixes https://github.com/Auterion/px4-ros2-interface-lib/issues/184

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-04-08 11:03:38 -06:00
Ramon Roche 5389f75577 ci(container): add build_ref input to allow dispatch against arbitrary refs
The current workflow_dispatch path builds whatever HEAD of the dispatch ref
is, labels the resulting image with px4_version, and publishes. That's
fine for rebuilding current state but it cannot rebuild the exact commit
a release tag points to, because the dispatch loads the workflow file
from one ref and implicitly checks out the same ref for the build.

This matters for release recovery. When the v1.17.0-rc2 tag push failed
to publish containers back on 2026-03-13 (the v1 GHA cache protocol
removal in RunsOn v2.12.0), the tag was not re-pushed, so the only way
to publish rc2 containers now is via workflow_dispatch. Without this
change, a dispatch against release/1.17 builds release/1.17 HEAD and
labels it v1.17.0-rc2, which produces a container whose contents do not
match the rc2 tag's actual code. That is not a faithful recovery.

Add a build_ref input that controls only the checkout ref, defaulting
to empty which falls back to github.ref (preserving current behavior
for both push events and dispatches that omit the input). With this,
a release recovery looks like:

  gh workflow run dev_container.yml --repo PX4/PX4-Autopilot \
    --ref release/1.17 \
    -f px4_version=v1.17.0-rc2 \
    -f build_ref=v1.17.0-rc2 \
    -f deploy_to_registry=true

The workflow loads from release/1.17 HEAD (which has the cache fix
from 39b0568 and the hardening from d74db56a), but the build uses
Tools/setup/Dockerfile from the rc2 tag. The published image has
rc2 contents under the rc2 label, as if the original tag push had
worked.

All three actions/checkout steps (setup, build, deploy) take the same
ref expression so every job sees a consistent workspace. Non-dispatch
events (push, PR) evaluate github.event.inputs.build_ref to empty and
fall back to github.ref exactly as before.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
(cherry picked from commit e4d46f20f4)
2026-04-07 18:22:39 -06:00
Ramon Roche 3b84cdbdbe ci(container): harden dev_container workflow against cache-export flakes
Three related fixes to prevent a repeat of the v1.17.0-rc2 incident, where a
post-push GHA cache-export 404 failed the arm64 build after both registry
pushes had already succeeded, fail-fast cancelled amd64, and the deploy job
was skipped, leaving the registries with only a partial arm64 publish and no
multi-arch manifest.

- Mark cache export as non-fatal via ignore-error=true on cache-to. A
  successful registry push should never be undone by a cache-layer flake.
  This alone would have let rc2 publish correctly.

- Decouple the deploy job from the build job's exit code. Change its if:
  gate to !cancelled() + setup success only, and promote the existing
  "Verify Images Exist Before Creating Manifest" step from a warning into
  a hard precondition. Deploy now runs whenever both per-arch tags actually
  exist in the registries, which is its real precondition, and fails loudly
  if a tag is missing.

- Bump every action to the current major (runs-on/action v2,
  actions/checkout v5, docker/login-action v4, docker/setup-buildx-action v4,
  docker/build-push-action v7, docker/metadata-action v6). This gets the
  workflow off Node 20 before GitHub's June 2 2026 forced runtime switch
  and keeps runs-on/action on the same major as the runs-on platform.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
(cherry picked from commit d74db56a06)
2026-04-07 18:22:39 -06:00
Jacob Dahl 54ab3158c5 fix(ekf2): allow optical flow to start when range finder is height reference
When EKF2_HGT_REF=2 (range sensor) with no GPS, optical flow could
never start. The starting condition required isTerrainEstimateValid()
or isHorizontalAidingActive(), but terrain is never "estimated" when
range is the height reference (ground is the datum, terrain state is
fixed at 0), and there's no horizontal aiding without GPS.

HAGL is directly known from the range measurement in this case, so
optical flow has everything it needs to fuse. Add the range height
reference check to the optical flow starting conditions.

Fixes: https://github.com/PX4/PX4-Autopilot/issues/25248
2026-04-03 19:53:58 -06:00
Silvan fab7cbda8d fix(npfg): use NFPG_DAMPING and NPFG_PERIOD to calculate directional p gain
Signed-off-by: Silvan <silvan@auterion.com>
2026-03-19 16:54:11 +01:00
Ramon Roche 39b0568ea4 fix(ci): remove deprecated v1 cache API from container build
Backport of #26742 to release/1.17.

RunsOn v2.12.0 (March 6, 2026) removed v1 cache toolkit support,
causing the buildx GHA cache proxy to return 404 for v1 endpoints.
This broke the v1.17.0-rc2 container build.

Removing the explicit version=1 parameter lets buildkit auto-detect
the v2 protocol.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 15:59:48 -07:00
Ramon Roche 0b6e4687de fix(mavlink): remove all stale mavlink_tests references
The mavlink_tests module was deleted in 1009268d31 but several
references were left behind, breaking builds on all targets.

Removed:
- CMakeLists.txt: add_subdirectory(mavlink_tests)
- mavlink_ftp.cpp: #include of deleted mavlink_ftp_test.h
- mavlink_ftp.h: MavlinkFtpTest forward decl and friend class
- posix-configs/SITL/init/test/test_mavlink: dead init script
- sitl_tests.cmake: sitl-mavlink CTest target
- install-voxl.sh: px4-mavlink_tests symlink

Ref: https://github.com/PX4/PX4-Autopilot/issues/26738
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
Ramon Roche 69a6b9eee6 fix(zenoh): validate payload size before stack allocation
Reject Zenoh payloads that exceed the expected uORB topic size plus
CDR header (4 bytes), or that are too small to contain a valid CDR
header. This prevents a stack overflow from crafted network input
where z_bytes_len(payload) controls a VLA allocation.

Fixes GHSA-69g4-hcqf-j45p

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
Ramon Roche b5b48536a3 fix(mavlink): correct session validation in FTP write and burst operations
Use logical OR (||) instead of AND (&&) in _workWrite() and _workBurst()
session validation, matching the correct logic already used in _workRead()
and _workTerminate(). The AND operator allowed operations to proceed with
an invalid session ID as long as a valid file descriptor existed.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
Ramon Roche 3bd06cc02f refactor(mavlink): remove dead FTP unit test code
Remove the old MAVLINK_FTP_UNIT_TEST infrastructure that has been dead
code for years (not enabled in any board config). This includes:

- src/modules/mavlink/mavlink_tests/ directory (test suite, CMakeLists)
- All #ifdef MAVLINK_FTP_UNIT_TEST blocks in mavlink_ftp.cpp
- set_unittest_worker() callback mechanism in mavlink_ftp.h
- Conditional uAvionix include in mavlink_bridge_header.h

The test suite will be ported to GTest as a follow-up.

Ref: https://github.com/PX4/PX4-Autopilot/issues/26738
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
Ramon Roche 48335ac3f1 fix(mavlink): reject path traversal sequences in FTP operations
Add _validatePath() that rejects paths containing ".." components,
preventing directory traversal outside the FTP root directory.
Applied to all FTP operation handlers (list, open, remove, truncate,
rename, mkdir, rmdir, CRC32).

Fixes GHSA-fh32-qxj9-x32f, GHSA-pm28-2j4f-8jxv

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
Ramon Roche 9605d8dce8 fix(tattu_can): validate CAN frame bounds before buffer copy
Add bounds checking in the CAN frame assembly loop to prevent a buffer
overflow when copying payloads into the Tattu12SBatteryMessage struct.
A crafted CAN frame with a corrupt payload_size could write past the
48-byte struct boundary. Also guard against payload_size of 0 which
would cause an unsigned integer underflow on the size_t subtraction.

Fixes GHSA-wxwm-xmx9-hr32

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
Ramon Roche 3d63672c03 fix(telemetry/bst): validate reply length and dev_name_len before use
Reject replies with length >= sizeof(BSTPacket) to prevent OOB read
in CRC calculation. Clamp dev_name_len to buffer size to prevent OOB
write during null termination.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-13 12:30:05 -07:00
20 changed files with 130 additions and 1414 deletions
+31 -20
View File
@@ -24,6 +24,11 @@ on:
description: 'Container tag (e.g. v1.16.0)'
required: true
type: string
build_ref:
description: 'Git ref to build from (branch, tag, or SHA). Leave empty to build from the dispatch ref.'
required: false
type: string
default: ''
deploy_to_registry:
description: 'Whether to push built images to the registry'
required: false
@@ -45,9 +50,10 @@ jobs:
meta_tags: ${{ steps.meta.outputs.tags }}
meta_labels: ${{ steps.meta.outputs.labels }}
steps:
- uses: runs-on/action@v1
- uses: actions/checkout@v4
- uses: runs-on/action@v2
- uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.build_ref || github.ref }}
fetch-tags: true
submodules: false
fetch-depth: 0
@@ -64,7 +70,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
ghcr.io/PX4/px4-dev
@@ -89,22 +95,23 @@ jobs:
runner: x64
runs-on: [runs-on,"runner=4cpu-linux-${{ matrix.runner }}","image=ubuntu24-full-${{ matrix.runner }}","run-id=${{ github.run_id }}",extras=s3-cache,spot=false]
steps:
- uses: runs-on/action@v1
- uses: actions/checkout@v4
- uses: runs-on/action@v2
- uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.build_ref || github.ref }}
fetch-tags: true
submodules: false
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
if: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry) }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
if: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry) }}
with:
registry: ghcr.io
@@ -112,13 +119,13 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
with:
driver: docker-container
platforms: ${{ matrix.platform }}
- name: Build and Load Container Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
id: docker
with:
context: Tools/setup
@@ -130,8 +137,8 @@ jobs:
load: false
push: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry) }}
provenance: false
cache-from: type=gha,version=1
cache-to: type=gha,version=1,mode=max
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }},ignore-error=true
deploy:
name: Deploy To Registry
@@ -140,23 +147,27 @@ jobs:
packages: write
runs-on: [runs-on,"runner=4cpu-linux-x64","image=ubuntu24-full-x64","run-id=${{ github.run_id }}",extras=s3-cache,spot=false]
needs: [build, setup]
if: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry) }}
if: |
!cancelled() &&
needs.setup.result == 'success' &&
(startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry == 'true'))
steps:
- uses: runs-on/action@v1
- uses: actions/checkout@v4
- uses: runs-on/action@v2
- uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.build_ref || github.ref }}
fetch-tags: true
submodules: false
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -164,10 +175,10 @@ jobs:
- name: Verify Images Exist Before Creating Manifest
run: |
docker manifest inspect px4io/px4-dev:${{ needs.setup.outputs.px4_version }}-arm64 || echo "⚠️ Warning: No ARM64 image found!"
docker manifest inspect px4io/px4-dev:${{ needs.setup.outputs.px4_version }}-amd64 || echo "⚠️ Warning: No AMD64 image found!"
docker manifest inspect ghcr.io/px4/px4-dev:${{ needs.setup.outputs.px4_version }}-arm64 || echo "⚠️ Warning: No ARM64 image found!"
docker manifest inspect ghcr.io/px4/px4-dev:${{ needs.setup.outputs.px4_version }}-amd64 || echo "⚠️ Warning: No AMD64 image found!"
docker manifest inspect px4io/px4-dev:${{ needs.setup.outputs.px4_version }}-arm64
docker manifest inspect px4io/px4-dev:${{ needs.setup.outputs.px4_version }}-amd64
docker manifest inspect ghcr.io/px4/px4-dev:${{ needs.setup.outputs.px4_version }}-arm64
docker manifest inspect ghcr.io/px4/px4-dev:${{ needs.setup.outputs.px4_version }}-amd64
- name: Create and Push Multi-Arch Manifest for Docker Hub
run: |
+11 -1
View File
@@ -89,7 +89,17 @@ jobs:
. /opt/ros/galactic/setup.bash
mkdir -p /opt/px4_ws/src
cd /opt/px4_ws/src
git clone --recursive https://github.com/Auterion/px4-ros2-interface-lib.git
# On a PR, target the branch we're merging into (main or release/X.Y).
# On a direct push, fall back to the branch we're running on.
BRANCH="${GITHUB_BASE_REF:-$GITHUB_REF_NAME}"
REPO_URL="https://github.com/Auterion/px4-ros2-interface-lib.git"
if git ls-remote --heads "$REPO_URL" "$BRANCH" | grep -q "$BRANCH"; then
echo "Cloning px4-ros2-interface-lib with matching branch: $BRANCH"
git clone --recursive --branch "$BRANCH" "$REPO_URL"
else
echo "Branch '$BRANCH' not found in px4-ros2-interface-lib, using default (main)"
git clone --recursive "$REPO_URL"
fi
cd ..
# Copy messages to ROS workspace
"${PX4_DIR}/Tools/copy_to_ros_ws.sh" "$(pwd)"
@@ -65,7 +65,6 @@ adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-logger"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-manual_control"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-mavlink"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-mavlink_bridge"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-mavlink_tests"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-mb12xx"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-mc_att_control"
adb shell "cd /usr/bin; /bin/ln -f -s px4 px4-mc_pos_control"
-14
View File
@@ -72,20 +72,6 @@ endforeach()
# Mavlink test requires mavlink running
add_test(NAME sitl-mavlink
COMMAND $<TARGET_FILE:px4>
-s ${PX4_SOURCE_DIR}/posix-configs/SITL/init/test/test_mavlink
-t ${PX4_SOURCE_DIR}/test_data
${PX4_SOURCE_DIR}/ROMFS/px4fmu_test
WORKING_DIRECTORY ${SITL_WORKING_DIR}
)
set_tests_properties(sitl-mavlink PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL")
set_tests_properties(sitl-mavlink PROPERTIES PASS_REGULAR_EXPRESSION "ALL TESTS PASSED")
sanitizer_fail_test_on_error(sitl-mavlink)
# IMU filtering
add_test(NAME sitl-imu_filtering
COMMAND $<TARGET_FILE:px4>
-23
View File
@@ -1,23 +0,0 @@
#!/bin/sh
# PX4 commands need the 'px4-' prefix in bash.
# (px4-alias.sh is expected to be in the PATH)
. px4-alias.sh
param load
param set CBRK_SUPPLY_CHK 894281
dataman start
battery_simulator start
simulator_mavlink start
tone_alarm start
pwm_out_sim start
ver all
mavlink start -x -u 14556 -r 2000000
mavlink boot_complete
mavlink_tests
shutdown
+9 -2
View File
@@ -96,9 +96,16 @@ void TattuCan::Run()
while (receive(&received_frame) > 0) {
if (received_frame.payload_size == 0) {
break;
}
size_t payload_size = received_frame.payload_size - 1;
// TODO: add check to prevent buffer overflow from a corrupt 'payload_size' value
// TODO: AND look for TAIL_BYTE_START_OF_TRANSFER to indicate end of transfer. Untested...
if (offset + payload_size > sizeof(tattu_message)) {
break;
}
memcpy(((char *)&tattu_message) + offset, received_frame.payload, payload_size);
offset += payload_size;
}
+10
View File
@@ -197,6 +197,12 @@ int BST::probe()
}
uint8_t *reply_raw = reinterpret_cast<uint8_t *>(&dev_info_reply);
if (dev_info_reply.length >= sizeof(dev_info_reply)) {
PX4_ERR("invalid reply length: %u", dev_info_reply.length);
return -EIO;
}
uint8_t crc_calc = crc8(reinterpret_cast<uint8_t *>(&dev_info_reply.type), dev_info_reply.length - 1);
uint8_t crc_recv = reply_raw[dev_info_reply.length];
@@ -205,6 +211,10 @@ int BST::probe()
return -EIO;
}
if (dev_info_reply.payload.dev_name_len >= sizeof(dev_info_reply.payload.dev_name)) {
dev_info_reply.payload.dev_name_len = sizeof(dev_info_reply.payload.dev_name) - 1;
}
dev_info_reply.payload.dev_name[dev_info_reply.payload.dev_name_len] = '\0';
PX4_DEBUG("device info: hardware ID: 0x%08X, firmware ID: 0x%04X, device name: %s",
(int)swap_uint32(dev_info_reply.payload.hw_id), (int)swap_uint16(dev_info_reply.payload.fw_id),
+4 -2
View File
@@ -59,13 +59,15 @@
#ifndef PX4_AIRSPEEDDIRECTIONONTROLLER_HPP
#define PX4_AIRSPEEDDIRECTIONONTROLLER_HPP
#include <matrix/math.hpp>
#include <lib/mathlib/mathlib.h>
class AirspeedDirectionController
{
public:
AirspeedDirectionController();
void setPGainFromPeriodAndDamping(float damping, float period) {p_gain_ = 4.f * M_PI_F * damping / math::max(period, FLT_EPSILON);}
float controlHeading(const float heading_sp, const float heading, const float airspeed) const;
private:
@@ -155,7 +155,7 @@ void Ekf::controlOpticalFlowFusion(const imuSample &imu_delayed)
&& is_magnitude_good
&& is_tilt_good
&& (_flow_counter > 10)
&& (isTerrainEstimateValid() || isHorizontalAidingActive())
&& (isTerrainEstimateValid() || isHorizontalAidingActive() || (_height_sensor_ref == HeightSensor::RANGE))
&& isTimedOut(_aid_src_optical_flow.time_last_fuse, (uint64_t)2e6); // Prevent rapid switching
// If the height is relative to the ground, terrain height cannot be observed.
@@ -115,6 +115,8 @@ FwLateralLongitudinalControl::parameters_update()
_tecs_alt_time_const_slew_rate.setSlewRate(TECS_ALT_TIME_CONST_SLEW_RATE);
_tecs_alt_time_const_slew_rate.setForcedValue(_param_fw_t_h_error_tc.get() * _param_fw_thrtc_sc.get());
_airspeed_direction_control.setPGainFromPeriodAndDamping(_param_npfg_damping.get(), _param_npfg_period.get());
}
void FwLateralLongitudinalControl::Run()
@@ -169,7 +169,9 @@ private:
(ParamFloat<px4::params::FW_LND_THRTC_SC>) _param_fw_thrtc_sc,
(ParamFloat<px4::params::FW_T_THR_LOW_HGT>) _param_fw_t_thr_low_hgt,
(ParamFloat<px4::params::FW_WIND_ARSP_SC>) _param_fw_wind_arsp_sc,
(ParamFloat<px4::params::FW_GND_SPD_MIN>) _param_fw_gnd_spd_min
(ParamFloat<px4::params::FW_GND_SPD_MIN>) _param_fw_gnd_spd_min,
(ParamFloat<px4::params::NPFG_DAMPING>) _param_npfg_damping,
(ParamFloat<px4::params::NPFG_PERIOD>) _param_npfg_period
)
hrt_abstime _last_time_loop_ran{};
-4
View File
@@ -143,10 +143,6 @@ px4_add_module(
UNITY_BUILD
)
if(PX4_TESTING)
add_subdirectory(mavlink_tests)
endif()
px4_add_unit_gtest(SRC MavlinkStatustextHandlerTest.cpp
INCLUDES
${MAVLINK_LIBRARY_DIR}
@@ -93,9 +93,7 @@ extern mavlink_status_t *mavlink_get_channel_status(uint8_t chan);
extern mavlink_message_t *mavlink_get_channel_buffer(uint8_t chan);
#include <mavlink.h>
#if !MAVLINK_FTP_UNIT_TEST
#include <uAvionix.h>
#endif
__END_DECLS
+49 -54
View File
@@ -43,8 +43,6 @@
#include <cstring>
#include "mavlink_ftp.h"
#include "mavlink_tests/mavlink_ftp_test.h"
#include "mavlink_main.h"
using namespace time_literals;
@@ -75,49 +73,22 @@ MavlinkFTP::get_size()
}
}
#ifdef MAVLINK_FTP_UNIT_TEST
void
MavlinkFTP::set_unittest_worker(ReceiveMessageFunc_t rcvMsgFunc, void *worker_data)
{
_utRcvMsgFunc = rcvMsgFunc;
_worker_data = worker_data;
}
#endif
uint8_t
MavlinkFTP::_getServerSystemId()
{
#ifdef MAVLINK_FTP_UNIT_TEST
// We use fake ids when unit testing
return MavlinkFtpTest::serverSystemId;
#else
// Not unit testing, use the real thing
return _mavlink.get_system_id();
#endif
}
uint8_t
MavlinkFTP::_getServerComponentId()
{
#ifdef MAVLINK_FTP_UNIT_TEST
// We use fake ids when unit testing
return MavlinkFtpTest::serverComponentId;
#else
// Not unit testing, use the real thing
return _mavlink.get_component_id();
#endif
}
uint8_t
MavlinkFTP::_getServerChannel()
{
#ifdef MAVLINK_FTP_UNIT_TEST
// We use fake ids when unit testing
return MavlinkFtpTest::serverChannel;
#else
// Not unit testing, use the real thing
return _mavlink.get_channel();
#endif
}
void
@@ -173,11 +144,7 @@ MavlinkFTP::_process_request(
if (payload->seq_number + 1 == last_payload->seq_number) {
// this is the same request as the one we replied to last. It means the (n)ack got lost, and the GCS
// resent the request
#ifdef MAVLINK_FTP_UNIT_TEST
_utRcvMsgFunc(last_reply, _worker_data);
#else
mavlink_msg_file_transfer_protocol_send_struct(_mavlink.get_channel(), last_reply);
#endif
return;
}
}
@@ -332,12 +299,7 @@ MavlinkFTP::_reply(mavlink_file_transfer_protocol_t *ftp_req)
PX4_DEBUG("FTP: %s seq_number: %" PRIu16, payload->opcode == kRspAck ? "Ack" : "Nak", payload->seq_number);
#ifdef MAVLINK_FTP_UNIT_TEST
// Unit test hook is set, call that instead
_utRcvMsgFunc(ftp_req, _worker_data);
#else
mavlink_msg_file_transfer_protocol_send_struct(_mavlink.get_channel(), ftp_req);
#endif
}
void MavlinkFTP::_constructPath(char *dst, int dst_len, const char *path) const
@@ -362,6 +324,10 @@ MavlinkFTP::_workList(PayloadHeader *payload)
{
_constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
if (!_validatePath(_work_buffer1)) {
return kErrFailFileProtected;
}
ErrorCode errorCode = kErrNone;
unsigned offset = 0;
@@ -510,6 +476,10 @@ MavlinkFTP::_workOpen(PayloadHeader *payload, int oflag)
_constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
if (!_validatePath(_work_buffer1)) {
return kErrFailFileProtected;
}
PX4_DEBUG("FTP: open '%s'", _work_buffer1);
uint32_t fileSize = 0;
@@ -594,7 +564,7 @@ MavlinkFTP::_workRead(PayloadHeader *payload)
MavlinkFTP::ErrorCode
MavlinkFTP::_workBurst(PayloadHeader *payload, uint8_t target_system_id, uint8_t target_component_id)
{
if (payload->session != 0 && _session_info.fd < 0) {
if (payload->session != 0 || _session_info.fd < 0) {
PX4_DEBUG("_workBurst: no session or no fd");
return kErrInvalidSession;
}
@@ -615,7 +585,7 @@ MavlinkFTP::_workBurst(PayloadHeader *payload, uint8_t target_system_id, uint8_t
MavlinkFTP::ErrorCode
MavlinkFTP::_workWrite(PayloadHeader *payload)
{
if (payload->session != 0 && _session_info.fd < 0) {
if (payload->session != 0 || _session_info.fd < 0) {
PX4_DEBUG("_workWrite: no session or no fd");
return kErrInvalidSession;
}
@@ -652,7 +622,7 @@ MavlinkFTP::_workRemoveFile(PayloadHeader *payload)
{
_constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
if (!_validatePathIsWritable(_work_buffer1)) {
if (!_validatePath(_work_buffer1) || !_validatePathIsWritable(_work_buffer1)) {
return kErrFailFileProtected;
}
@@ -676,7 +646,7 @@ MavlinkFTP::_workTruncateFile(PayloadHeader *payload)
_constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
payload->size = 0;
if (!_validatePathIsWritable(_work_buffer1)) {
if (!_validatePath(_work_buffer1) || !_validatePathIsWritable(_work_buffer1)) {
return kErrFailFileProtected;
}
@@ -837,7 +807,7 @@ MavlinkFTP::_workRename(PayloadHeader *payload)
_constructPath(_work_buffer1, _work_buffer1_len, ptr);
_constructPath(_work_buffer2, _work_buffer2_len, ptr + oldpath_sz + 1);
if (!_validatePathIsWritable(_work_buffer2)) {
if (!_validatePath(_work_buffer1) || !_validatePath(_work_buffer2) || !_validatePathIsWritable(_work_buffer2)) {
return kErrFailFileProtected;
}
@@ -860,7 +830,7 @@ MavlinkFTP::_workRemoveDirectory(PayloadHeader *payload)
{
_constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
if (!_validatePathIsWritable(_work_buffer1)) {
if (!_validatePath(_work_buffer1) || !_validatePathIsWritable(_work_buffer1)) {
return kErrFailFileProtected;
}
@@ -883,7 +853,7 @@ MavlinkFTP::_workCreateDirectory(PayloadHeader *payload)
{
_constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
if (!_validatePathIsWritable(_work_buffer1)) {
if (!_validatePath(_work_buffer1) || !_validatePathIsWritable(_work_buffer1)) {
return kErrFailFileProtected;
}
@@ -908,6 +878,10 @@ MavlinkFTP::_workCalcFileCRC32(PayloadHeader *payload)
ssize_t bytes_read;
_constructPath(_work_buffer2, _work_buffer2_len, _data_as_cstring(payload));
if (!_validatePath(_work_buffer2)) {
return kErrFailFileProtected;
}
int fd = ::open(_work_buffer2, O_RDONLY);
if (fd < 0) {
@@ -1043,7 +1017,6 @@ void MavlinkFTP::send()
return;
}
#ifndef MAVLINK_FTP_UNIT_TEST
// Skip send if not enough room
unsigned max_bytes_to_send = _mavlink.get_free_tx_buf();
PX4_DEBUG("MavlinkFTP::send max_bytes_to_send(%u) get_free_tx_buf(%u)", max_bytes_to_send, _mavlink.get_free_tx_buf());
@@ -1052,8 +1025,6 @@ void MavlinkFTP::send()
return;
}
#endif
// Send stream packets until buffer is full
bool more_data;
@@ -1117,8 +1088,6 @@ void MavlinkFTP::send()
_session_info.stream_download = false;
} else {
#ifndef MAVLINK_FTP_UNIT_TEST
if (max_bytes_to_send < (get_size() * 2)) {
more_data = false;
@@ -1130,14 +1099,10 @@ void MavlinkFTP::send()
}
} else {
#endif
more_data = true;
payload->burst_complete = false;
#ifndef MAVLINK_FTP_UNIT_TEST
max_bytes_to_send -= get_size();
}
#endif
}
ftp_msg.target_system = _session_info.stream_target_system_id;
@@ -1147,6 +1112,36 @@ void MavlinkFTP::send()
} while (more_data);
}
/**
* Reject paths containing ".." components to prevent directory traversal
* outside of _root_dir. Walks the path checking each component between
* '/' separators. A component of exactly ".." (followed by '/' or end
* of string) is rejected.
*
* Examples:
* "/fs/microsd/logs" -> allowed
* "/fs/microsd/../etc" -> rejected
* "../../tmp/pwned" -> rejected
* "/fs/microsd/logs/.." -> rejected
* "/fs/microsd/..hidden" -> allowed (not a ".." component)
*/
bool MavlinkFTP::_validatePath(const char *path)
{
const char *p = path;
while (*p != '\0') {
if ((p == path || *(p - 1) == '/') && p[0] == '.' && p[1] == '.'
&& (p[2] == '/' || p[2] == '\0')) {
PX4_ERR("FTP: rejecting path traversal in %s", path);
return false;
}
p++;
}
return true;
}
bool MavlinkFTP::_validatePathIsWritable(const char *path)
{
#ifdef __PX4_NUTTX
+2 -19
View File
@@ -45,7 +45,6 @@
#include "mavlink_bridge_header.h"
class MavlinkFtpTest;
class Mavlink;
/// MAVLink remote file server. Support FTP like commands using MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL message.
@@ -64,13 +63,6 @@ public:
/// Handle possible FTP message
void handle_message(const mavlink_message_t *msg);
typedef void (*ReceiveMessageFunc_t)(const mavlink_file_transfer_protocol_t *ftp_req, void *worker_data);
/// @brief Sets up the server to run in unit test mode.
/// @param rcvmsgFunc Function which will be called to handle outgoing mavlink messages.
/// @param worker_data Data to pass to worker
void set_unittest_worker(ReceiveMessageFunc_t rcvMsgFunc, void *worker_data);
/// @brief This is the payload which is in mavlink_file_transfer_protocol_t.payload.
/// This needs to be packed, because it's typecasted from mavlink_file_transfer_protocol_t.payload, which starts
/// at a 3 byte offset, causing an unaligned access to seq_number and offset
@@ -156,6 +148,7 @@ private:
*/
void _constructPath(char *dst, int dst_len, const char *path) const;
bool _validatePath(const char *path);
bool _validatePathIsWritable(const char *path);
/**
@@ -183,9 +176,6 @@ private:
};
struct SessionInfo _session_info {}; ///< Session info, fd=-1 for no active session
ReceiveMessageFunc_t _utRcvMsgFunc{}; ///< Unit test override for mavlink message sending
void *_worker_data{nullptr}; ///< Additional parameter to _utRcvMsgFunc;
Mavlink &_mavlink;
/* do not allow copying this class */
@@ -200,20 +190,13 @@ private:
hrt_abstime _last_work_buffer_access{0}; ///< timestamp when the buffers were last accessed
// prepend a root directory to each file/dir access to avoid enumerating the full FS tree (e.g. on Linux).
// Note that requests can still fall outside of the root dir by using ../..
#ifdef MAVLINK_FTP_UNIT_TEST
static constexpr const char _root_dir[] = "";
#else
// Path traversal via ".." is rejected by _validatePath().
static constexpr const char _root_dir[] = PX4_ROOTFSDIR;
#endif
static constexpr const int _root_dir_len = sizeof(_root_dir) - 1;
bool _last_reply_valid = false;
uint8_t _last_reply[MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL_LEN - MAVLINK_MSG_FILE_TRANSFER_PROTOCOL_FIELD_PAYLOAD_LEN
+ sizeof(PayloadHeader) + sizeof(uint32_t)];
// Mavlink test needs to be able to call send
friend class MavlinkFtpTest;
int _our_errno {0};
};
@@ -1,56 +0,0 @@
############################################################################
#
# Copyright (c) 2015 PX4 Development Team. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# 3. Neither the name PX4 nor the names of its contributors may be
# used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
############################################################################
px4_add_module(
MODULE modules__mavlink__mavlink_tests
MAIN mavlink_tests
STACK_MAIN 8192
INCLUDES
${MAVLINK_LIBRARY_DIR}
${MAVLINK_LIBRARY_DIR}/${CONFIG_MAVLINK_DIALECT}
COMPILE_FLAGS
-DMAVLINK_FTP_UNIT_TEST
#-DMAVLINK_FTP_DEBUG
-DMavlinkStream=MavlinkStreamTest
-DMavlinkFTP=MavlinkFTPTest
-Wno-cast-align # TODO: fix and enable
-Wno-address-of-packed-member # TODO: fix in c_library_v2
-Wno-double-promotion # The fix has been proposed as PR upstream (2020-03-08)
SRCS
mavlink_tests.cpp
mavlink_ftp_test.cpp
../mavlink_stream.cpp
../mavlink_ftp.cpp
DEPENDS
mavlink_c_generate
)
File diff suppressed because it is too large Load Diff
@@ -1,139 +0,0 @@
/****************************************************************************
*
* Copyright (C) 2014 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/// @file mavlink_ftp_test.h
/// @author Don Gagne <don@thegagnes.com>
#pragma once
#include <unit_test.h>
#include "../mavlink_bridge_header.h"
#include "../mavlink_ftp.h"
#include "../mavlink_main.h"
class MavlinkFtpTest : public UnitTest
{
public:
MavlinkFtpTest();
virtual ~MavlinkFtpTest() = default;
virtual bool run_tests(void);
static void receive_message_handler_generic(const mavlink_file_transfer_protocol_t *ftp_req, void *worker_data);
/// Worker data for stream handler
struct BurstInfo {
MavlinkFtpTest *ftp_test_class;
int burst_state;
bool single_packet_file;
uint32_t file_size;
uint8_t *file_bytes;
};
static void receive_message_handler_burst(const mavlink_file_transfer_protocol_t *ftp_req, void *worker_data);
static const uint8_t serverSystemId = 50; ///< System ID for server
static const uint8_t serverComponentId = 1; ///< Component ID for server
static const uint8_t serverChannel = 0; ///< Channel to send to
static const uint8_t clientSystemId = 1; ///< System ID for client
static const uint8_t clientComponentId = 0; ///< Component ID for client
// We don't want any of these
MavlinkFtpTest(const MavlinkFtpTest &);
MavlinkFtpTest &operator=(const MavlinkFtpTest &);
private:
virtual void _init(void);
virtual void _cleanup(void);
bool _create_test_files(void);
bool _remove_test_files(void);
bool _ack_test(void);
bool _bad_opcode_test(void);
bool _bad_datasize_test(void);
bool _list_test(void);
bool _list_eof_test(void);
bool _open_badfile_test(void);
bool _open_terminate_test(void);
bool _terminate_badsession_test(void);
bool _read_test(void);
bool _read_badsession_test(void);
bool _burst_test(void);
bool _removedirectory_test(void);
bool _createdirectory_test(void);
bool _removefile_test(void);
void _receive_message_handler_generic(const mavlink_file_transfer_protocol_t *ftp_req);
bool _setup_ftp_msg(const MavlinkFTP::PayloadHeader *payload_header,
const uint8_t *data, const uint8_t data_len,
mavlink_message_t *msg);
bool _decode_message(const mavlink_file_transfer_protocol_t *ftp_msg, const MavlinkFTP::PayloadHeader **payload);
bool _send_receive_msg(MavlinkFTP::PayloadHeader *payload_header,
const uint8_t *data,
const size_t data_len,
const MavlinkFTP::PayloadHeader **payload_reply);
void _cleanup_microsd(void);
/// A single download test case
struct DownloadTestCase {
const char *file;
const uint16_t length;
bool singlePacketRead;
bool exactlyFillPacket;
};
/// The set of test cases for download testing
static const DownloadTestCase _rgDownloadTestCases[];
/// States for stream download handler
enum {
burst_state_first_ack,
burst_state_last_ack,
burst_state_nak_eof,
burst_state_complete
};
bool _receive_message_handler_burst(const mavlink_file_transfer_protocol_t *ftp_req, BurstInfo *burst_info);
MavlinkFTP *_ftp_server;
Mavlink _mavlink;
uint16_t _expected_seq_number;
mavlink_file_transfer_protocol_t _reply_msg;
static const char _unittest_microsd_dir[];
static const char _unittest_microsd_file[];
};
bool mavlink_ftp_test(void);
@@ -1,47 +0,0 @@
/****************************************************************************
*
* Copyright (C) 2014 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* @file mavlink_tests.cpp
*/
#include <systemlib/err.h>
#include "mavlink_ftp_test.h"
extern "C" __EXPORT int mavlink_tests_main(int argc, char *argv[]);
int mavlink_tests_main(int argc, char *argv[])
{
return mavlink_ftp_test() ? 0 : -1;
}
@@ -79,6 +79,14 @@ public:
const z_loaned_bytes_t *payload = z_sample_payload(sample);
size_t len = z_bytes_len(payload);
// Validate payload size to prevent stack overflow from untrusted input.
// CDR payload = 4-byte header + serialized data, which should not exceed o_size + 4.
const size_t max_payload_size = _uorb_meta->o_size + 4;
if (len > max_payload_size || len < 4) {
return;
}
#if defined(Z_FEATURE_UNSTABLE_API)
// Check if payload is contiguous so we can decode directly on that pointer
z_view_slice_t view;