public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory
@ 2023-08-04 10:02 Gabriel Goller
  2023-08-04 15:49 ` Thomas Lamprecht
  2023-08-08 10:25 ` Wolfgang Bumiller
  0 siblings, 2 replies; 4+ messages in thread
From: Gabriel Goller @ 2023-08-04 10:02 UTC (permalink / raw)
  To: pbs-devel

When creating a backup, we now check if we have the correct permissions
(r,x) before entering a directory. This is mainly to prevent stat() from
failing with EACCESS errors. We also check if the directory contains
non-excluded files and warn the user.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pbs-client/src/pxar/create.rs | 47 +++++++++++++++++++++++++++++++++--
 1 file changed, 45 insertions(+), 2 deletions(-)

diff --git a/pbs-client/src/pxar/create.rs b/pbs-client/src/pxar/create.rs
index 2577cf98..f2333284 100644
--- a/pbs-client/src/pxar/create.rs
+++ b/pbs-client/src/pxar/create.rs
@@ -638,7 +638,7 @@ impl Archiver {
     async fn add_directory<T: SeqWrite + Send>(
         &mut self,
         encoder: &mut Encoder<'_, T>,
-        dir: Dir,
+        mut dir: Dir,
         dir_name: &CStr,
         metadata: &Metadata,
         stat: &FileStat,
@@ -663,9 +663,52 @@ impl Archiver {
                 skip_contents = !set.contains(&stat.st_dev);
             }
         }
+        if skip_contents {
+            log::warn!("Skipping mount point: {:?}", self.path);
+        }
+
+        let mode = nix::sys::stat::Mode::from_bits_truncate(stat.st_mode);
+        // if we have read and write permissions on the folder
+        if (!mode.contains(Mode::S_IRUSR) || !mode.contains(Mode::S_IXUSR))
+            && skip_contents == false
+        {
+            skip_contents = true;
+            let mut contains_non_excluded_files = false;
+            if mode.contains(Mode::S_IRUSR) {
+                // check if all children are excluded
+                for file in dir.iter() {
+                    let file = file?;
+
+                    let file_name = file.file_name().to_owned();
+                    let file_name_bytes = file_name.to_bytes();
+                    if file_name_bytes == b"." || file_name_bytes == b".." {
+                        continue;
+                    }
+                    let os_file_name = OsStr::from_bytes(file_name_bytes);
+                    assert_single_path_component(os_file_name)?;
+                    let full_path = self.path.join(os_file_name);
+                    let match_path = PathBuf::from("/").join(full_path.clone());
+                    if self
+                        .patterns
+                        .matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode))
+                        != Some(MatchType::Exclude)
+                    {
+                        contains_non_excluded_files = true;
+                        break;
+                    }
+                }
+            }
+            if contains_non_excluded_files {
+                log::warn!(
+                    "Skipping directory: {:?}, access denied (contains non-excluded files)",
+                    self.path
+                );
+            } else {
+                log::warn!("Skipping directory: {:?}, access denied", self.path);
+            }
+        }
 
         let result = if skip_contents {
-            log::info!("skipping mount point: {:?}", self.path);
             Ok(())
         } else {
             self.archive_dir_contents(&mut encoder, dir, false).await
-- 
2.39.2





^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory
  2023-08-04 10:02 [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory Gabriel Goller
@ 2023-08-04 15:49 ` Thomas Lamprecht
  2023-08-08 10:25 ` Wolfgang Bumiller
  1 sibling, 0 replies; 4+ messages in thread
From: Thomas Lamprecht @ 2023-08-04 15:49 UTC (permalink / raw)
  To: Proxmox Backup Server development discussion, Gabriel Goller

just some higher-level process review:

please use a v2 tag if it's the second version, both git format-patch and
git send-email support adding that conveniently using -v2 (or -v3 and so
one if a later revision).

Am 04/08/2023 um 12:02 schrieb Gabriel Goller:
> When creating a backup, we now check if we have the correct permissions
> (r,x) before entering a directory. This is mainly to prevent stat() from
> failing with EACCESS errors. We also check if the directory contains
> non-excluded files and warn the user.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---

please add a series changelog here, i.e., outside of what gets added to git
but easily discoverable. E.g., like:

changes v1:
- ...


See https://pve.proxmox.com/wiki/Developer_Documentation#Preparing_Patches

and btw., as I observed it in a reply to Fiona in the v1 of this fix: please
ensure you use "reply all" when replying, so that the mailing list stays
included.




^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory
  2023-08-04 10:02 [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory Gabriel Goller
  2023-08-04 15:49 ` Thomas Lamprecht
@ 2023-08-08 10:25 ` Wolfgang Bumiller
  2023-08-10  7:20   ` Gabriel Goller
  1 sibling, 1 reply; 4+ messages in thread
From: Wolfgang Bumiller @ 2023-08-08 10:25 UTC (permalink / raw)
  To: Gabriel Goller; +Cc: pbs-devel

NAK.

You can never assume that the permission bits apply to your user, and if
they do, there can be ACLs and security-module rules (Apparmor, SELinux,
...) involved and a lot more.
Permission checks are much more complicated, and checking them manually
is *always* wrong. Handling an `EPERM`/`EACCESS` error is the only
correct way.
(The closest you'd get would be with the `eaccess()` syscall, but that's
also unnecessarily racy, and an extra call for something you just don't
need...)

See my reply to the other patch about how I'd tackle this.

Anyway, further code comments down below.

On Fri, Aug 04, 2023 at 12:02:25PM +0200, Gabriel Goller wrote:
> When creating a backup, we now check if we have the correct permissions
> (r,x) before entering a directory. This is mainly to prevent stat() from
> failing with EACCESS errors. We also check if the directory contains
> non-excluded files and warn the user.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  pbs-client/src/pxar/create.rs | 47 +++++++++++++++++++++++++++++++++--
>  1 file changed, 45 insertions(+), 2 deletions(-)
> 
> diff --git a/pbs-client/src/pxar/create.rs b/pbs-client/src/pxar/create.rs
> index 2577cf98..f2333284 100644
> --- a/pbs-client/src/pxar/create.rs
> +++ b/pbs-client/src/pxar/create.rs
> @@ -638,7 +638,7 @@ impl Archiver {
>      async fn add_directory<T: SeqWrite + Send>(
>          &mut self,
>          encoder: &mut Encoder<'_, T>,
> -        dir: Dir,
> +        mut dir: Dir,
>          dir_name: &CStr,
>          metadata: &Metadata,
>          stat: &FileStat,
> @@ -663,9 +663,52 @@ impl Archiver {
>                  skip_contents = !set.contains(&stat.st_dev);
>              }
>          }
> +        if skip_contents {
> +            log::warn!("Skipping mount point: {:?}", self.path);
> +        }
> +
> +        let mode = nix::sys::stat::Mode::from_bits_truncate(stat.st_mode);
> +        // if we have read and write permissions on the folder

^ wrong comment?

> +        if (!mode.contains(Mode::S_IRUSR) || !mode.contains(Mode::S_IXUSR))

^ if you look at the bitflags docs' description of `.contains()` vs
`.intersects()` you'll see that `.contains()` requires all the bits in
question to be set.
You're asking "(does not contain A) or (does not contain B)"
=> which means: "not (both are set)"
=> `!mode.contains(Mode::S_IRUSR | Mode::S_IXUSR)` should be a shorter
version of the same, no? :-)

> +            && skip_contents == false
> +        {
> +            skip_contents = true;
> +            let mut contains_non_excluded_files = false;
> +            if mode.contains(Mode::S_IRUSR) {
> +                // check if all children are excluded
> +                for file in dir.iter() {
> +                    let file = file?;
> +
> +                    let file_name = file.file_name().to_owned();

So this bit is copied - but the original could use some cleanup ;-)
`to_owned()` allocates and is not necessary for the `.to_bytes()` call
and we don't actually need it owned anywhere up until the end

> +                    let file_name_bytes = file_name.to_bytes();
> +                    if file_name_bytes == b"." || file_name_bytes == b".." {
> +                        continue;
> +                    }
> +                    let os_file_name = OsStr::from_bytes(file_name_bytes);
> +                    assert_single_path_component(os_file_name)?;
> +                    let full_path = self.path.join(os_file_name);
> +                    let match_path = PathBuf::from("/").join(full_path.clone());
> +                    if self
> +                        .patterns
> +                        .matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode))
> +                        != Some(MatchType::Exclude)
> +                    {
> +                        contains_non_excluded_files = true;
> +                        break;
> +                    }
> +                }
> +            }
> +            if contains_non_excluded_files {
> +                log::warn!(
> +                    "Skipping directory: {:?}, access denied (contains non-excluded files)",

^ that is a weird error message which sounds like the existence of files
is the reason the access is denied ;-)

> +                    self.path
> +                );
> +            } else {
> +                log::warn!("Skipping directory: {:?}, access denied", self.path);
> +            }
> +        }
>  
>          let result = if skip_contents {
> -            log::info!("skipping mount point: {:?}", self.path);
>              Ok(())
>          } else {
>              self.archive_dir_contents(&mut encoder, dir, false).await
> -- 
> 2.39.2




^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory
  2023-08-08 10:25 ` Wolfgang Bumiller
@ 2023-08-10  7:20   ` Gabriel Goller
  0 siblings, 0 replies; 4+ messages in thread
From: Gabriel Goller @ 2023-08-10  7:20 UTC (permalink / raw)
  To: Wolfgang Bumiller; +Cc: pbs-devel

Thanks for the feedback, submitted a new patch (v3).

On 8/8/23 12:25, Wolfgang Bumiller wrote:
> NAK.
>
> You can never assume that the permission bits apply to your user, and if
> they do, there can be ACLs and security-module rules (Apparmor, SELinux,
> ...) involved and a lot more.
> Permission checks are much more complicated, and checking them manually
> is *always* wrong. Handling an `EPERM`/`EACCESS` error is the only
> correct way.
> (The closest you'd get would be with the `eaccess()` syscall, but that's
> also unnecessarily racy, and an extra call for something you just don't
> need...)
>
> See my reply to the other patch about how I'd tackle this.
>
> Anyway, further code comments down below.
>
> On Fri, Aug 04, 2023 at 12:02:25PM +0200, Gabriel Goller wrote:
>> When creating a backup, we now check if we have the correct permissions
>> (r,x) before entering a directory. This is mainly to prevent stat() from
>> failing with EACCESS errors. We also check if the directory contains
>> non-excluded files and warn the user.
>>
>> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
>> ---
>>   pbs-client/src/pxar/create.rs | 47 +++++++++++++++++++++++++++++++++--
>>   1 file changed, 45 insertions(+), 2 deletions(-)
>>
>> diff --git a/pbs-client/src/pxar/create.rs b/pbs-client/src/pxar/create.rs
>> index 2577cf98..f2333284 100644
>> --- a/pbs-client/src/pxar/create.rs
>> +++ b/pbs-client/src/pxar/create.rs
>> @@ -638,7 +638,7 @@ impl Archiver {
>>       async fn add_directory<T: SeqWrite + Send>(
>>           &mut self,
>>           encoder: &mut Encoder<'_, T>,
>> -        dir: Dir,
>> +        mut dir: Dir,
>>           dir_name: &CStr,
>>           metadata: &Metadata,
>>           stat: &FileStat,
>> @@ -663,9 +663,52 @@ impl Archiver {
>>                   skip_contents = !set.contains(&stat.st_dev);
>>               }
>>           }
>> +        if skip_contents {
>> +            log::warn!("Skipping mount point: {:?}", self.path);
>> +        }
>> +
>> +        let mode = nix::sys::stat::Mode::from_bits_truncate(stat.st_mode);
>> +        // if we have read and write permissions on the folder
> ^ wrong comment?
>
>> +        if (!mode.contains(Mode::S_IRUSR) || !mode.contains(Mode::S_IXUSR))
> ^ if you look at the bitflags docs' description of `.contains()` vs
> `.intersects()` you'll see that `.contains()` requires all the bits in
> question to be set.
> You're asking "(does not contain A) or (does not contain B)"
> => which means: "not (both are set)"
> => `!mode.contains(Mode::S_IRUSR | Mode::S_IXUSR)` should be a shorter
> version of the same, no? :-)
>
>> +            && skip_contents == false
>> +        {
>> +            skip_contents = true;
>> +            let mut contains_non_excluded_files = false;
>> +            if mode.contains(Mode::S_IRUSR) {
>> +                // check if all children are excluded
>> +                for file in dir.iter() {
>> +                    let file = file?;
>> +
>> +                    let file_name = file.file_name().to_owned();
> So this bit is copied - but the original could use some cleanup ;-)
> `to_owned()` allocates and is not necessary for the `.to_bytes()` call
> and we don't actually need it owned anywhere up until the end
>
>> +                    let file_name_bytes = file_name.to_bytes();
>> +                    if file_name_bytes == b"." || file_name_bytes == b".." {
>> +                        continue;
>> +                    }
>> +                    let os_file_name = OsStr::from_bytes(file_name_bytes);
>> +                    assert_single_path_component(os_file_name)?;
>> +                    let full_path = self.path.join(os_file_name);
>> +                    let match_path = PathBuf::from("/").join(full_path.clone());
>> +                    if self
>> +                        .patterns
>> +                        .matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode))
>> +                        != Some(MatchType::Exclude)
>> +                    {
>> +                        contains_non_excluded_files = true;
>> +                        break;
>> +                    }
>> +                }
>> +            }
>> +            if contains_non_excluded_files {
>> +                log::warn!(
>> +                    "Skipping directory: {:?}, access denied (contains non-excluded files)",
> ^ that is a weird error message which sounds like the existence of files
> is the reason the access is denied ;-)
>
>> +                    self.path
>> +                );
>> +            } else {
>> +                log::warn!("Skipping directory: {:?}, access denied", self.path);
>> +            }
>> +        }
>>   
>>           let result = if skip_contents {
>> -            log::info!("skipping mount point: {:?}", self.path);
>>               Ok(())
>>           } else {
>>               self.archive_dir_contents(&mut encoder, dir, false).await
>> -- 
>> 2.39.2




^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2023-08-10  7:20 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-08-04 10:02 [pbs-devel] [PATCH proxmox-backup] fix #4380: check permissions before entering directory Gabriel Goller
2023-08-04 15:49 ` Thomas Lamprecht
2023-08-08 10:25 ` Wolfgang Bumiller
2023-08-10  7:20   ` Gabriel Goller

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal