The story begins, as many do, quite mundanely. I needed to install a piece of software called SwashbucklerDiary, which was only officially available as a .deb package. For an Arch Linux user, this is hardly a problem; the debtap utility was made for exactly this scenario.

As is my usual practice, I created a temporary directory to handle the conversion, ensuring I wouldn’t clutter my main Downloads folder.

> pwd
/home/myusername/Downloads/tmp

> ls
SwashbucklerDiary-1.17.0-linux-x64.deb

Everything looked normal. The directory contained only the .deb file I had just downloaded. I first ran the standard debtap command, and it successfully generated the pkg.tar.zst package I needed.

> debtap SwashbucklerDiary-1.17.0-linux-x64.deb
... (conversion process output omitted) ...
==> Package successfully created!
==> Removing leftover files...

> ls -alh
total 106M
drwxr-xr-x 2 myusername myusername 116 Aug  6 12:34 .
drwxr-xr-x 4 myusername myusername  41 Aug  6 12:34 ..
-rw-r--r-- 1 myusername root        58M Aug  6 12:34 com.yucore.swashbucklerdiary-1.17.0-1-x86_64.pkg.tar.zst
-rw-r--r-- 1 myusername myusername 49M Aug  4 18:14 SwashbucklerDiary-1.17.0-linux-x64.deb

Perfect. The converted package and the original .deb file were both present. But then, driven by curiosity and a desire to learn, I decided I wanted to see what the PKGBUILD file generated by debtap looked like. The tool provides the -p or -P flag for this purpose. So, I deleted the newly created package and ran the command again, this time with the -p flag.

> debtap -p SwashbucklerDiary-1.17.0-linux-x64.deb
... (same interactive prompts) ...
==> Package successfully created!
==> Generating PKGBUILD file...
mv: cannot stat 'PKGBUILD': No such file or directory
==> PKGBUILD is now located in "/home/myusername/Downloads/tmp" and ready to be edited
==> Removing leftover files...

The output seemed a bit odd. There was an error message: mv: cannot stat 'PKGBUILD': No such file or directory. But the final line still confidently informed me that the PKGBUILD had been generated in the current directory. I didn’t think much of it and habitually typed ls -alh.

Then, I saw something that sent a chill down my spine.

> ls -alh
total 0

Completely empty.

My first reaction was shock. Not only was the expected PKGBUILD directory missing, but the original .deb file had also vanished! The entire tmp directory had been wiped clean.

Stranger things were yet to come. I tried to cd .. and then cd tmp back into the directory. My shell prompt (I use Oh My Zsh with Powerlevel10k) displayed some bizarre artifacts, as if the directory’s metadata itself had been corrupted. When I ran ls -alh again, I was greeted with an even more bewildering output:

> ls -alh
total 0
drwxr-xr-x 2 myusername myusername  6 Aug  6 12:35 .
drwxr-xr-x 4 myusername myusername 41 Aug  6 12:35 ..

Look at the size of the . directory: 6 bytes. A normal, freshly emptied directory on my XFS filesystem should be 4096 bytes. This was highly unusual.

My mind started racing. My /home directory is on a RAID0 array of two SSDs, formatted with XFS. My first thought was, “It’s over. Did my RAID0 array just fail? Did one of the drives die?” The high performance of RAID0 comes at the cost of zero redundancy; the failure of a single drive means the loss of all data on the array. I immediately started checking dmesg and system logs, but I found no signs of I/O errors or filesystem corruption.

After ruling out hardware and filesystem issues, I calmed down and started to suspect debtap itself. Since the first run without -p was normal, and the second run with -p caused the disaster, the problem had to be linked to that specific flag.

I decided to reproduce the issue, but this time in a completely safe environment. I created a new test directory, populated it with a few harmless dummy files, and a copy of the .deb package.

mkdir ~/safe-test
cd ~/safe-test
touch fileA.txt fileB.log
cp ~/Downloads/SwashbucklerDiary-1.17.0-linux-x64.deb .
ls -l
# total 49264
# -rw-r--r-- 1 myusername myusername 50442386 Aug  4 18:14 SwashbucklerDiary-1.17.0-linux-x64.deb
# -rw-r--r-- 1 myusername myusername        0 Aug  6 13:00 fileA.txt
# -rw-r--r-- 1 myusername myusername        0 Aug  6 13:00 fileB.log

Then, holding my breath, I executed the “demonic” command once more:

debtap -p SwashbucklerDiary-1.17.0-linux-x64.deb

After the process finished, I ran ls.

ls -l
# total 0

The result was identical. The directory was wiped clean.

At this point, the case was clear. This was no paranormal event or hardware failure. It was an extremely dangerous bug in debtap that, when used with the -p or -P flag, would delete all files in the current working directory.

