Hi Aaron,

On Mon, 2026-06-01 at 20:23 -0400, Aaron Merey wrote:
> > @@ -257,4 +259,52 @@ xbasename(const char *s)
> >  }
> >  #pragma GCC poison basename
> > 
> > +/* There is no mkstempat needed for creating a temp file in a specific
> > +   directory. Needed e.g. in combination with renameat to atomicly
> > +   replace a file. So define one ourselves. Like mkstemp the template
> > +   must end with must be "XXXXXX", which are replaced by an unique
> 
> Looks like the sentence ends early here.

Oops. And it says "must end with must be..." Clearly I not speak
English very goodly... Changed to:

Like mkstemp the template must end in "XXXXXX", which are replaced by
an unique filename suffix. The file is created with user read/write
permissions only in the given dirfd using openat. 

> > 
> > @@ -611,26 +623,68 @@ process_file (const char *fname)
> >        scnnames = xcalloc (shnum, sizeof (char *));
> >      }
> > 
> > -  /* Create a new (temporary) ELF file for the result.  */
> > -  if (foutput == NULL)
> > +  /* Now deal with the output.  If we can (exclusively) open the
> > +     output file directly, we can just use that.  */
> > +  fnew = xstrdup (foutput == NULL ? fname : foutput);
> > +  fdnew = open (fnew, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC,
> > +               st.st_mode & ALLPERMS);
> > +
> > +  /* If we cannot open the output exclusively for writing directly
> > +     (because it already exists, e.g. it might be the current input
> 
> The '(' on this line doesn't have a matching ')'.

yeah, should be after "exists", before ",".

> > +      /* Split up the path into the dir and base parts.  */
> > +      dirc = xstrdup (fnew);
> > +      dname = dirname (dirc);
> > +      basec = xstrdup (fnew);
> > +      bname = xbasename (basec);
> > +
> > +      /* Pin the directory.  */
> > +      dirfd = open (dname, O_RDONLY | O_DIRECTORY);
> > +      if (dirfd < 0)
> > +       {
> > +         error (0, errno, "Couldn't open output dir %s", dname);
> > +         goto cleanup;
> > +       }
> > +
> > +      /* Create a temp file inside the output dir.  This could
> > +        possibly done with O_TMPFILE and then using /proc/self/fd to
> > +        rename.  But it is not clear how portable that is.  */
> > +      size_t fnew_len = strlen (fnew);
> > +      tempname = xmalloc (fnew_len + sizeof (".XXXXXX"));
> 
> tempname is an absolute path and openat, renameat and unlinkat will ignore
> the dirfd argument when the pathname argument is absolute.  This will
> almost always work as intended but if dirfd's path changes unexpectedly
> then renameat below could fail.

Doh. After carefully splitting the (fnew) path we should obviously use
bname here.

      size_t bname_len = strlen (bname);
      tempname = xmalloc (bname_len + sizeof (".XXXXXX"));
      sprintf (tempname, "%s.XXXXXX", bname);
      fdnew = xmkstempat (dirfd, tempname);

Unfortunate the testcases didn't catch this. It is somewhat hard to
scramble the paths during the test runs.

> > +      sprintf (tempname, "%s.XXXXXX", fnew);
> > +      fdnew = xmkstempat (dirfd, tempname);
> >      }
> > 
> >    if (fdnew < 0)
> >      {
> > -      error (0, errno, "Couldn't create output file %s", fnew);
> > +      error (0, errno, "Couldn't create output file %s",
> > +            tempname == NULL ? fnew : tempname);
> >        /* Since we didn't create it we don't want to try to unlink it.  */
> > -      free (fnew);
> > -      fnew = NULL;
> > +      if (tempname != NULL)
> > +       {
> > +         free (fnew);
> > +         fnew = NULL;
> > +       }
> >        goto cleanup;
> >      }

Note this is wrong. It should be:

      /* Since we couldn't create it we don't want to try to unlink it.  */
      if (tempname != NULL)
        {
          free (tempname);
          tempname = NULL;
        }

> > @@ -1358,12 +1412,15 @@ process_file (const char *fname)
> >        error (0, errno, "Couldn't fchmod %s", fnew);
> > 
> >    /* Finally replace the old file with the new file.  */
> > -  if (foutput == NULL)
> > -    if (rename (fnew, fname) != 0)
> > -      {
> > -       error (0, errno, "Couldn't rename %s to %s", fnew, fname);
> > -       goto cleanup;
> > -      }
> > +  if (tempname != NULL)
> > +    {
> > +      fsync (fdnew); /* Flush all data and metadata before replacing file. 
> >  */
> > +      if (renameat (dirfd, tempname, dirfd, bname) != 0)
> > +       {
> > +         error (0, errno, "Couldn't rename %s to %s", tempname, fnew);
> > +         goto cleanup;
> > +       }
> > +    }

There is a typo in the error message, it should say bname instead of
fnew. And here, after renameat succeeds, we should clear tempname:

  /* Finally replace the old file with the new file.  */
  if (tempname != NULL)
    {
      fsync (fdnew); /* Flush all data and metadata before replacing file.  */
      if (renameat (dirfd, tempname, dirfd, bname) != 0)
        {
          error (0, errno, "Couldn't rename %s to %s", tempname, bname);
          goto cleanup;
        }
      /* We are finally done with the temp file, don't unlink it now.  */
      free (tempname);
      tempname = NULL;
    }

> >    /* We are finally done with the new file, don't unlink it now.  */
> >    free (fnew);
> > @@ -1377,13 +1434,18 @@ cleanup:
> >    elf_end (elfnew);
> >    close (fdnew);
> > 
> > -  if (fnew != NULL)
> > +  if (tempname != NULL)
> >      {
> > -      unlink (fnew);
> > -      free (fnew);
> 
> fnew used to always get freed here if it was non-NULL. Now it's only freed
> on success and on one error path.  The other 'goto cleanup' that aren't
> preceded by freeing fnew results in a leak.
> 
> > -      fnew = NULL;
> > +      unlinkat (dirfd, tempname, 0);

Yeah, you are right, we need to unconditionaly free (fname) under
cleanup.

> We don't ever unlink fnew, so if we created it during open and later there
> is an error, we will leave an empty or partial file around.  We could add a
> flag indicating whether we created fnew to determine whether or not to unlink
> it during error cleanup.

The "flag" is tempname. If it is set then we need to unlink tempname.
fnew is always set, but is the name/path of the output file in case we
can open it directly/exclusively. If not we split fnew (realpath) and
create tempname to write to (temporarily). There were a couple of
places where fnew was used were we should have used tempname. I have
added a comment to fnew because I obviously was confused myself.

I'll sent a V2.

Thanks,

Mark

Reply via email to