Skip to content

Change in deployment fails due to corrupt JSON in container createOptions due to a fault JSON-merge #7489

@ortogonal

Description

@ortogonal

Expected Behavior

When changing from deployment A to B the edgeAgent try and merge (on JSON-level) the old deployment with the new. The expected behavior is that this merge shall succeed so edgeAgent can configure the modules correctly.

Current Behavior

The JSON-merge fails in special cases when going from a long createOptions to a shorter one.

Steps to Reproduce

Provide a detailed set of steps to reproduce the bug.

  1. Create a deployment A with a module with a really long createOptions. It must be long enough so it's divided into createOptions and createOptions01.
  2. Install it on a device.
  3. Create a deployment B with the same module as in A but make the createOptions shorter so it only get a createOptions and not a createOptions01.
  4. Change the deployment from A to B on the device.
  5. edgeAgent will halt the update and complain about broken JSON data.

Context (Environment)

Output of iotedge check

Click here

Configuration checks (aziot-identity-service)
---------------------------------------------
√ keyd configuration is well-formed - OK
√ certd configuration is well-formed - OK
√ tpmd configuration is well-formed - OK
√ identityd configuration is well-formed - OK
√ daemon configurations up-to-date with config.toml - OK
√ identityd config toml file specifies a valid hostname - OK
‼ aziot-identity-service package is up-to-date - Warning
    Installed aziot-identity-service package has version 1.4.3 but 1.4.9 is the latest stable version available.
    Please see https://aka.ms/aziot-update-runtime for update instructions.
√ host time is close to reference time - OK
√ preloaded certificates are valid - OK
√ keyd is running - OK
√ certd is running - OK
√ identityd is running - OK
√ read all preloaded certificates from the Certificates Service - OK
√ read all preloaded key pairs from the Keys Service - OK
√ check all EST server URLs utilize HTTPS - OK
√ ensure all preloaded certificates match preloaded private keys with the same ID - OK

Connectivity checks (aziot-identity-service)
--------------------------------------------
√ host can connect to and perform TLS handshake with iothub AMQP port - OK
√ host can connect to and perform TLS handshake with iothub HTTPS / WebSockets port - OK
√ host can connect to and perform TLS handshake with iothub MQTT port - OK

Configuration checks
--------------------
√ aziot-edged configuration is well-formed - OK
√ configuration up-to-date with config.toml - OK
√ container engine is installed and functional - OK
√ configuration has correct URIs for daemon mgmt endpoint - OK
‼ aziot-edge package is up-to-date - Warning
    Installed IoT Edge daemon has version 1.4.9 but 1.4.41 is the latest stable version available.
    Please see https://aka.ms/iotedge-update-runtime for update instructions.
√ container time is close to host time - OK
√ DNS server - OK
√ production readiness: logs policy - OK
√ production readiness: Edge Agent's storage directory is persisted on the host filesystem - OK
√ production readiness: Edge Hub's storage directory is persisted on the host filesystem - OK
√ Agent image is valid and can be pulled from upstream - OK
√ proxy settings are consistent in aziot-edged, aziot-identityd, moby daemon and config.toml - OK

Connectivity checks
-------------------
√ container on the default network can connect to upstream AMQP port - OK
√ container on the default network can connect to upstream HTTPS / WebSockets port - OK
√ container on the IoT Edge module network can connect to upstream AMQP port - OK
√ container on the IoT Edge module network can connect to upstream HTTPS / WebSockets port - OK
33 check(s) succeeded.
2 check(s) raised warnings. Re-run with --verbose for more details.
2 check(s) were skipped due to errors from other checks. Re-run with --verbose for more details.


Device Information

  • Host OS [e.g. Ubuntu 22.04, Windows Server IoT 2019]: Custom Yocto dist
  • Architecture [e.g. amd64, arm32, arm64]: arm64
  • Container OS [e.g. Linux containers, Windows containers]: Linux containers

