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:
- The bug is strongly correlated with the
-p
/-P
flags. - The function of these flags is to generate a
PKGBUILD
. - 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.
$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
.$pkgname
: This variable is extracted from the temporarily generatedPKGBUILD
file. According to my logs, the converted package name wascom.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:
dirname "$package_with_full_path"
is still/home/myusername/Downloads/tmp
.- 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.