diff mbox series

[2/2] btrfs: canonicalize the device path before adding it

Message ID c6408ea85cd10e4042df528708dd9c2ec1db78c0.1727154543.git.wqu@suse.com (mailing list archive)
State New
Headers show
Series btrfs: enhance btrfs device path rename | expand

Commit Message

Qu Wenruo Sept. 24, 2024, 5:17 a.m. UTC
[PROBLEM]
Currently btrfs accepts any file path for its device, resulting some
weird situation:

 # ./mount_by_fd /dev/test/scratch1  /mnt/btrfs/

The program has the following source code:

 #include <fcntl.h>
 #include <stdio.h>
 #include <sys/mount.h>

 int main(int argc, char *argv[]) {
	int fd = open(argv[1], O_RDWR);
	char path[256];
	snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
	return mount(path, argv[2], "btrfs", 0, NULL);
 }

Then we can have the following weird device path:

 BTRFS: device fsid 2378be81-fe12-46d2-a9e8-68cf08dd98d5 devid 1 transid 7 /proc/self/fd/3 (253:2) scanned by mount_by_fd (18440)

Normally it's not a big deal, and later udev can trigger a device path
rename. But if udev didn't trigger, the device path "/proc/self/fd/3"
will show up in mtab.

[CAUSE]
For filename "/proc/self/fd/3", it means the opened file descriptor 3.
In above case, it's exactly the device we want to open, aka points to
"/dev/test/scratch1" which is another softlink pointing to "/dev/dm-2".

Inside btrfs we solve the path using LOOKUP_FOLLOW, which follows the
symbolic link and grab the proper block device.

But we also save the filename into btrfs_device::name, still resulting
the weird path.

[FIX]
Instead of unconditionally trust the path, check if the original file
(not following the symbolic link) is inside "/dev/", if not, then
manually lookup the path to its final destination, and use that as our
device path.

This allows us to still use symbolic links, like
"/dev/mapper/test-scratch" from LVM2, which is required for fstests runs
with LVM2 setup.

And for really weird names, like the above case, we solve it to
"/dev/dm-2" instead.

Link: https://bugzilla.suse.com/show_bug.cgi?id=1230641
Reported-by: Fabian Vogt <fvogt@suse.com>
Signed-off-by: Qu Wenruo <wqu@suse.com>
---
 fs/btrfs/volumes.c | 79 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 78 insertions(+), 1 deletion(-)

Comments