Runtime Versions

  • aziot-edged [run iotedge version]: 1.4.9
  • Edge Agent [image tag (e.g. 1.0.0)]: 1.5.31
  • Edge Hub [image tag (e.g. 1.0.0)]: 1.5.31
  • Docker/Moby [run docker version]: 20.10.25-ce

Logs

aziot-edged logs

<Paste here between the triple backticks>

edge-agent logs

<4> 2025-12-04 15:23:40.269 +00:00 [WRN] - Reconcile failed because of invalid configuration format
Microsoft.Azure.Devices.Edge.Agent.Core.ConfigSources.ConfigFormatException: Agent configuration format is invalid.
 ---> Newtonsoft.Json.JsonReaderException: Error parsing comment. Expected: *, got d. Path '', line 1, position 416.
   at Newtonsoft.Json.JsonTextReader.ParseComment(Boolean setToken)
   at Newtonsoft.Json.JsonTextReader.Read()
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Microsoft.Azure.Devices.Edge.Agent.Docker.DockerConfig.GetCreateOptions(String createOptions) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Docker/DockerConfig.cs:line 82
   at Microsoft.Azure.Devices.Edge.Agent.Docker.DockerConfig..ctor(String image, String createOptions, Option`1 digest) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Docker/DockerConfig.cs:line 28
   at Microsoft.Azure.Devices.Edge.Agent.Docker.DockerConfig.DockerConfigJsonConverter.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Docker/DockerConfig.cs:line 213
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolvePropertyAndCreatorValues(JsonObjectContract contract, JsonProperty containerProperty, JsonReader reader, Type objectType)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(JsonReader reader, JsonObjectContract contract, JsonProperty containerProperty, ObjectConstructor`1 creator, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type)
   at Microsoft.Azure.Devices.Edge.Agent.Core.Serde.TypeSpecificSerDe`1.TypeSpecificJsonConverter.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/serde/TypeSpecificSerDe.cs:line 104
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolvePropertyAndCreatorValues(JsonObjectContract contract, JsonProperty containerProperty, JsonReader reader, Type objectType)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(JsonReader reader, JsonObjectContract contract, JsonProperty containerProperty, ObjectConstructor`1 creator, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Microsoft.Azure.Devices.Edge.Agent.Core.Serde.TypeSpecificSerDe`1.Deserialize[TU](String json) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/serde/TypeSpecificSerDe.cs:line 63
   at Microsoft.Azure.Devices.Edge.Agent.Core.Serde.TypeSpecificSerDe`1.Deserialize(String json) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/serde/TypeSpecificSerDe.cs:line 56
   at Microsoft.Azure.Devices.Edge.Agent.IoTHub.EdgeAgentConnection.UpdateDeploymentConfig(TwinCollection desiredProperties) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs:line 435
   --- End of inner exception stack trace ---
   at Microsoft.Azure.Devices.Edge.Agent.IoTHub.EdgeAgentConnection.UpdateDeploymentConfig(TwinCollection desiredProperties) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs:line 447
   at Microsoft.Azure.Devices.Edge.Agent.IoTHub.EdgeAgentConnection.ApplyPatchAsync(TwinCollection desiredProperties, TwinCollection patch) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs:line 411
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132
   at Microsoft.Azure.Devices.Edge.Agent.Core.Agent.ReconcileAsync(CancellationToken token) in /mnt/vss/_work/1/s/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs:line 132


edge-hub logs

<Paste here between the triple backticks>

Additional Information

More detailed example.
Deployment A has a module that looks like this. Note that there is a createOptions and a createOptions01.

