diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index 5a5537b7..0380a785 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -136,6 +136,10 @@ class Mounts(AndroidArtifact): mount_options = fields[5].split(",") super_options = fields[separator + 3].split(",") options = list(dict.fromkeys(mount_options + super_options)) + # fields[5] are the per-mount (VFS) flags, the field after the + # separator the superblock's; a mount is writable only if both + # allow it. The merged list made every read-only overlay a HIGH. + is_read_write = "rw" in mount_options and "rw" in super_options mount_point = fields[4].replace("\\040", " ") device = fields[separator + 2].replace("\\040", " ") filesystem_type = fields[separator + 1] @@ -156,7 +160,7 @@ class Mounts(AndroidArtifact): "options_list": options, "optional_fields": fields[6:separator], "is_system_partition": is_system, - "is_read_write": "rw" in options, + "is_read_write": is_read_write, "process_ids": [process_id], } ) @@ -191,7 +195,14 @@ class Mounts(AndroidArtifact): ) # Check for other suspicious mount options - suspicious_opts = [opt for opt in options if opt in SUSPICIOUS_OPTIONS] + # `rw` sitting in the merged option list is not evidence of + # writability (see is_read_write in parse_mountinfo); every other + # suspicious option is meaningful in either list. + suspicious_opts = [ + opt + for opt in options + if opt in SUSPICIOUS_OPTIONS and (opt != "rw" or mount["is_read_write"]) + ] if suspicious_opts and mount["is_system_partition"]: if ( "noatime" in mount["mount_options"] diff --git a/tests/android/test_artifact_mounts.py b/tests/android/test_artifact_mounts.py new file mode 100644 index 00000000..6b7d1faf --- /dev/null +++ b/tests/android/test_artifact_mounts.py @@ -0,0 +1,43 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2026 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ +"""A mount is writable only if both mountinfo option layers allow it.""" + +from mvt.android.artifacts.mounts import Mounts +from mvt.common.alerts import AlertLevel + +# fields[5] is the per-mount (VFS) layer, the field after "-" the superblock. +RO_OVERLAY = ( + "304 166 0:130 / /product/usr rw,relatime shared:63 - overlay overlay " + "ro,seclabel,lowerdir=/mnt/vendor/ext/product/usr:/product/usr" +) +RW_SYSTEM = ( + "305 166 0:131 / /system rw,relatime shared:64 - ext4 /dev/block/dm-1 " + "rw,seclabel,errors=panic" +) + + +def _run(line): + mounts = Mounts() + mounts.results = Mounts.parse_mountinfo(line, 1) + mounts.check_indicators() + return mounts + + +class TestMountsReadWriteLayers: + def test_rw_vfs_over_ro_superblock_is_not_writable(self): + mounts = _run(RO_OVERLAY) + assert mounts.results[0]["is_read_write"] is False + assert mounts.alertstore.count(AlertLevel.HIGH) == 0 + assert mounts.alertstore.count(AlertLevel.MEDIUM) == 0 + + def test_rw_in_both_layers_still_alerts(self): + mounts = _run(RW_SYSTEM) + assert mounts.results[0]["is_read_write"] is True + assert mounts.alertstore.count(AlertLevel.HIGH) == 1 + + def test_both_option_layers_are_still_reported(self): + mounts = _run(RO_OVERLAY) + options = mounts.results[0]["options_list"] + assert "rw" in options and "ro" in options