Filipe Manana Sept. 24, 2024, 12:12 p.m. UTC | #1
On Tue, Sep 24, 2024 at 6:18 AM Qu Wenruo <wqu@suse.com> wrote:
>
> [PROBLEM]
> Currently btrfs accepts any file path for its device, resulting some
> weird situation:
>
>  # ./mount_by_fd /dev/test/scratch1  /mnt/btrfs/
>
> The program has the following source code:
>
>  #include <fcntl.h>
>  #include <stdio.h>
>  #include <sys/mount.h>
>
>  int main(int argc, char *argv[]) {
>         int fd = open(argv[1], O_RDWR);
>         char path[256];
>         snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
>         return mount(path, argv[2], "btrfs", 0, NULL);
>  }
>
> Then we can have the following weird device path:
>
>  BTRFS: device fsid 2378be81-fe12-46d2-a9e8-68cf08dd98d5 devid 1 transid 7 /proc/self/fd/3 (253:2) scanned by mount_by_fd (18440)
>
> Normally it's not a big deal, and later udev can trigger a device path
> rename. But if udev didn't trigger, the device path "/proc/self/fd/3"
> will show up in mtab.
>
> [CAUSE]
> For filename "/proc/self/fd/3", it means the opened file descriptor 3.
> In above case, it's exactly the device we want to open, aka points to
> "/dev/test/scratch1" which is another softlink pointing to "/dev/dm-2".
>
> Inside btrfs we solve the path using LOOKUP_FOLLOW, which follows the
> symbolic link and grab the proper block device.
>
> But we also save the filename into btrfs_device::name, still resulting
> the weird path.
>
> [FIX]
> Instead of unconditionally trust the path, check if the original file
> (not following the symbolic link) is inside "/dev/", if not, then
> manually lookup the path to its final destination, and use that as our
> device path.
>
> This allows us to still use symbolic links, like
> "/dev/mapper/test-scratch" from LVM2, which is required for fstests runs
> with LVM2 setup.
>
> And for really weird names, like the above case, we solve it to
> "/dev/dm-2" instead.
>
> Link: https://bugzilla.suse.com/show_bug.cgi?id=1230641
> Reported-by: Fabian Vogt <fvogt@suse.com>
> Signed-off-by: Qu Wenruo <wqu@suse.com>
> ---
>  fs/btrfs/volumes.c | 79 +++++++++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 78 insertions(+), 1 deletion(-)
>
> diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
> index b713e4ebb362..8acb3c465783 100644
> --- a/fs/btrfs/volumes.c
> +++ b/fs/btrfs/volumes.c
> @@ -732,6 +732,70 @@ const u8 *btrfs_sb_fsid_ptr(const struct btrfs_super_block *sb)
>         return has_metadata_uuid ? sb->metadata_uuid : sb->fsid;
>  }
>
> +/*
> + * We can have very wide soft links passed in.

Wide? Did you mean wild in the sense of unusual?

> + * One example is "/proc/<uid>/fd/<fd>", which can be a soft link to
> + * a proper block device.
> + *
> + * But it's never a good idea to use those weird names.
> + * Here we check if the path (not following symlinks) is a good one inside
> + * "/dev/".
> + */
> +static bool is_good_dev_path(const char *dev_path)
> +{
> +       struct path path = { .mnt = NULL, .dentry = NULL };
> +       char *path_buf = NULL;
> +       char *resolved_path;
> +       bool is_good = false;
> +       int ret;
> +
> +       path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
> +       if (!path_buf)
> +               goto out;
> +
> +       /*
> +        * Do not follow soft link, just check if the original path is inside
> +        * "/dev/".
> +        */
> +       ret = kern_path(dev_path, 0, &path);
> +       if (ret)
> +               goto out;
> +       resolved_path = d_path(&path, path_buf, PATH_MAX);
> +       if (strncmp(resolved_path, "/dev/", strlen("/dev/")))
> +               goto out;
> +       is_good = true;
> +out:
> +       kfree(path_buf);
> +       path_put(&path);
> +       return is_good;
> +}
> +
> +static int get_canonical_dev_path(const char *dev_path, char *canonical)
> +{
> +       struct path path = { .mnt = NULL, .dentry = NULL };
> +       char *path_buf = NULL;
> +       char *resolved_path;
> +       int ret;
> +
> +       path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
> +       if (!path_buf) {
> +               ret = -ENOMEM;
> +               goto out;
> +       }
> +
> +       ret = kern_path(dev_path, LOOKUP_FOLLOW, &path);
> +       if (ret) {
> +               pr_info("path lookup failed for %s: %d\n", dev_path, ret);

Why not btrfs_info(), or better yet, btrfs_warn()?

It accepts a NULL fs_info argument and allows for a more standardized
btrfs message, with a proper prefix, etc.


> +               goto out;
> +       }
> +       resolved_path = d_path(&path, path_buf, PATH_MAX);
> +       strncpy(canonical, resolved_path, PATH_MAX - 1);

Please don't use strncpy(). This is strongly discouraged due to
security issues, see:

https://github.com/KSPP/linux/issues/90
https://www.kernel.org/doc/html/latest/process/deprecated.html#strncpy-on-nul-terminated-strings

> +out:
> +       kfree(path_buf);
> +       path_put(&path);
> +       return ret;
> +}
> +
>  static bool is_same_device(struct btrfs_device *device, const char *new_path)
>  {
>         struct path old = { .mnt = NULL, .dentry = NULL };
> @@ -1408,12 +1472,23 @@ struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
>         bool new_device_added = false;
>         struct btrfs_device *device = NULL;
>         struct file *bdev_file;
> +       char *canonical_path = NULL;
>         u64 bytenr;
>         dev_t devt;
>         int ret;
>
>         lockdep_assert_held(&uuid_mutex);
>
> +       if (!is_good_dev_path(path)) {
> +               canonical_path = kmalloc(PATH_MAX, GFP_KERNEL);
> +               if (canonical_path) {
> +                       ret = get_canonical_dev_path(path, canonical_path);
> +                       if (ret < 0) {
> +                               kfree(canonical_path);
> +                               canonical_path = NULL;
> +                       }
> +               }
> +       }
>         /*
>          * Avoid an exclusive open here, as the systemd-udev may initiate the
>          * device scan which may race with the user's mount or mkfs command,
> @@ -1458,7 +1533,8 @@ struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
>                 goto free_disk_super;
>         }
>
> -       device = device_list_add(path, disk_super, &new_device_added);
> +       device = device_list_add(canonical_path ? canonical_path : path,

Can use the shortcut:    canonical_path ?: path

The rest looks fine, thanks.


> +                                disk_super, &new_device_added);
>         if (!IS_ERR(device) && new_device_added)
>                 btrfs_free_stale_devices(device->devt, device);
>
> @@ -1467,6 +1543,7 @@ struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
>
>  error_bdev_put:
>         fput(bdev_file);
> +       kfree(canonical_path);
>
>         return device;
>  }
> --
> 2.46.1
>
>
Roman Mamedov Sept. 24, 2024, 4:21 p.m. UTC | #2
On Tue, 24 Sep 2024 13:12:09 +0100
Filipe Manana <fdmanana@kernel.org> wrote:

> > +/*
> > + * We can have very wide soft links passed in.
> 
> Wide? Did you mean wild in the sense of unusual?

Samba uses this term to refer to links pointing outside of the particular
network share tree:
https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#WIDELINKS
https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#ALLOWINSECUREWIDELINKS

Not sure what is the origin or etymology, guess it may stem from "VFS-wide",
links across the entire pathname space.
Qu Wenruo Sept. 24, 2024, 9:16 p.m. UTC | #3
在 2024/9/24 21:42, Filipe Manana 写道:
> On Tue, Sep 24, 2024 at 6:18 AM Qu Wenruo <wqu@suse.com> wrote:
>>
>> [PROBLEM]
>> Currently btrfs accepts any file path for its device, resulting some
>> weird situation:
>>
>>   # ./mount_by_fd /dev/test/scratch1  /mnt/btrfs/
>>
>> The program has the following source code:
>>
>>   #include <fcntl.h>
>>   #include <stdio.h>
>>   #include <sys/mount.h>
>>
>>   int main(int argc, char *argv[]) {
>>          int fd = open(argv[1], O_RDWR);
>>          char path[256];
>>          snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
>>          return mount(path, argv[2], "btrfs", 0, NULL);
>>   }
>>
>> Then we can have the following weird device path:
>>
>>   BTRFS: device fsid 2378be81-fe12-46d2-a9e8-68cf08dd98d5 devid 1 transid 7 /proc/self/fd/3 (253:2) scanned by mount_by_fd (18440)
>>
>> Normally it's not a big deal, and later udev can trigger a device path
>> rename. But if udev didn't trigger, the device path "/proc/self/fd/3"
>> will show up in mtab.
>>
>> [CAUSE]
>> For filename "/proc/self/fd/3", it means the opened file descriptor 3.
>> In above case, it's exactly the device we want to open, aka points to
>> "/dev/test/scratch1" which is another softlink pointing to "/dev/dm-2".
>>
>> Inside btrfs we solve the path using LOOKUP_FOLLOW, which follows the
>> symbolic link and grab the proper block device.
>>
>> But we also save the filename into btrfs_device::name, still resulting
>> the weird path.
>>
>> [FIX]
>> Instead of unconditionally trust the path, check if the original file
>> (not following the symbolic link) is inside "/dev/", if not, then
>> manually lookup the path to its final destination, and use that as our
>> device path.
>>
>> This allows us to still use symbolic links, like
>> "/dev/mapper/test-scratch" from LVM2, which is required for fstests runs
>> with LVM2 setup.
>>
>> And for really weird names, like the above case, we solve it to
>> "/dev/dm-2" instead.
>>
>> Link: https://bugzilla.suse.com/show_bug.cgi?id=1230641
>> Reported-by: Fabian Vogt <fvogt@suse.com>
>> Signed-off-by: Qu Wenruo <wqu@suse.com>
>> ---
>>   fs/btrfs/volumes.c | 79 +++++++++++++++++++++++++++++++++++++++++++++-
>>   1 file changed, 78 insertions(+), 1 deletion(-)
>>
>> diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
>> index b713e4ebb362..8acb3c465783 100644
>> --- a/fs/btrfs/volumes.c
>> +++ b/fs/btrfs/volumes.c
>> @@ -732,6 +732,70 @@ const u8 *btrfs_sb_fsid_ptr(const struct btrfs_super_block *sb)
>>          return has_metadata_uuid ? sb->metadata_uuid : sb->fsid;
>>   }
>>
>> +/*
>> + * We can have very wide soft links passed in.
>
> Wide? Did you mean wild in the sense of unusual?

My bad, I mean "weird".

>
>> + * One example is "/proc/<uid>/fd/<fd>", which can be a soft link to
>> + * a proper block device.
>> + *
>> + * But it's never a good idea to use those weird names.
>> + * Here we check if the path (not following symlinks) is a good one inside
>> + * "/dev/".
>> + */
>> +static bool is_good_dev_path(const char *dev_path)
>> +{
>> +       struct path path = { .mnt = NULL, .dentry = NULL };
>> +       char *path_buf = NULL;
>> +       char *resolved_path;
>> +       bool is_good = false;
>> +       int ret;
>> +
>> +       path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
>> +       if (!path_buf)
>> +               goto out;
>> +
>> +       /*
>> +        * Do not follow soft link, just check if the original path is inside
>> +        * "/dev/".
>> +        */
>> +       ret = kern_path(dev_path, 0, &path);
>> +       if (ret)
>> +               goto out;
>> +       resolved_path = d_path(&path, path_buf, PATH_MAX);
>> +       if (strncmp(resolved_path, "/dev/", strlen("/dev/")))
>> +               goto out;
>> +       is_good = true;
>> +out:
>> +       kfree(path_buf);
>> +       path_put(&path);
>> +       return is_good;
>> +}
>> +
>> +static int get_canonical_dev_path(const char *dev_path, char *canonical)
>> +{
>> +       struct path path = { .mnt = NULL, .dentry = NULL };
>> +       char *path_buf = NULL;
>> +       char *resolved_path;
>> +       int ret;
>> +
>> +       path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
>> +       if (!path_buf) {
>> +               ret = -ENOMEM;
>> +               goto out;
>> +       }
>> +
>> +       ret = kern_path(dev_path, LOOKUP_FOLLOW, &path);
>> +       if (ret) {
>> +               pr_info("path lookup failed for %s: %d\n", dev_path, ret);
>
> Why not btrfs_info(), or better yet, btrfs_warn()?

Oh, it's some debug code not removed.

In fact there should be no need to output anything.
We can always fallback to the super weird name.

>
> It accepts a NULL fs_info argument and allows for a more standardized
> btrfs message, with a proper prefix, etc.
>
>
>> +               goto out;
>> +       }
>> +       resolved_path = d_path(&path, path_buf, PATH_MAX);
>> +       strncpy(canonical, resolved_path, PATH_MAX - 1);
>
> Please don't use strncpy(). This is strongly discouraged due to
> security issues, see:
>
> https://github.com/KSPP/linux/issues/90
> https://www.kernel.org/doc/html/latest/process/deprecated.html#strncpy-on-nul-terminated-strings

Thanks a lot for pointing to this.

Thanks,
Qu
>
>> +out:
>> +       kfree(path_buf);
>> +       path_put(&path);
>> +       return ret;
>> +}
>> +
>>   static bool is_same_device(struct btrfs_device *device, const char *new_path)
>>   {
>>          struct path old = { .mnt = NULL, .dentry = NULL };
>> @@ -1408,12 +1472,23 @@ struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
>>          bool new_device_added = false;
>>          struct btrfs_device *device = NULL;
>>          struct file *bdev_file;
>> +       char *canonical_path = NULL;
>>          u64 bytenr;
>>          dev_t devt;
>>          int ret;
>>
>>          lockdep_assert_held(&uuid_mutex);
>>
>> +       if (!is_good_dev_path(path)) {
>> +               canonical_path = kmalloc(PATH_MAX, GFP_KERNEL);
>> +               if (canonical_path) {
>> +                       ret = get_canonical_dev_path(path, canonical_path);
>> +                       if (ret < 0) {
>> +                               kfree(canonical_path);
>> +                               canonical_path = NULL;
>> +                       }
>> +               }
>> +       }
>>          /*
>>           * Avoid an exclusive open here, as the systemd-udev may initiate the
>>           * device scan which may race with the user's mount or mkfs command,
>> @@ -1458,7 +1533,8 @@ struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
>>                  goto free_disk_super;
>>          }
>>
>> -       device = device_list_add(path, disk_super, &new_device_added);
>> +       device = device_list_add(canonical_path ? canonical_path : path,
>
> Can use the shortcut:    canonical_path ?: path
>
> The rest looks fine, thanks.
>
>
>> +                                disk_super, &new_device_added);
>>          if (!IS_ERR(device) && new_device_added)
>>                  btrfs_free_stale_devices(device->devt, device);
>>
>> @@ -1467,6 +1543,7 @@ struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
>>
>>   error_bdev_put:
>>          fput(bdev_file);
>> +       kfree(canonical_path);
>>
>>          return device;
>>   }
>> --
>> 2.46.1
>>
>>
>
diff mbox series

Patch

diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
index b713e4ebb362..8acb3c465783 100644
--- a/fs/btrfs/volumes.c
+++ b/fs/btrfs/volumes.c
@@ -732,6 +732,70 @@  const u8 *btrfs_sb_fsid_ptr(const struct btrfs_super_block *sb)
 	return has_metadata_uuid ? sb->metadata_uuid : sb->fsid;
 }
 
+/*
+ * We can have very wide soft links passed in.
+ * One example is "/proc/<uid>/fd/<fd>", which can be a soft link to
+ * a proper block device.
+ *
+ * But it's never a good idea to use those weird names.
+ * Here we check if the path (not following symlinks) is a good one inside
+ * "/dev/".
+ */
+static bool is_good_dev_path(const char *dev_path)
+{
+	struct path path = { .mnt = NULL, .dentry = NULL };
+	char *path_buf = NULL;
+	char *resolved_path;
+	bool is_good = false;
+	int ret;
+
+	path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
+	if (!path_buf)
+		goto out;
+
+	/*
+	 * Do not follow soft link, just check if the original path is inside
+	 * "/dev/".
+	 */
+	ret = kern_path(dev_path, 0, &path);
+	if (ret)
+		goto out;
+	resolved_path = d_path(&path, path_buf, PATH_MAX);
+	if (strncmp(resolved_path, "/dev/", strlen("/dev/")))
+		goto out;
+	is_good = true;
+out:
+	kfree(path_buf);
+	path_put(&path);
+	return is_good;
+}
+
+static int get_canonical_dev_path(const char *dev_path, char *canonical)
+{
+	struct path path = { .mnt = NULL, .dentry = NULL };
+	char *path_buf = NULL;
+	char *resolved_path;
+	int ret;
+
+	path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
+	if (!path_buf) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	ret = kern_path(dev_path, LOOKUP_FOLLOW, &path);
+	if (ret) {
+		pr_info("path lookup failed for %s: %d\n", dev_path, ret);
+		goto out;
+	}
+	resolved_path = d_path(&path, path_buf, PATH_MAX);
+	strncpy(canonical, resolved_path, PATH_MAX - 1);
+out:
+	kfree(path_buf);
+	path_put(&path);
+	return ret;
+}
+
 static bool is_same_device(struct btrfs_device *device, const char *new_path)
 {
 	struct path old = { .mnt = NULL, .dentry = NULL };
@@ -1408,12 +1472,23 @@  struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
 	bool new_device_added = false;
 	struct btrfs_device *device = NULL;
 	struct file *bdev_file;
+	char *canonical_path = NULL;
 	u64 bytenr;
 	dev_t devt;
 	int ret;
 
 	lockdep_assert_held(&uuid_mutex);
 
+	if (!is_good_dev_path(path)) {
+		canonical_path = kmalloc(PATH_MAX, GFP_KERNEL);
+		if (canonical_path) {
+			ret = get_canonical_dev_path(path, canonical_path);
+			if (ret < 0) {
+				kfree(canonical_path);
+				canonical_path = NULL;
+			}
+		}
+	}
 	/*
 	 * Avoid an exclusive open here, as the systemd-udev may initiate the
 	 * device scan which may race with the user's mount or mkfs command,
@@ -1458,7 +1533,8 @@  struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
 		goto free_disk_super;
 	}
 
-	device = device_list_add(path, disk_super, &new_device_added);
+	device = device_list_add(canonical_path ? canonical_path : path,
+				 disk_super, &new_device_added);
 	if (!IS_ERR(device) && new_device_added)
 		btrfs_free_stale_devices(device->devt, device);
 
@@ -1467,6 +1543,7 @@  struct btrfs_device *btrfs_scan_one_device(const char *path, blk_mode_t flags,
 
 error_bdev_put:
 	fput(bdev_file);
+	kfree(canonical_path);
 
 	return device;
 }