With the problem identified, the next step was to find the cause. debtap is a shell script, which makes source code analysis very straightforward. I opened /usr/bin/debtap, version 3.6.2. It was a massive script, over three thousand lines long, so a full read-through was out of the question.

My investigation had a clear focus:

  1. The bug is strongly correlated with the -p/-P flags.
  2. The function of these flags is to generate a PKGBUILD.
  3. The final result is the deletion of the current directory.

Therefore, I needed to find the code block in the script that handled the -p/-P flags and was responsible for generating and moving the PKGBUILD file. I searched the code for the keyword pkgbuild.

Near the end of the script, I quickly found the logic for handling the PKGBUILD creation.

# ... (code for generating PKGBUILD content) ...

# Moving PKGBUILD (and .INSTALL, if it exists) and announcing its creation
pkgname="$(grep '^pkgname=' PKGBUILD | sed s'/^pkgname=//')"
if [[ $output == set ]]; then
    pkgbuild_location="$(dirname "$outputdirectory/$pkgname-PKGBUILD")"
    rm -rf "$pkgbuild_location" 2> /dev/null
    mkdir "$pkgbuild_location" 2> /dev/null
    # ... (error handling and file moving code) ...
else
    pkgbuild_location="$(dirname ""$(dirname "$package_with_full_path")"/$pkgname-PKGBUILD")"
    rm -rf "$pkgbuild_location" 2> /dev/null
    mkdir "$pkgbuild_location" 2> /dev/null
    # ... (error handling and file moving code) ...
fi

My eyes were immediately drawn to the line rm -rf "$pkgbuild_location". This was, without a doubt, the prime suspect. The script was executing a forced, recursive delete command. The question now was: what was the actual value of the $pkgbuild_location variable?

Let’s focus on the key line in the else block, which is where execution goes since I didn’t use the -o output directory option:

pkgbuild_location="$(dirname ""$(dirname "$package_with_full_path")"/$pkgname-PKGBUILD")"

This line looks a bit complex, with two nested dirname commands. Let’s dissect it and analyze its execution step by step.

dirname is a basic shell command that strips the filename from a path, returning only the directory path. For example:

  • dirname /usr/bin/ls returns /usr/bin
  • dirname /home/user/file.txt returns /home/user

Now, let’s substitute the actual variable values from my session.

  1. $package_with_full_path: This variable is defined at the beginning of the script as the absolute path to the input .deb file. In my case, its value was /home/myusername/Downloads/tmp/SwashbucklerDiary-1.17.0-linux-x64.deb.
  2. $pkgname: This variable is extracted from the temporarily generated PKGBUILD file. According to my logs, the converted package name was com.yucore.swashbucklerdiary-1.17.0-1.

Now, let’s trace the nested command:

Step 1: Execute the inner dirname

"$(dirname "$package_with_full_path")"
# Becomes:
"$(dirname "/home/myusername/Downloads/tmp/SwashbucklerDiary-1.17.0-linux-x64.deb")"

The output of this step is the directory containing the .deb file: /home/myusername/Downloads/tmp.

Step 2: Concatenate the string

The result from the previous step is then concatenated with the rest of the string, forming a longer path:

"/home/myusername/Downloads/tmp/$pkgname-PKGBUILD"
# Substituting $pkgname:
"/home/myusername/Downloads/tmp/com.yucore.swashbucklerdiary-1.17.0-1-PKGBUILD"

This string represents a path… wait, this looks like a file path, not a directory. The author’s intent was likely to create a directory named packagename-PKGBUILD to place the PKGBUILD file into.

Step 3: Execute the outer dirname

Now for the most critical step. The script takes the entire string generated in Step 2 and runs the outer dirname on it:

"$(dirname "/home/myusername/Downloads/tmp/com.yucore.swashbucklerdiary-1.17.0-1-PKGBUILD")"

And what is the output of this command? It is /home/myusername/Downloads/tmp!

The Truth is Revealed

After these three steps, we have the final value of the pkgbuild_location variable: /home/myusername/Downloads/tmp, which was the current working directory where I ran the debtap command.

Now let’s look back at those fatal lines of code:

pkgbuild_location="/home/myusername/Downloads/tmp"
rm -rf "$pkgbuild_location"  # Effectively becomes: rm -rf "/home/myusername/Downloads/tmp"
mkdir "$pkgbuild_location" # Effectively becomes: mkdir "/home/myusername/Downloads/tmp"

The mystery was solved. The script first calculated the wrong path—the current working directory—and then, without hesitation, executed rm -rf, deleting the directory and everything inside it (including my original .deb file). Immediately after, the mkdir command recreated the directory, which is why I was left with an empty tmp directory whose metadata appeared to have been “initialized.”

This was a classic yet terrifying logical error. The author likely intended to ensure the target directory was clean by deleting and recreating it. However, the incorrect use of a double dirname caused the deletion target to shift from the “intended subdirectory” to the “entire current directory.”