"mosquitto": {
    "restartPolicy": "always",
    "settings": {
        "createOptions": "{\"HostConfig\":{\"LogConfig\":{\"Type\":\"local\",\"Config\":{\"max-size\":\"6m\",\"max-file\":\"2\"}},\"Privileged\":true,\"ExtraHosts\":[\"host.docker.internal:host-gateway\"],\"ExposedPorts\":{\"8884/tcp\":{}},\"PortBindings\":{\"1883/tcp\":[{\"HostIp\":\"127.0.0.1\",\"HostPort\":1883}],\"8884/tcp\":[{\"HostPort\":\"8884\"}],\"9001/tcp\":[{\"HostPort\":\"9001\"}],\"1888/tcp\":[{\"HostPort\":\"1888\"}]},\"Binds\":[\"/mnt/storage/mosquitto/data:/var/tmp/data\",\"/mnt/storage/mosquitto/log:/var/tmp/log\",\"/mnt/secure/mosquitto/certificates:/var/tmp/certificates:ro\",\"",
        "image": "foo.azurecr.io/aarch64/mosquitto:v1.0.0",
        "createOptions01": "/dev:/dev\"]},\"Cmd\":[\"sh\",\"-c\",\"run-mosquitto.sh\"]}"
    },
    "startupOrder": 100,
    "status": "running",
    "type": "docker"
},

Deployment B has a module that looks like this. Not only a createOptions.

"mosquitto": {
    "restartPolicy": "always",
    "settings": {
        "createOptions": "{\"HostConfig\":{\"LogConfig\":{\"Type\":\"local\",\"Config\":{\"max-size\":\"6m\",\"max-file\":\"2\"}},\"ExposedPorts\":{\"8884/tcp\":{}},\"PortBindings\":{\"1883/tcp\":[{\"HostIp\":\"127.0.0.1\",\"HostPort\":1883}],\"8884/tcp\":[{\"HostPort\":\"8884\"}]},\"Binds\":[\"/mnt/storage/mosquitto/data:/var/tmp/data\",\"/mnt/storage/mosquitto/log:/var/tmp/log\",\"/mnt/secure/mosquitto/certificates:/var/tmp/certificates:ro\"]},\"Cmd\":[\"sh\",\"-c\",\"run-mosquitto.sh\"]}",
        "image": "foo.azurecr.io/aarch64/mosquitto:v1.0.0"
    },
    "startupOrder": 100,
    "status": "running",
    "type": "docker"
},

In EdgeAgentConnections.cs (ApplyPathAsync) a JSON merge between A and B i done.

string mergedJson = JsonEx.Merge(desiredProperties, patch, true);

Sine the class JsonEx doesn't know that createOptions and createOptions01are special (chunked) it will merge it into this:

"mosquitto": {
    "restartPolicy": "always",
    "settings": {
        "createOptions": "{\"HostConfig\":{\"LogConfig\":{\"Type\":\"local\",\"Config\":{\"max-size\":\"6m\",\"max-file\":\"2\"}},\"ExposedPorts\":{\"8884/tcp\":{}},\"PortBindings\":{\"1883/tcp\":[{\"HostIp\":\"127.0.0.1\",\"HostPort\":1883}],\"8884/tcp\":[{\"HostPort\":\"8884\"}]},\"Binds\":[\"/mnt/storage/mosquitto/data:/var/tmp/data\",\"/mnt/storage/mosquitto/log:/var/tmp/log\",\"/mnt/secure/mosquitto/certificates:/var/tmp/certificates:ro\"]},\"Cmd\":[\"sh\",\"-c\",\"run-mosquitto.sh\"]}",
        "image": "foo.azurecr.io/aarch64/mosquitto:v1.0.0",
        "createOptions01": "/dev:/dev\"]},\"Cmd\":[\"sh\",\"-c\",\"run-mosquitto.sh\"]}"
    },
    "startupOrder": 100,
    "status": "running",
    "type": "docker"
},

The result is that createOptions01 is still left in the JSON. When later DockerConfig.cs takes that JSON object to create a docker instance it will see createOptions and createOptions01 and use them as a chunked string. The result in invalid JSON which leads to a halt in the deployment change.

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