Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 34 additions & 22 deletions .github/workflows/L1-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
strategy:
fail-fast: false
matrix:
compiler: [ gcc, clang ]
coverage: [ with-coverage, without-coverage ]
compiler: [gcc, clang]
coverage: [with-coverage, without-coverage]
exclude:
- compiler: clang
coverage: with-coverage
Expand All @@ -25,7 +25,17 @@ jobs:
# If adding a RUN_TESTS cmake option, it will build with enabling optional_flags and run the L1 tests
# matrix runs both versions
build_type: ["Release", "Debug"]
extra_flags: [ "RUN_TESTS", "-DLEGACY_COMPONENTS=ON", "-DLEGACY_COMPONENTS=OFF", "-DUSE_SYSTEMD=ON", "-DUSE_SYSTEMD=OFF", "-DDOBBY_HIBERNATE_MEMCR_IMPL=ON -DDOBBY_HIBERNATE_MEMCR_PARAMS_ENABLED=OFF", "-DDOBBY_HIBERNATE_MEMCR_IMPL=ON -DDOBBY_HIBERNATE_MEMCR_PARAMS_ENABLED=ON", "-DDOBBY_HIBERNATE_MEMCR_IMPL=OFF"]
extra_flags:
[
"RUN_TESTS",
"-DLEGACY_COMPONENTS=ON",
"-DLEGACY_COMPONENTS=OFF",
"-DUSE_SYSTEMD=ON",
"-DUSE_SYSTEMD=OFF",
"-DDOBBY_HIBERNATE_MEMCR_IMPL=ON -DDOBBY_HIBERNATE_MEMCR_PARAMS_ENABLED=OFF",
"-DDOBBY_HIBERNATE_MEMCR_IMPL=ON -DDOBBY_HIBERNATE_MEMCR_PARAMS_ENABLED=ON",
"-DDOBBY_HIBERNATE_MEMCR_IMPL=OFF",
]
name: Build in ${{ matrix.build_type }} Mode (${{ matrix.extra_flags }})
steps:
- name: checkout
Expand All @@ -48,42 +58,43 @@ jobs:

- name: Install gmock
run: |
cd $GITHUB_WORKSPACE
git clone https://github.com/google/googletest.git -b release-1.11.0
cd googletest
mkdir build
cd build
cmake ..
make
sudo make install
cd $GITHUB_WORKSPACE
git clone https://github.com/google/googletest.git -b release-1.11.0
cd googletest
mkdir build
cd build
cmake ..
make
sudo make install

- name: build dobby
run: |
cd $GITHUB_WORKSPACE
mkdir build
cd build
if [ ${{ matrix.extra_flags }} = "RUN_TESTS" ]
then
cmake -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" -DRDK_PLATFORM=DEV_VM -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_DOBBYL1TEST=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ env.optional_flags }} ${{ env.optional_plugins }} ..
else
cmake -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" -DRDK_PLATFORM=DEV_VM -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.extra_flags }} ${{ env.optional_plugins }} ..
fi
make -j $(nproc)
cd $GITHUB_WORKSPACE
mkdir build
cd build
if [ ${{ matrix.extra_flags }} = "RUN_TESTS" ]
then
cmake -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" -DRDK_PLATFORM=DEV_VM -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_DOBBYL1TEST=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ env.optional_flags }} ${{ env.optional_plugins }} ..
else
cmake -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" -DRDK_PLATFORM=DEV_VM -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.extra_flags }} ${{ env.optional_plugins }} ..
fi
make -j $(nproc)

- name: run l1-tests
if: ${{ matrix.extra_flags == 'RUN_TESTS' && matrix.build_type == 'Debug' }}
run: |
sudo valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --track-fds=yes --fair-sched=try $GITHUB_WORKSPACE/build/tests/L1_testing/tests/DobbyTest/DobbyL1Test --gtest_output="json:$(pwd)/DobbyL1TestResults.json"
sudo $GITHUB_WORKSPACE/build/tests/L1_testing/tests/DobbyUtilsTest/DobbyUtilsL1Test --gtest_output="json:$(pwd)/DobbyUtilsL1TestResults.json"
sudo valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --track-fds=yes --fair-sched=try $GITHUB_WORKSPACE/build/tests/L1_testing/tests/DobbyManagerTest/DobbyManagerL1Test --gtest_output="json:$(pwd)/DobbyManagerL1TestResults.json"
sudo valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --track-fds=yes --fair-sched=try $GITHUB_WORKSPACE/build/tests/L1_testing/tests/DobbySpecConfigTest/DobbySpecConfigL1Test --gtest_output="json:$(pwd)/DobbySpecConfigL1TestResults.json"