After discovering the root cause, a new thought occurred to me: a bug this severe couldn’t have been in debtap for long, or it would have been discovered ages ago. It must have been introduced recently.

I decided to do some “code archeology” in the debtap GitHub repository to uncover the bug’s history. Using git blame and browsing the commit history, I quickly zeroed in on a suspicious commit: commit 27a9ff5.

The commit message was a simple code update. Let’s look at its diff:

diff --git a/debtap b/debtap
index 4518a7a..71aea20 100755
--- a/debtap
+++ b/debtap
@@ -3458,8 +3458,8 @@ if [[ $output == set ]]; then
    fi
 else
    pkgbuild_location="$(dirname ""$(dirname "$package_with_full_path")"/$pkgname-PKGBUILD")"
-   rm -rf "$pkgbuilt_location" 2> /dev/null
-   mkdir "$pkgbuilt_location" 2> /dev/null
+   rm -rf "$pkgbuild_location" 2> /dev/null
+   mkdir "$pkgbuild_location" 2> /dev/null
    if [[ $? != 0 ]]; then
        echo -e "${red}Error: Cannot create PKGBUILD directory to the same directory as .deb package, permission denied. Removing leftover files and exiting...${NC}"
        rm -rf "$working_directory"

Seeing this, it all clicked, and I didn’t know whether to laugh or cry.

Before this commit, the code was:

rm -rf "$pkgbuilt_location" 2> /dev/null
mkdir "$pkgbuilt_location" 2> /dev/null

Notice the variable name: pkgbuilt_location. But the variable defined above was named pkgbuild_location. It was a * typo*!

In shell scripting, referencing a non-existent variable (due to a typo, for instance) causes it to expand to an empty string. Therefore, before commit 27a9ff5, the commands being executed were effectively:

rm -rf "" 2> /dev/null
mkdir "" 2> /dev/null

rm -rf "" and mkdir "" do nothing and produce no errors. Thus, although the flawed dirname logic was calculating the wrong path, this typo prevented it from ever being used in the rm -rf command. The typo acted like a safety fuse, unintentionally protecting countless users’ data by a strange twist of fate.

The author of commit 27a9ff5 likely spotted this typo during a code review and, with the good intention of “fixing the code,” changed pkgbuilt_location to the correct pkgbuild_location. He “fixed” the typo, but in doing so, he unwittingly armed the bomb.

It’s a textbook case of how a seemingly trivial, well-intentioned change can lead to catastrophic consequences if the full context and potential impact are not understood.

Having unraveled the entire story, I knew I had to report this to the project maintainer immediately to prevent more users from falling victim. I quickly created a new issue on the debtap GitHub repository.

The issue got a swift response from the community. Other users confirmed they had encountered the same problem, with one user expressing relief that they hadn’t run the command in their $HOME directory—a comment that further underscored the bug’s severity.

The project maintainer, helixarch, took notice quickly and released a fix a few days later. Let’s look at the core diff that fixed the bug:

--- a/debtap
+++ b/debtap
@@ -3486,7 +3486,7 @@ if [[ $output == set ]]; then
        echo -e "${lightgreen}==>${NC} ${bold}PKGBUILD is now located in${normal} ${lightblue}\"$pkgbuild_location\"${NC} ${bold}and ready to be edited${normal}"
    fi
 else
-   pkgbuild_location="$(dirname ""$(dirname "$package_with_full_path")"/$pkgname-PKGBUILD")"
+   pkgbuild_location=""$(dirname "$package_with_full_path")"/$pkgname-PKGBUILD"
    rm -rf "$pkgbuild_location" 2> /dev/null
    mkdir "$pkgbuild_location" 2> /dev/null
    if [[ $? != 0 ]]; then

The fix was direct and elegant. The maintainer removed the outer dirname.

Now, the calculation for pkgbuild_location became:

pkgbuild_location=""$(dirname "$package_with_full_path")"/$pkgname-PKGBUILD"

Let’s trace this new logic:

  1. dirname "$package_with_full_path" is still /home/myusername/Downloads/tmp.
  2. The concatenated string is /home/myusername/Downloads/tmp/com.yucore.swashbucklerdiary-1.17.0-1-PKGBUILD.

This value is now directly assigned to pkgbuild_location. Consequently, the subsequent commands become:

rm -rf "/home/myusername/Downloads/tmp/com.yucore.swashbucklerdiary-1.17.0-1-PKGBUILD"
mkdir "/home/myusername/Downloads/tmp/com.yucore.swashbucklerdiary-1.17.0-1-PKGBUILD"

This is exactly the behavior we want! The script now correctly creates a new, clean subdirectory within the current directory to store the PKGBUILD file, without posing any threat to the current directory itself.

With the release of debtap 3.6.3, this heart-stopping bug was finally squashed.