Repository: ArduPilot/ardupilot
Pull request #29988: Disallow setting of readonly/internal parameters via mavftp 
Author: login: peterbarker
Labels: name: BUG
name: WikiNeeded
description: needs wiki update
Created: 2025-05-06T07:07:41Z | Merged: 2025-05-12T03:01:03Z

Closes https://github.com/ardupilot/ardupilot/issues/29985

This work sponsored by Freespace Solutions



Diff:
diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py
index b30491817b2f88..0f2db3b733331a 100644
--- a/Tools/autotest/arducopter.py
+++ b/Tools/autotest/arducopter.py
@@ -8,6 +8,7 @@
 import math
 import os
 import shutil
+import tempfile
 import time
 import numpy
 
@@ -11990,6 +11991,7 @@ def tests1e(self):
              self.MAV_CMD_NAV_RETURN_TO_LAUNCH,
              self.MAV_CMD_NAV_VTOL_LAND,
              self.clear_roi,
+             self.ReadOnlyDefaults,
         ])
         return ret
 
@@ -13410,6 +13412,49 @@ def CommonOrigin(self):
         # restart GPS driver
         self.reboot_sitl()
 
+    def ReadOnlyDefaults(self):
+        '''test that defaults marked "readonly" can't be set'''
+        defaults_filepath = tempfile.NamedTemporaryFile(mode='w', delete=False)
+        defaults_filepath.write("""
+DISARM_DELAY 77 @READONLY
+RTL_ALT 123
+RTL_ALT_FINAL 129
+""")
+        defaults_filepath.close()
+        self.customise_SITL_commandline([
+        ], defaults_filepath=defaults_filepath.name)
+
+        self.context_collect('STATUSTEXT')
+        self.send_set_parameter_direct("DISARM_DELAY", 88)
+
+        self.wait_statustext("Param write denied (DISARM_DELAY)")
+        self.assert_parameter_value("DISARM_DELAY", 77)
+        self.assert_parameter_value("RTL_ALT", 123)
+
+        self.start_subtest('Ensure something is writable....')
+        self.set_parameter('RTL_ALT_FINAL', 101)
+
+        new_values_filepath = tempfile.NamedTemporaryFile(mode='w', delete=False)
+        new_values_filepath.write("""
+DISARM_DELAY 99
+RTL_ALT 111
+""")
+        new_values_filepath.close()
+
+        self.start_subtest("Ensure parameters can't be set via FTP either")
+        mavproxy = self.start_mavproxy()
+        # can't do two FTP things at once, so wait until parameters are received
+        mavproxy.expect("Received .* parameters")
+        self.mavproxy_load_module(mavproxy, 'ftp')
+        mavproxy.send(f"param ftpload {new_values_filepath.name}\n")
+        mavproxy.expect("Loaded")
+        self.delay_sim_time(1)
+        self.stop_mavproxy(mavproxy)
+
+        self.assert_parameter_value("DISARM_DELAY", 77)
+        self.assert_parameter_value("RTL_ALT", 111)
+        self.assert_parameter_value('RTL_ALT_FINAL', 101)
+
     def ScriptingFlipMode(self):
         '''test adding custom mode from scripting'''
         # Really it would be nice to check for the AVAILABLE_MODES message, but pymavlink does not understand them yet.
diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py
index 01b87670abbd69..5790e5bf7f168c 100644
--- a/Tools/autotest/vehicle_test_suite.py
+++ b/Tools/autotest/vehicle_test_suite.py
@@ -216,6 +216,7 @@ def __init__(self):
         self.installed_scripts = []
         self.installed_modules = []
         self.overridden_message_rates = {}
+        self.raising_debug_trap_on_exceptions = False
 
 
 # https://stackoverflow.com/questions/616645/how-do-i-duplicate-sys-stdout-to-a-log-file-in-python
@@ -3684,6 +3685,35 @@ def HIGH_LATENCY2(self):
             raise NotAchievedException("Air Temperature not received from HIGH_LATENCY2")
         self.HIGH_LATENCY2_links()
 
+    def context_set_send_debug_trap_on_exceptions(self, value=True):
+        '''send a debug trap to ArduPilot if an ErrorException is raised.'''
+
+        # this is a diagnostic tool, only expected to be used for
+        # debugging, never for committed code
+
+        def trace_calls(frame, event, arg):
+            if event == 'exception':
+                exc_type, exc_value, tb = arg
+                if issubclass(exc_type, ErrorException):
+                    print(f"[Tracer] Exception raised: {exc_type}")
+                    self.send_debug_trap()
+            return trace_calls
+
+        context = self.context_get()
+
+        if value:
+            if sys.gettrace() is not None:
+                raise ValueError("Can't trace, something else already is")
+            sys.settrace(trace_calls)
+            context.raising_debug_trap_on_exceptions = True
+            return
+
+        if not sys.gettrace():
+            raise ValueError("Expected to see something tracing")
+
+        context.raising_debug_trap_on_exceptions = False
+        sys.settrace(None)
+
     def context_set_message_rate_hz(self, id, rate_hz, run_cmd=None):
         if run_cmd is None:
             run_cmd = self.run_cmd
@@ -6456,6 +6486,8 @@ def context_pop(self, process_interaction_allowed=True, hooks_already_removed=Fa
             self.remove_installed_modules(module)
         if dead.sitl_commandline_customised and len(self.contexts):
             self.contexts[-1].sitl_commandline_customised = True
+        if dead.raising_debug_trap_on_exceptions:
+            sys.settrace(None)
 
         dead_parameters_dict = {}
         for p in dead.parameters:
diff --git a/libraries/AP_Filesystem/AP_Filesystem_Param.cpp b/libraries/AP_Filesystem/AP_Filesystem_Param.cpp
index d79f2cd7b3a09a..e043cb2633d68d 100644
--- a/libraries/AP_Filesystem/AP_Filesystem_Param.cpp
+++ b/libraries/AP_Filesystem/AP_Filesystem_Param.cpp
@@ -581,8 +581,10 @@ bool AP_Filesystem_Param::param_upload_parse(const rfile &r, bool &need_retry)
 
         b += 2 + name_len;
 
+        // assume that this write to the Param filesystem is coming
+        // via mavftp:
         AP_Param *p = AP_Param::find(name, &ptype2, &flags2);
-        if (p == nullptr) {
+        if (p == nullptr || !p->allow_set_via_mavlink(flags2)) {
             if (ptype == AP_PARAM_INT8) {
                 b++;
             } else if (ptype == AP_PARAM_INT16) {
diff --git a/libraries/AP_Param/AP_Param.cpp b/libraries/AP_Param/AP_Param.cpp
index d23a10e085ae40..fb9f435adebdca 100644
--- a/libraries/AP_Param/AP_Param.cpp
+++ b/libraries/AP_Param/AP_Param.cpp
@@ -1448,6 +1448,25 @@ bool AP_Param::is_read_only(void) const
     return false;
 }
 
+// returns true if this parameter should be settable via the
+// MAVLink interface:
+bool AP_Param::allow_set_via_mavlink(uint16_t flags) const
+{
+    if (is_read_only()) {
+        return false;
+    }
+
+    if (flags & AP_PARAM_FLAG_INTERNAL_USE_ONLY) {
+        // the user can set BRD_OPTIONS to enable set of internal
+        // parameters, for developer testing or unusual use cases
+        if (!AP_BoardConfig::allow_set_internal_parameters()) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
 // set a AP_Param variable to a specified value
 void AP_Param::set_value(enum ap_var_type type, void *ptr, float value)
 {
diff --git a/libraries/AP_Param/AP_Param.h b/libraries/AP_Param/AP_Param.h
index 70833df28f94d3..70e57b720924ee 100644
--- a/libraries/AP_Param/AP_Param.h
+++ b/libraries/AP_Param/AP_Param.h
@@ -568,7 +568,11 @@ class AP_Param
 
     // return the persistent top level key for the ParamToken key
     static uint16_t get_persistent_key(uint16_t key) { return var_info(key).key; }
-    
+
+    // returns true if this parameter should be settable via the
+    // MAVLink interface:
+    bool allow_set_via_mavlink(uint16_t flags) const;
+
     // count of parameters in tree
     static uint16_t count_parameters(void);
 
diff --git a/libraries/GCS_MAVLink/GCS_Param.cpp b/libraries/GCS_MAVLink/GCS_Param.cpp
index 5d73e8a646b095..c193cbf5ef08e0 100644
--- a/libraries/GCS_MAVLink/GCS_Param.cpp
+++ b/libraries/GCS_MAVLink/GCS_Param.cpp
@@ -281,15 +281,7 @@ void GCS_MAVLINK::handle_param_set(const mavlink_message_t &msg)
 
     float old_value = vp->cast_to_float(var_type);
 
-    if (parameter_flags & AP_PARAM_FLAG_INTERNAL_USE_ONLY) {
-        // the user can set BRD_OPTIONS to enable set of internal
-        // parameters, for developer testing or unusual use cases
-        if (AP_BoardConfig::allow_set_internal_parameters()) {
-            parameter_flags &= ~AP_PARAM_FLAG_INTERNAL_USE_ONLY;
-        }
-    }
-
-    if ((parameter_flags & AP_PARAM_FLAG_INTERNAL_USE_ONLY) || vp->is_read_only()) {
+    if (!vp->allow_set_via_mavlink(parameter_flags)) {
         GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Param write denied (%s)", key);
         // send the readonly value
         send_parameter_value(key, var_type, old_value);


Reviews:
user: login: rmackay9
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:11:13Z
user: login: peterbarker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:47:10Z
user: login: tridge
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T01:02:30Z
user: login: rmackay9
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T02:38:10Z
user: login: rmackay9
body: sorry, I hate to be annoying but I'd just like to be sure the small requested comment fix goes in before merging
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T02:38:44Z
user: login: peterbarker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T05:09:14Z
user: login: rmackay9
body: I think we can just merge this now can't we?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T00:20:52Z

Review comments:
diff_hunk: @@ -568,7 +568,9 @@ class AP_Param
 
     // return the persistent top level key for the ParamToken key
     static uint16_t get_persistent_key(uint16_t key) { return var_info(key).key; }
-    
+
+    bool allow_set_via_mavlink(uint16_t flags) const;
path: libraries/AP_Param/AP_Param.h
user: login: rmackay9
body: can I be annoying and ask for a comment above this function?
created_at: 2025-05-06T07:11:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -568,7 +568,9 @@ class AP_Param
 
     // return the persistent top level key for the ParamToken key
     static uint16_t get_persistent_key(uint16_t key) { return var_info(key).key; }
-    
+
+    bool allow_set_via_mavlink(uint16_t flags) const;
path: libraries/AP_Param/AP_Param.h
user: login: peterbarker
body: Done

created_at: 2025-05-06T07:47:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -568,7 +568,9 @@ class AP_Param
 
     // return the persistent top level key for the ParamToken key
     static uint16_t get_persistent_key(uint16_t key) { return var_info(key).key; }
-    
+
+    bool allow_set_via_mavlink(uint16_t flags) const;
path: libraries/AP_Param/AP_Param.h
user: login: rmackay9
body: Hi @peterbarker, for some strange reason I don't see the comment still..
created_at: 2025-05-08T02:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -568,7 +568,9 @@ class AP_Param
 
     // return the persistent top level key for the ParamToken key
     static uint16_t get_persistent_key(uint16_t key) { return var_info(key).key; }
-    
+
+    bool allow_set_via_mavlink(uint16_t flags) const;
path: libraries/AP_Param/AP_Param.h
user: login: peterbarker
body: My apologies, I managed to force-push over my own patch adding the comments:
![image](https://github.com/user-attachments/assets/55ae8040-2a68-4297-82d3-1dd1c233b0f9)

That first link shows the patches going in.

created_at: 2025-05-09T05:09:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: peterbarker
created_at: 2025-05-09T05:09:38Z
body: > sorry, I hate to be annoying but I'd just like to be sure the small requested comment fix goes in before merging

Comments re-added.

author_association: CONTRIBUTOR
user: login: peterbarker
created_at: 2025-05-12T03:01:38Z
body: > I think we can just merge this now can't we?

Well... I just did, so yes :-)

I've merged this so I can work on a backport PR for it.

author_association: CONTRIBUTOR
user: login: rmackay9
created_at: 2025-06-24T10:43:43Z
body: I've included the flight code change in 4.6.2-beta1 but could not include the autotest changes due to a merge conflict
author_association: CONTRIBUTOR
user: login: rmackay9
created_at: 2025-06-25T07:35:14Z
body: This is included in 4.6.2-beta1 via https://github.com/ArduPilot/ardupilot/pull/30464
author_association: CONTRIBUTOR