- name: Generate coverage
if: ${{ matrix.coverage == 'with-coverage' && matrix.extra_flags == 'RUN_TESTS' && matrix.build_type == 'Debug' }}
run: >
lcov
--rc geninfo_unexecuted_blocks=1
--ignore-errors source
--ignore-errors mismatch
--ignore-errors mismatch
-c
-o coverage.info
-d $GITHUB_WORKSPACE
Expand All @@ -108,5 +119,6 @@ jobs:
DobbyL1TestResults.json
DobbyUtilsL1TestResults.json
DobbyManagerL1TestResults.json
DobbySpecConfigL1TestResults.json
coverage
if-no-files-found: warn
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,51 @@ Usage: DobbyBundleGenerator <option(s)>
-o, --outputDirectory=PATH Where to save the generated OCI bundle
```

## Dobby Spec Format
When using `DobbyDaemon` or `DobbyBundleGenerator`, containers are described using a Dobby-specific JSON spec file. Example specs can be found in `tests/L2_testing/dobby_specs/`.

The table below lists the supported top-level fields. Fields marked **mandatory** must always be present.

| Field | Type | Mandatory | Description |
|-------|------|-----------|-------------|
| `version` | string | Yes | Spec version. Currently `"1.0"` or `"1.1"`. |
| `args` | array | Yes | Command and arguments to run inside the container. |
Comment on lines +133 to +136
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown table is using || at the start of each row, which introduces an unintended empty first column and renders oddly on GitHub. Use a single leading | for the header/separator/rows to format the table correctly.

Copilot uses AI. Check for mistakes.
| `user` | object | Yes | `uid` and `gid` the container process runs as. |
| `memLimit` | integer | Yes | Memory limit in bytes (`memory.limit_in_bytes`). Must be ≥ 256 KiB. |
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README says memLimit “Must be ≥ 256 KiB”, but the implementation only logs a warning when it’s below 256 KiB and still accepts the value (see processMemLimit in bundle/lib/source/DobbySpecConfig.cpp). Either update the documentation to reflect the warning-only behavior, or enforce the minimum in code so the doc is accurate.

Suggested change
| `memLimit` | integer | Yes | Memory limit in bytes (`memory.limit_in_bytes`). Must be ≥ 256 KiB. |
| `memLimit` | integer | Yes | Memory limit in bytes (`memory.limit_in_bytes`). Values below 256 KiB are accepted but only generate a warning and may not be effective. |

Copilot uses AI. Check for mistakes.
| `swapLimit` | integer | No | Swap+memory limit in bytes (`memory.memsw.limit_in_bytes`). Must be ≥ `memLimit`. Defaults to `memLimit` (no extra swap). |
| `env` | array | No | Environment variables in `"KEY=VALUE"` format. |
| `cwd` | string | No | Working directory inside the container. |
| `console` | object | No | Console log settings: `path` and `limit` (bytes). |
| `etc` | object | No | Inline `/etc` file content (`passwd`, `group`, `hosts`, `services`, `ld.so.preload`). |
| `network` | string | No | Network mode: `"nat"`, `"open"`, or `"private"`. Defaults to `"private"`. |
| `mounts` | array | No | Additional bind-mounts into the container. |
| `cpu` | object | No | CPU cgroup settings: `shares` (percentage 1–100) and `cores` (bitmask string). |
| `rtPriority` | object | No | Real-time scheduling priority settings. |
| `userNs` | boolean | No | Enable user namespacing. Defaults to `true`. |
| `gpu` | object | No | GPU device node access settings. |
| `vpu` | object | No | VPU device node access settings. |
| `devices` | array | No | Additional device nodes to whitelist. |
| `capabilities` | array | No | Linux capabilities to grant the container. |
| `seccomp` | object | No | Seccomp syscall filter profile. |
| `syslog` | object | No | Syslog plugin configuration. |
| `dbus` | object | No | D-Bus access configuration. |
| `restartOnCrash` | boolean | No | Restart the container automatically if it crashes. |
| `plugins` | object | No | Legacy plugin configuration (prefer `rdkPlugins`). |

### Memory configuration example

```json
{
"version": "1.0",
"args": [ "/usr/bin/myapp" ],
"user": { "uid": 1000, "gid": 1000 },
"memLimit": 67108864,
"swapLimit": 134217728
}
```

`swapLimit` sets the combined memory+swap ceiling enforced by the kernel cgroup (`memory.memsw.limit_in_bytes`). When omitted, swap is capped at the same value as `memLimit`, effectively disabling extra swap for the container.

## DobbyTool
This is a simple command line tool that is used for debugging purporses. It connects to the Dobby daemon over dbus and allows for debugging and testing containers.

Expand Down
1 change: 1 addition & 0 deletions bundle/lib/include/DobbySpecConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class DobbySpecConfig : public DobbyConfig
JSON_FIELD_PROCESSOR(processMounts);
JSON_FIELD_PROCESSOR(processLegacyPlugins);
JSON_FIELD_PROCESSOR(processMemLimit);
JSON_FIELD_PROCESSOR(processSwapLimit);
JSON_FIELD_PROCESSOR(processGpu);
JSON_FIELD_PROCESSOR(processVpu);
JSON_FIELD_PROCESSOR(processDbus);
Expand Down
64 changes: 63 additions & 1 deletion bundle/lib/source/DobbySpecConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ static const ctemplate::StaticTemplateString USERNS_DISABLED =

static const ctemplate::StaticTemplateString MEM_LIMIT =
STS_INIT(MEM_LIMIT, "MEM_LIMIT");
static const ctemplate::StaticTemplateString MEM_SWAP =
STS_INIT(MEM_SWAP, "MEM_SWAP");

static const ctemplate::StaticTemplateString CPU_SHARES_ENABLED =
STS_INIT(CPU_SHARES_ENABLED, "CPU_SHARES_ENABLED");
Expand Down Expand Up @@ -187,6 +189,7 @@ static const ctemplate::StaticTemplateString SECCOMP_SYSCALLS =
#define JSON_FLAG_FILECAPABILITIES (0x1U << 20)
#define JSON_FLAG_VPU (0x1U << 21)
#define JSON_FLAG_SECCOMP (0x1U << 22)
#define JSON_FLAG_SWAPLIMIT (0x1U << 23)

int DobbySpecConfig::mNumCores = -1;

Expand Down Expand Up @@ -504,7 +507,8 @@ bool DobbySpecConfig::parseSpec(ctemplate::TemplateDictionary* dictionary,
{ "cpu", { JSON_FLAG_CPU, &DobbySpecConfig::processCpu } },
{ "devices", { JSON_FLAG_DEVICES, &DobbySpecConfig::processDevices } },
{ "capabilities", { JSON_FLAG_CAPABILITIES, &DobbySpecConfig::processCapabilities } },
{ "seccomp", { JSON_FLAG_SECCOMP, &DobbySpecConfig::processSeccomp } }
{ "seccomp", { JSON_FLAG_SECCOMP, &DobbySpecConfig::processSeccomp } },
{ "swapLimit", { JSON_FLAG_SWAPLIMIT, &DobbySpecConfig::processSwapLimit } }
};

// step 1 - parse the 'dobby' spec document
Expand Down Expand Up @@ -627,6 +631,16 @@ bool DobbySpecConfig::parseSpec(ctemplate::TemplateDictionary* dictionary,
dictionary->SetIntValue(RLIMIT_RTPRIO, 0);
}

if (!(flags & JSON_FLAG_SWAPLIMIT))
{
// swapLimit not supplied: default swap to memLimit (no extra swap)
const Json::Value& memLimitVal = mSpec["memLimit"];
if (memLimitVal.isIntegral())
{
dictionary->SetIntValue(MEM_SWAP, memLimitVal.asUInt());
}
}
Comment on lines +634 to +642
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The swapLimit defaulting logic is keyed off the processed flags. If the JSON contains swapLimit but its processor returns false (e.g. non-integral or < memLimit), the flag never gets set and this block will still populate MEM_SWAP from memLimit, producing a seemingly-valid config.json despite a rejected swapLimit. Prefer basing the default on JSON presence (e.g. !mSpec.isMember("swapLimit")) and/or only applying this default when parsing is still successful, so invalid swapLimit values don’t get silently replaced by the default in the generated dictionary.

Copilot uses AI. Check for mistakes.

if (!(flags & JSON_FLAG_CAPABILITIES))
{
dictionary->SetValue(NO_NEW_PRIVS, "true");
Expand Down Expand Up @@ -1278,6 +1292,54 @@ bool DobbySpecConfig::processMemLimit(const Json::Value& value,
return true;
}

// -----------------------------------------------------------------------------
/**
* @brief Processes the optional swap limit field.
*
* When present, this value is used as the cgroup memory.memsw.limit_in_bytes,
* allowing swap to be configured independently of the memory limit. When
* absent the swap limit defaults to the same value as memLimit (i.e. no
* extra swap beyond the memory limit).
*
* The kernel requires swap >= memLimit, so an error is returned if the
* supplied value is smaller than the memLimit already set.
*
* Example json:
*
* "swapLimit": 2097152
*
*
*
* @param[in] value The json spec document from the client
* @param[in] dictionary Pointer to the OCI dictionary to populate
*
* @return true if correctly processed the value, otherwise false.
*/
bool DobbySpecConfig::processSwapLimit(const Json::Value& value,
ctemplate::TemplateDictionary* dictionary)
{
if (!value.isIntegral())
{
AI_LOG_ERROR("invalid swapLimit field");
return false;
}

unsigned memSwap = value.asUInt();

// the kernel requires memory.memsw.limit_in_bytes >= memory.limit_in_bytes
const Json::Value& memLimitVal = mSpec["memLimit"];
if (memLimitVal.isIntegral() && (memSwap < memLimitVal.asUInt()))
{
AI_LOG_ERROR("swapLimit (%u) must be >= memLimit (%u)",
memSwap, memLimitVal.asUInt());
return false;
}

Comment on lines +1327 to +1337
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processSwapLimit accepts any integral JSON value and then uses asUInt(). With JsonCpp, a negative integer is still “integral” and will wrap to a huge unsigned value, bypassing the swap >= memLimit check and setting an unintended cgroup limit. Reject negative values explicitly (e.g., require value.isUInt() / memLimitVal.isUInt()), or validate value.asInt64() >= 0 before converting.

Suggested change
unsigned memSwap = value.asUInt();
// the kernel requires memory.memsw.limit_in_bytes >= memory.limit_in_bytes
const Json::Value& memLimitVal = mSpec["memLimit"];
if (memLimitVal.isIntegral() && (memSwap < memLimitVal.asUInt()))
{
AI_LOG_ERROR("swapLimit (%u) must be >= memLimit (%u)",
memSwap, memLimitVal.asUInt());
return false;
}
// Validate that swapLimit is not negative before converting to unsigned
const int64_t memSwapSigned = value.asInt64();
if (memSwapSigned < 0)
{
AI_LOG_ERROR("swapLimit must be non-negative");
return false;
}
// the kernel requires memory.memsw.limit_in_bytes >= memory.limit_in_bytes
const Json::Value& memLimitVal = mSpec["memLimit"];
if (memLimitVal.isIntegral())
{
const int64_t memLimitSigned = memLimitVal.asInt64();
if (memLimitSigned < 0)
{
AI_LOG_ERROR("memLimit must be non-negative when swapLimit is specified");
return false;
}
if (memSwapSigned < memLimitSigned)
{
AI_LOG_ERROR("swapLimit (%lld) must be >= memLimit (%lld)",
static_cast<long long>(memSwapSigned),
static_cast<long long>(memLimitSigned));
return false;
}
}
unsigned memSwap = static_cast<unsigned>(memSwapSigned);

Copilot uses AI. Check for mistakes.
dictionary->SetIntValue(MEM_SWAP, memSwap);

return true;
}

// -----------------------------------------------------------------------------
/**
* @brief Adds the GPU device nodes (if any) to supplied dictionary.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ static const char* ociJsonTemplate = R"JSON(
],
"memory": {
"limit": {{MEM_LIMIT}},
"swap": {{MEM_LIMIT}},
"swap": {{MEM_SWAP}},
"swappiness": 60
},
"cpu": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ static const char* ociJsonTemplate = R"JSON(
],
"memory": {
"limit": {{MEM_LIMIT}},
"swap": {{MEM_LIMIT}},
"swap": {{MEM_SWAP}},
"swappiness": 60
},
"cpu": {
Expand Down
4 changes: 4 additions & 0 deletions tests/L1_testing/mocks/DobbyBundle.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class DobbyBundleImpl {
virtual ~DobbyBundleImpl() = default;

virtual void setPersistence(bool persist) = 0;
virtual bool getPersistence() const = 0;
virtual int dirFd() const = 0;
virtual bool isValid() const = 0;
virtual const std::string& path() const = 0;

Expand Down Expand Up @@ -58,6 +60,8 @@ class DobbyBundle {

static void setImpl(DobbyBundleImpl* newImpl);
void setPersistence(bool persist);
bool getPersistence() const;
int dirFd() const;
bool isValid() const;
};

Expand Down
11 changes: 11 additions & 0 deletions tests/L1_testing/mocks/DobbyBundleMock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ const std::string& DobbyBundle::path() const
return impl->path();
}

bool DobbyBundle::getPersistence() const
{
EXPECT_NE(impl, nullptr);
return impl->getPersistence();
}

int DobbyBundle::dirFd() const
{
EXPECT_NE(impl, nullptr);
return impl->dirFd();
}
2 changes: 2 additions & 0 deletions tests/L1_testing/mocks/DobbyBundleMock.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class DobbyBundleMock : public DobbyBundleImpl {
virtual ~DobbyBundleMock() = default;

MOCK_METHOD(void, setPersistence, (bool persist), (override));
MOCK_METHOD(bool, getPersistence, (), (const,override));
MOCK_METHOD(int, dirFd, (), (const,override));
MOCK_METHOD(bool, isValid, (), (const,override));
MOCK_METHOD((const std::string&), path, (), (const,override));
};
4 changes: 3 additions & 1 deletion tests/L1_testing/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@

add_subdirectory(DobbyUtilsTest)
add_subdirectory(DobbyTest)
add_subdirectory(DobbyManagerTest)
add_subdirectory(DobbyManagerTest)
add_subdirectory(DobbySpecConfigTest)

Loading
Loading