Skip to content

JGit SubmoduleDeinitCommand can delete files from a dirty requested submodule #293

Description

@N0zoM1z0

Version

v7.8.0.202609011348-r-10-g87ec21000

Operating System

Linux/Unix

Bug description

SubmoduleDeinitCommand can deinitialize a dirty requested submodule without
force when another earlier submodule entry records the same object ID.

The command is expected to check the requested path and refuse to remove its
worktree when it contains local changes. In the reproducing repository,
submodules a and b both point to the same commit. a is clean and b
contains an untracked file. The call below requests only b and does not call
setForce(true):

new SubmoduleDeinitCommand(db).addPath("b").call();

call() initially walks the requested path, but checkDirty() then opens a
second SubmoduleWalk.forIndex(repo) without a path filter and examines only
the first index entry. Because a appears before b and has the same object
ID, the status of clean submodule a is used for the requested path b.
The command consequently proceeds to deinit("b"), which recursively deletes
the contents of b.

Relevant source at the tested revision:

Actual behavior

The requested dirty submodule is reported as successfully deinitialized, and
its untracked file is deleted even though force was not requested:

OBSERVED deinit requested=b status=SUCCESS dirty_file_exists=false

The control case, with only submodule b present, correctly detects the same
dirty file:

CONTROL deinit only=b status=DIRTY dirty_file_exists=true

Expected behavior

Because b contains local modifications and force was not requested, the
command should return SubmoduleDeinitStatus.DIRTY for b and leave all files
under b intact. This is also the behavior documented by setForce(false).

Relevant log output

OBSERVED deinit requested=b status=SUCCESS dirty_file_exists=false
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

CONTROL deinit only=b status=DIRTY dirty_file_exists=true
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Other information

The standalone reproducer is included in the accompanying archive:

  • reproduce.sh copies reproduce_test.java into
    org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule, runs the two tests,
    records the environment and revision, and removes the temporary test source
    and generated test artifacts.
  • The test source is
    org.eclipse.jgit.submodule.SubmoduleDeinitReproductionTest.
  • Run it from the repository root with:
findings/submodules/jgit-deinit-unfiltered-dirty-check/reproduce.sh \
  findings/submodules/jgit-deinit-unfiltered-dirty-check/evidence/20260909

The effective Maven command uses -Pjavac -pl org.eclipse.jgit.test -am and
the following environment was used:

Git 2.39.5
Java openjdk version "17.0.20.1" 2026-08-18
Maven 3.8.7

The issue causes local data loss: uncommitted, untracked, generated, or other
files in the requested submodule can be removed by a non-forced deinit. The
demonstration requires two submodule entries with the same recorded object ID,
an initialized dirty target submodule, and an application or user invoking
deinit for that target. It does not demonstrate deletion outside the requested
submodule, code execution, privilege escalation, or a remote-only trigger.

The complete reproduction output is in
evidence/20260909/run.log. The implementation contract is documented by
setForce(),
which states that deinitialization should refuse local modifications unless
force is enabled.

Attachments

jgit-deinit-unfiltered-dirty-check.zip

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions